Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support to ban subnets #927

Open
K1LLUM1N471 opened this issue Jan 26, 2015 · 45 comments
Open

Support to ban subnets #927

K1LLUM1N471 opened this issue Jan 26, 2015 · 45 comments

Comments

@K1LLUM1N471
Copy link

Hello,

in which case does fail2ban support to ban subnets.
I used the instruction from http://cup.wpcoder.de/fail2ban-ip-blacklist/ and entered the subnet instead of the single IP, but fail2ban seems not to handle them at all.

Kind regards,
K1LLUM1N471

@leeclemens
Copy link
Contributor

I'm not sure where on that page it refers to subnets being blacklisted (ignoring /32 as a subnet). Generally only one IP fails to authenticate and subsequently gets blacklisted by fail2ban. Do you have a specific use case you are attempting to achieve? How would the subnet be determined from a single IP failing to log in?

@K1LLUM1N471
Copy link
Author

There is no specific use case, just a server in a sensitive area of
research and development, which has to be secured. The main reason is,
to block iterated unauthorised access from foreign countries e.g. China,
USA).
The determination of the associating subnet is currently done by hand
(Google + Subnet calculator).

Kind regards,
K1LLUM1N471

Am 28.01.2015 um 01:27 schrieb Lee Clemens:

I'm not sure where on that page it refers to subnets being blacklisted
(ignoring /32 as a subnet). Generally only one IP fails to
authenticate and subsequently gets blacklisted by fail2ban. Do you
have a specific use case you are attempting to achieve? How would the
subnet be determined from a single IP failing to log in?


Reply to this email directly or view it on GitHub
#927 (comment).

@sebres
Copy link
Contributor

sebres commented Feb 2, 2015

@K1LLUM1N471 I have a branch doing that, at the moment written relative #716 (as part of increment feature - ban-time-incr within factor plugin for observer). It can be easy configured in [jail] resp. [default] or as include in it, here is an example:

...
# If geo feature enabled (dictionaries "geo.country" or "geo.region" are specified):
#   - for each failure this factor appears like a simple "divider" for "maxretry" (inside of "findtime" interval);
#   - for time of each ban it is a simple multiplier coefficient besides "bantime.factor" ("jail.Factor"), 
#     to change default behavior use "ban.Factor" in expression "bantime.formula".

# The larger value of factor leads to faster ban (few failures) and longest time of ban by increase.

# "geo.country" - dictionary to define factor by country of IP address (country:factor pair)
#               (using of this method needs a Country, Region, or City database)
#geo.country =
geo.country = default:10
              DE:1 RU:2
...

Example above means - the factor for each country is 10, however for DE it is 1 and RU it is 2.

This feature is configurable to use geoip databases or cymru-like dns resolvers...

I will commit it this or next week and let know if as far as.

@leeclemens
Copy link
Contributor

@K1LLUM1N471 Can you help me understand "block iterated unauthorised access". Do you mean if an IP from RU gets banned, all IP space from RU gets banned? Or if one IP gets banned, for fail2ban to ban the entire subnet it is a member of (as allocated by the RIR)?

@K1LLUM1N471
Copy link
Author

@sebres Thanks for info. How reliable is the identification of a IP this way?

@leeclemens I guess it is the second case. When one IP got banned, all the other IPs from this subnet got banned, too.

For this reason I would prefer to set manually the array of IPs or subnets by the use of a blacklist, which is interpreted by Fail2Ban.
For example:
"subnet.blacklist" > "198.27.100.224/29"
or
"ip.blacklist" > "198.27.100.224 - 198.27.100.231"

Ban array of IPs by subnet or by defined array

Kind regards,
K1LLU1N471

@sebres
Copy link
Contributor

sebres commented Feb 13, 2015

@K1LLUM1N471

How reliable is the identification of a IP this way?

Good question, I would say how outdated the geoip database is.
This applies of course to cymru-like services also.

But this impacts failures/bans only, so if IP makes no failures - it country will not be taken into account.
After putting this into service I have decreased a value of banTime to 1m (because initial value, will be incremented + factored), to prevent a banning for long time for first failures from "foreign" countries.

@Hexasoft
Copy link

I +1 this request. I found that some China subnets work together for ssh-bruteforce : when one IP is banned an other one (very close to the 1st one) continues few minutes after.
To handle this I created an ad hoc script that takes IP to ban and check in a subnet list (from a file) and returns a subnet ban to iptables (i.e. X.Y.Z.W/24 rather than just X.Y.Z.W). And of course the same for deban.
But it would be better if this can be handled directly from fail2ban, because with my approach it don't consider several attempts from different IPs (in the subnet) as counting for the subnet ban: one IP must trigger the ban.

Regards,

Hexasoft

@justabaka
Copy link

You can try replacing <ip> in your action(s) with this:whois <ip> | grep route: | awk '{print $2}'. It will ban the whole subnet according to the whois data, not only /24 which may be not enough.

@XaF
Copy link

XaF commented Aug 2, 2015

As I had the same concern as you seem to have, I tried myself at making a special subnet banning system using fail2ban, called fail2ban-subnets, that you can find here.
It's a simple python script that you can configure quite easily and that will use the fail2ban.log file to identify subnets and log the ones that have a consequent (configurable) number of bans and IPs involved. It only supports banning up to /24 subnets currently as I didn't want to make it find too big subnets if there's innocent IPs in that range. I plan however on making that configurable too in the future if some people would need it.
I'm using it on a few servers currently and it works like a charm!
Hope this helps.

@infotek
Copy link

infotek commented Jun 6, 2016

I see reference by sebres using geoip to modify behaviour based on country of origin. Is this a concept that will be added to fail2ban soon?

> # If geo feature enabled (dictionaries "geo.country" or "geo.region" are specified):
> #   - for each failure this factor appears like a simple "divider" for "maxretry" (inside of "findtime" interval);
> #   - for time of each ban it is a simple multiplier coefficient besides "bantime.factor" ("jail.Factor"), 
> #     to change default behavior use "ban.Factor" in expression "bantime.formula".
> 
> # The larger value of factor leads to faster ban (few failures) and longest time of ban by increase.
> 
> # "geo.country" - dictionary to define factor by country of IP address (country:factor pair)
> #               (using of this method needs a Country, Region, or City database)
> #geo.country =
> geo.country = default:10
>               DE:1 RU:2

@ptempier
Copy link

ptempier commented Oct 5, 2016

What i do every few months and that i'd like to be upstream is permaban full subnets after too many recidives.

It goes like
ban an ip for a few minutes, unban it
do this a few times you hit a recidive rule and get banned for a day
a few dozen ip from the same subnet hit the recidive rule during a month, permaban the subnet
... no issues so far

it takes like 4_5_20 = 400 attempts before getting a permaban (didnt checked my exacts rules)
Seems fair to me; it takes a lot of attempts during an extended period of time to get permabaned
Any legitimate admin/user would notice an issue beforehand and the attempts would stop.

It works because these guys use whole ip ranges, are very persistant and switch to another ip of their subnet once they are hit by a recidive rule.

tbh 99 of the ips are from china, it s just that i thought it was too discrimnatory to geoban china

iptables-save | grep fail2ban |grep '/32' | wc -l
206
iptables-save | grep fail2ban |grep '/24' | wc -l
217
iptables-save | grep fail2ban |grep '/16' | wc -l
21

I ban /16 if there s too much /24 banned as reported by a whois

@rodrigorojasmoraleda
Copy link

Using @justabaka Idea, this works for me:

actionban = whois <ip> | grep route: | awk '{print $2}' | xargs -n1 -I{} iptables -I fail2ban-<name> 1 -s {} -j <blocktype>

actionunban = whois <ip> | grep route: | awk '{print $2}' | xargs -n1 -I{} iptables -D fail2ban-<name> 1 -s {} -j <blocktype>

@liar666
Copy link

liar666 commented May 2, 2017

@rodrigorojasmoraleda: this solution is really interesting, but unfortunately not generic enough.

Indeed, in my experience, I've seen that each whois server replies with its own format. The string "route:" does not necessarily appear in the answer. Thus many IP will get through your fail2ban.

For instance, using the 3 last IP addresses that attacked my companies' servers:

$ whois 14.119.66.49 | grep -F 'route:'
$ whois 139.227.188.103 | grep -F 'route:'
$ whois 138.219.29.18 | grep -F 'route:'

I've seen whois answers with "CIDR" or "inetnum" or "route", and generally they do not use the / notation, but an ip range :{

I'm sure it's possible to write a rule to manage all possible cases, but it would require to know them all. Also since the rule would be complicated, it would probably be better to wrap it inside a module/plug-in.

@toreit
Copy link

toreit commented Jun 12, 2017

When I use :
actionunban = whois | grep route: | awk '{print $2}' | xargs -n1 -I{} iptables -D fail2ban- 1 -s {} -j
Then met below error :

2017-06-09 17:37:01,323 fail2ban.action         [26026]: ERROR   whois 159.226.71.236 | grep route: | awk '{print $2}' | xargs -n1 -I{} iptables -w -D f2b-sshd 1 -s {} -j REJECT --reject-with icmp-port-unreachable -- stderr: b"iptables v1.6.0: Illegal option `-s' with this command\n\nTry \`iptables -h' or 'iptables --help' for more information.\n"

So removed 1 then it is ok. (Can anyone give some comment for this?)

Also found that results of whois use different format & keyword by each hosting carrier such as route/CIDR/inetnum IPv4 etc.
So wrote below script to handle this - referred several web pages(CIDR regex etc.)

#!/bin/bash
debug=0
checkCIDR="^(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])(\/([0-9]|[1-2][0-9]|3[0-2]))$"
checkIP="^(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])$"

function debugEcho()
{
    if [ $debug == 1 ] ; then
        echo "$*" 1>&2
    fi
}
function findCIDR()
{
    if [ ! -z "$1" ]; then 
        cidrData="$1"
        if [[ ! "$cidrData" =~ $checkCIDR ]]; then
            debugEcho "Try xx.xx/xx type"
            cidrData=`echo $1 | sed -E 's/^([0-9]{1,3}\.[0-9]{1,3})(\/([0-9]|[1-2][0-9]|3[0-2]))/\1.0.0\2/'`
            if [[ ! "$cidrData" =~ $checkCIDR ]] ; then
                debugEcho "$cidrData is not CIDR"
                debugEcho "Try xx.xx.xx/xx type"
                cidrData=`echo $1 | sed -E 's/^([0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3})(\/([0-9]|[1-2][0-9]|3[0-2]))/\1.0\2/'`
                if [[ ! "$cidrData" =~ $checkCIDR ]] ; then
                    debugEcho "$cidrData is not CIDR"
                    cidrData=""
                else
                    debugEcho "$cidrData is CIDR"
                fi
            else
                debugEcho "$cidrData is CIDR"
            fi
        else
            debugEcho "$cidrData is CIDR"
        fi
    else
        cidrData=""
    fi
    echo $cidrData
}
function findRange()
{
    localSubnet=""
    rangeData="$*"
    debugEcho "rangeData : $rangeData"
    # Check Range or not
    IP_1=`echo $rangeData | awk '{print $1}'`
    IP_2=`echo $rangeData | awk '{print $3}'`
    debugEcho "IP_1 : $IP_1"
    debugEcho "IP_2 : $IP_2"
    if [[ "$IP_1" =~ $checkIP && "$IP_2" =~ $checkIP ]]; then
        localSubnet=`ipcalc $rangeData | grep -m 1 -v deaggregate | awk '{print $1}'`
        if [[ ! "$localSubnet" =~ $checkCIDR ]]; then
            debugEcho "$localSubnet is not CIDR"
            localSubnet=""
        else
            debugEcho "$localSubnet is CIDR"
        fi
    else
        debugEcho "Invald Range"
    fi
    echo $localSubnet
    
}
function getSubnet()
{
    IP="$1"
    debugEcho "Input : $IP"
    debugEcho "Try Keyword : route"
    whoisResult=$(whois $IP)
    Subnet=`echo "$whoisResult" | grep -m 1 route: | awk '{print $2}'`
    if [ -z "$Subnet" ]; then 
        debugEcho "Keyword route result for $IP is null"
        debugEcho "Try Keyword : CIDR"
        Subnet=`echo "$whoisResult" | grep -m 1 CIDR | awk '{print $2}'`
        debugEcho "CIDR result is $Subnet"
        Subnet=$(findCIDR $Subnet)
    fi
    if [ -z "$Subnet" ]; then 
        debugEcho "Keyword CIDR is null"
        debugEcho "Try Keyword : inetnum"
        NetRange=`echo "$whoisResult" | grep -m 1 inetnum: | awk '{print $2 " - " $4}'`
        if [ ! -z "$NetRange" ]; then
            Subnet=$(findRange $NetRange)
            if [ -z "$Subnet" ]; then
                # inetnum result is not Range, check whether CIDR or not
                debugEcho "Check CIDR of inetnum ($NetRange)"
                Subnet=`echo $NetRange | awk '{print $1}'`
                Subnet=$(findCIDR $Subnet);
            fi
        else
            debugEcho "Keyword inetnum result for $IP is null."
        fi
    fi
    if [ -z "$Subnet" ]; then 
        debugEcho "Try Keyword : IPv4"
        NetRange=`echo "$whoisResult" | grep -m 1 "IPv4 Address" | awk '{print $4 " - " $6}'`
        debugEcho "Result of IPv4 is $NetRange"
        if [ ! -z "$NetRange" ]; then
            Subnet=$(findRange $NetRange)
        else
            debugEcho "Result of IPv4 for $IP is null try NetRange"
            Subnet=""
        fi
    fi
    if [ -z "$Subnet" ]; then 
        debugEcho "Try Keyword : NetRange"
        NetRange=`echo "$whoisResult" | grep NetRange | awk '{print $2 " - " $4}'`
        Subnet=$(findRange $NetRange)
        if [ -z $Subnet ]; then
            Subnet=`echo $Netrange | awk '{print $1}'`
            Subnet=$(findCIDR $Subnet)
        fi
    fi
    if [ -z "$Subnet" ]; then 
        debugEcho "Couldn't find Subnet, Use IP instead"
        Subnet=$IP
    fi
    echo $Subnet
}

if [ ! -z "$1" ]; then
    if [ -f "$1" ]; then
        while read -r line
        do
            result=$(getSubnet $line)
            debugEcho $line : $result
            echo $result
        done < $1
    else
        if [[ "$1" =~ $checkIP ]]; then
            result=$(getSubnet $1)
            debugEcho "$1 : $result"
            echo $result
        else
            debugEcho "Invalid IP : $1"
            echo INVALID
        fi
    fi
fi

iptable-allports.conf & iptable-multiport.conf

actionban = f2b.getSubnet.sh <ip> | xargs -n1 -I{} <iptables> -I f2b-<name> 1 -s {} -j <blocktype>
actionunban = f2b.getSubnet.sh <ip> | xargs -n1 -I{} <iptables> -D f2b-<name> -s {} -j <blocktype>

will be better if those are included in the module/plug-in.

@jn0
Copy link

jn0 commented Jun 16, 2017

Right now (Fail2Ban v0.9.6) we have

[jno@git fail2ban]$ sudo fail2ban-client -vvv set sshd banip 61.177.0.0/16
INFO   Loading configs for fail2ban under /etc/fail2ban 
DEBUG  Reading configs for fail2ban under /etc/fail2ban 
DEBUG  Reading config files: /etc/fail2ban/fail2ban.conf
INFO     Loading files: ['/etc/fail2ban/fail2ban.conf']
Level 7     Reading file: /etc/fail2ban/fail2ban.conf
INFO     Loading files: ['/etc/fail2ban/fail2ban.conf']
Level 7     Shared file: /etc/fail2ban/fail2ban.conf
INFO   Using socket file /var/run/fail2ban/fail2ban.sock
DEBUG  OK : '61.177.0.0/16'
DEBUG  Beautify '61.177.0.0/16' with ['set', 'sshd', 'banip', '61.177.0.0/16']
61.177.0.0/16

It could get into the database:

[jno@git fail2ban]$ echo 'select distinct ip from bans;' | sudo sqlite3 /var/lib/fail2ban/fail2ban.sqlite3 | grep 61.177
61.177.0.0/16
61.177.172.19
61.177.172.34

And finally:

[jno@git fail2ban]$ sudo iptables-save | grep 61.177
-A f2b-sshd -s 61.177.0.0/16 -j REJECT --reject-with icmp-port-unreachable
-A f2b-sshd -s 61.177.172.34/32 -j REJECT --reject-with icmp-port-unreachable

So, should it be closed now?

@ptempier
Copy link

My point of view on the original issue and need for this, and maybe that can help decide if you can close it.

Some hackers have controls of /24 and /16. The pattern is that if fail2ban bans an ip, their tools will understand this, and switch to another ip try to continue to connect. In these cases, fail2ban is of little uses because it ban individual ip. So what is needed is to ban the whole block to stop it.

Personally one thing i would like to see before the issue is closed is more integration. Like what has been done for recidive (no need to fiddle yourself in the logs, you can just enable recidive in fail2ban conf).

Maybe allow to enable actions/detection to manage things as subnet.
Like If the subnet hit 50 failed attempts or as already been banned 50 times, ban subnet?

@jn0
Copy link

jn0 commented Jun 19, 2017

Good idea.
But one have to decide on the source for that "subnets".
Whois on various servers? RA.net? Looking glasses?
How long should the found values be cached?
Should the whole "criminal" AS be banned (route to null would be fine here) along with its IPs?

@ptempier
Copy link

ptempier commented Jun 23, 2017

My process is manual. But what about toreit script to manage subnets?

Also maybe with an automated process, it s not necessary to handle anything else than /24 range as x.x.x.0-255 which are 90% of these ban anyway. As fail2ban would handle the ban in realtime and not every few months like me.

I permaban after about 400 attempts.
If the process was automated maybe fail2ban could ban faster; unban after 1week or 1month then reban if needed to avoid to keep a very long list.

@toreit
Copy link

toreit commented Jun 23, 2017

FYI,

During 10 days, I got results using my script above.

fail2ban-client status sshd & recidive

Status for the jail: sshd
|- Filter
|  |- Currently failed: 22
|  |- Total failed:     419
|  `- File list:        /var/log/auth.log
`- Actions
   |- Currently banned: 12
   |- Total banned:     57
   `- Banned IP list:   181.21.52.102 45.55.207.181 91.134.133.251 81.228.144.32 195.14.163.214 171.244.18.196 188.22.239.233 216.243.62.206 74.74.132.156 186.62.15.142 201.254.78.45 190.48.114.151

Status for the jail: recidive
|- Filter
|  |- Currently failed: 42
|  |- Total failed:     113
|  `- File list:        /var/log/fail2ban.log
`- Actions
   |- Currently banned: 27
   |- Total banned:     27
   `- Banned IP list:   103.89.88.182 104.131.50.215 111.40.166.130 113.252.218.224 115.195.119.3 117.0.12.108 121.150.125.212 125.227.128.173 171.49.178.208 182.100.67.120 186.121.240.62 189.44.10.114 198.0.148.211 2.177.129.3 209.93.132.216 211.44.43.165 212.83.142.251 45.243.21.106 61.177.21.226 64.179.211.161 81.137.204.43 86.104.15.15 93.149.10.162 93.187.16.70 94.233.189.208 103.68.42.168 103.79.141.150

iptables -L -n

Chain f2b-recidive (1 references)
target     prot opt source               destination         
REJECT     all  --  103.79.140.0/22      0.0.0.0/0            reject-with icmp-port-unreachable
REJECT     all  --  103.68.40.0/22       0.0.0.0/0            reject-with icmp-port-unreachable
REJECT     all  --  94.233.184.0/21      0.0.0.0/0            reject-with icmp-port-unreachable
REJECT     all  --  93.187.16.0/21       0.0.0.0/0            reject-with icmp-port-unreachable
REJECT     all  --  93.149.0.0/16        0.0.0.0/0            reject-with icmp-port-unreachable
REJECT     all  --  86.104.15.0/24       0.0.0.0/0            reject-with icmp-port-unreachable
REJECT     all  --  81.128.0.0/12        0.0.0.0/0            reject-with icmp-port-unreachable
REJECT     all  --  64.179.208.0/20      0.0.0.0/0            reject-with icmp-port-unreachable
REJECT     all  --  61.177.0.0/16        0.0.0.0/0            reject-with icmp-port-unreachable
REJECT     all  --  45.240.0.0/13        0.0.0.0/0            reject-with icmp-port-unreachable
REJECT     all  --  212.83.128.0/19      0.0.0.0/0            reject-with icmp-port-unreachable
REJECT     all  --  211.44.0.0/17        0.0.0.0/0            reject-with icmp-port-unreachable
REJECT     all  --  209.93.0.0/16        0.0.0.0/0            reject-with icmp-port-unreachable
REJECT     all  --  2.177.0.0/16         0.0.0.0/0            reject-with icmp-port-unreachable
REJECT     all  --  198.0.0.0/16         0.0.0.0/0            reject-with icmp-port-unreachable
REJECT     all  --  189.44.0.0/16        0.0.0.0/0            reject-with icmp-port-unreachable
REJECT     all  --  186.121.192.0/18     0.0.0.0/0            reject-with icmp-port-unreachable
REJECT     all  --  182.96.0.0/12        0.0.0.0/0            reject-with icmp-port-unreachable
REJECT     all  --  171.49.160.0/19      0.0.0.0/0            reject-with icmp-port-unreachable
REJECT     all  --  125.224.0.0/13       0.0.0.0/0            reject-with icmp-port-unreachable
REJECT     all  --  121.128.0.0/11       0.0.0.0/0            reject-with icmp-port-unreachable
REJECT     all  --  117.0.0.0/13         0.0.0.0/0            reject-with icmp-port-unreachable
REJECT     all  --  115.194.0.0/15       0.0.0.0/0            reject-with icmp-port-unreachable
REJECT     all  --  113.252.0.0/14       0.0.0.0/0            reject-with icmp-port-unreachable
REJECT     all  --  111.0.0.0/10         0.0.0.0/0            reject-with icmp-port-unreachable
REJECT     all  --  104.131.0.0/16       0.0.0.0/0            reject-with icmp-port-unreachable
REJECT     all  --  103.89.88.0/22       0.0.0.0/0            reject-with icmp-port-unreachable
RETURN     all  --  0.0.0.0/0            0.0.0.0/0           

Chain f2b-sshd (1 references)
target     prot opt source               destination         
REJECT     all  --  190.48.0.0/16        0.0.0.0/0            reject-with icmp-port-unreachable
REJECT     all  --  201.254.0.0/16       0.0.0.0/0            reject-with icmp-port-unreachable
REJECT     all  --  186.60.0.0/14        0.0.0.0/0            reject-with icmp-port-unreachable
REJECT     all  --  74.64.0.0/12         0.0.0.0/0            reject-with icmp-port-unreachable
REJECT     all  --  216.243.0.0/18       0.0.0.0/0            reject-with icmp-port-unreachable
REJECT     all  --  188.20.0.0/14        0.0.0.0/0            reject-with icmp-port-unreachable
REJECT     all  --  171.224.0.0/11       0.0.0.0/0            reject-with icmp-port-unreachable
REJECT     all  --  195.14.160.0/19      0.0.0.0/0            reject-with icmp-port-unreachable
REJECT     all  --  81.224.0.0/12        0.0.0.0/0            reject-with icmp-port-unreachable
REJECT     all  --  91.134.0.0/16        0.0.0.0/0            reject-with icmp-port-unreachable
REJECT     all  --  45.55.0.0/16         0.0.0.0/0            reject-with icmp-port-unreachable
REJECT     all  --  181.20.0.0/14        0.0.0.0/0            reject-with icmp-port-unreachable
RETURN     all  --  0.0.0.0/0            0.0.0.0/0           

@XaF
Copy link

XaF commented Jun 23, 2017

As I see the conversation is still running here, just a following comment to my previous one #927 (comment) to say that I'm still using my fail2ban-subnets tool on a few servers and that it still work to protect them. I am also still actively supporting it to improve it as needed.

@liar666
Copy link

liar666 commented Jun 23, 2017

I agree with XaF, I'm also still actively supporting an improvement of the subnet banning feature.

What I would see as important features:

  1. easy ban by geoip
    Indeed, China is the source of 99% of attacks on my companies' server and we don't do business with china, so being able to simply bann china as a whole woulb be great.

  2. easy ban of the whole subnet of the originating IP.
    But this would not act by blindly, by banning /xx , rather "intelligently", using the "whois" solution, but taking into account that different whois servers reply in different manners ("route:"/"CDIR:"/"inetnum:"/... , followed with either an IP-range or a subnet "shortcut")

@ramon-garcia
Copy link

Please do support ban by network number.
Many attackers change the IP address to circunvent fail2ban, with IP address within a range.

@rrauenza
Copy link

Reading over this I don't think I saw this suggested -- would it make more sense rather than banning a /24 when a particular /32 within the /24 hits a threshold to instead keep track of the entire /24 and when the aggregate /24 hits the threshold, ban the /24?

In a sense you would be treating the /24 as a single IP.

@cepheid666
Copy link
Contributor

cepheid666 commented Apr 2, 2019

I support @rrauenza's suggestion as an option alongside single-IP banning -- what would be REALLY great is an option to track failures on a given /24 subnet (or, even better, the entire CIDR determined from whois) so that it can track attackers who are using subnets to evade single-IP-based tracking. @XaF's and @toreit's scripts require the IPs to already have been banned, but many modern distributed botnets push an attack through a given IP only once every few hours, or maybe days, so single IPs may never get banned. But tracking failures per subnet would help to combat this.

So, for example, one could have:

findtime_perIP = 600 ; 10 minutes
maxretry_perIP = 3
findtime_perSubnet = 604800 ; 1 week
maxretry_perSubnet = 30

so that a given IP is banned for 3 failures in 10 minutes, but an entire subnet is banned for 30 failures (from that subnet) in 1 week even if no single IP within that subnet ever met the perIP ban criteria. This can help to combat these "slow burn" distributed botnets that use multiple IPs per subnet.

I wouldn't recommend JUST tracking the subnet because you don't want to ban an entire subnet when only one IP has failed... but tracking both single-IP and subnet, with different findtime/maxretry for each, allows knocking out single-IP attacks on a short-term basis while simultaneously handling subnet attacks longer-term.

@etron770
Copy link

etron770 commented Feb 11, 2020

thank you for your help, there is something what I do not understand
doing the banning wiht /24 (if it is really working) the result is:

$ fail2ban-client status subnets
Status for the jail: subnets
|- Filter
|  |- Currently failed: 805
|  |- Total failed:     1961
|  `- File list:        /var/log/fail2ban.subnets/fail2ban.sub.log
`- Actions
   |- Currently banned: 5
   |- Total banned:     5
   `- Banned IP list:   134.73.51.0 217.112.142.0 78.128.113.0 193.56.28.0
$ iptables -nL INPUT | grep 134.73.51

No entry for 134.73.51

doing:
fail2ban-client set subnets  banip 134.73.51.0/24
$ fail2ban-client status subnets

Status for the jail: subnets
|- Filter
|  |- Currently failed: 805
|  |- Total failed:     1995
|  `- File list:        /var/log/fail2ban.subnets/fail2ban.sub.log
`- Actions
   |- Currently banned: 6
   |- Total banned:     6
   `- Banned IP list:   134.73.51.0/24  134.73.51.0 217.112.142.0 78.128.113.0 193.56.28.0
$ iptables -nL INPUT | grep 134.73.51

No entry for 134.73.51Chain INPUT (policy ACCEPT))

$ iptables -nL INPUT
target     prot opt source               destination
f2b-subnets  tcp  --  0.0.0.0/0            0.0.0.0/0
f2b-recidive  tcp  --  0.0.0.0/0            0.0.0.0/0
f2b-mysqld-auth  tcp  --  0.0.0.0/0            0.0.0.0/0            multiport dports 3306

and maybe it is a good idea to add in recidive.conf

_jailnameignore = subnets
ignoreregex =   ^(%(__prefix_line)s| %(_daemon)s%(__pid_re)s?:\s+)NOTICE\s+\[(?!%(_jailnameignore)s\])(?:.*)\]\s+Ban\s+<HOST>\s*$

but the subnets jail is definitvely working

@sebres
Copy link
Contributor

sebres commented Feb 11, 2020

$ iptables -nL INPUT | grep 134.73.51
No entry for 134.73.51Chain INPUT (policy ACCEPT))

of course there is no entry, your chain (where fail2ban would add it) is called f2b-subnets, so take a look in:

iptables -nL f2b-subnets | grep 134.73.51

@etron770
Copy link

etron770 commented Feb 11, 2020

.... I will go walking for a while to cool down my brain ...

$ iptables -nL f2b-subnets
Chain f2b-subnets (1 references)
target     prot opt source               destination
REJECT     all  --  78.128.113.0/24      0.0.0.0/0            reject-with icmp-port-unreachable
REJECT     all  --  217.112.142.0/24     0.0.0.0/0            reject-with icmp-port-unreachable
REJECT     all  --  134.73.51.0/24       0.0.0.0/0            reject-with icmp-port-unreachable
RETURN     all  --  0.0.0.0/0            0.0.0.0/0

So the dirty workaround is working fine ....
hopefully with less false positives

@sebres
Copy link
Contributor

sebres commented Feb 11, 2020

So the dirty workaround is working fine

:)
As for your approach (batch file), may be I can help you to do it a bit simpler (without re-parse of logs etc).

In my own observer module (I have still to back-port several things to f2b to make it public, but no time ATM) I use our sqlite-database to determine a subnet, and use there a statement like this (basically I have not the bans only, but also failures, just current f2b-version does not have it):

select jail, rtrim(ip, replace(ip, '.', '')) as 'cidrp', count(ip) as 'tickets'
from bans
-- where enofban > now:
where datetime(timeofban + bantime, 'unixepoch', 'localtime') > datetime('now', 'localtime')
group by jail, cidrp
having tickets >= 5
order by tickets desc, jail

which retrieves the jails with /24 subnets with at least 5 active bans , so produces output like this:

jail cidrp tickets
some-jail-1 192.0.2. 16
some-jail-2 192.0.2. 5
... ... ...

Of course you can use it from batch (shell) using something like that:

sqlite3 'file:/var/lib/fail2ban/fail2ban.sqlite3?mode=ro' "$sql"

Note: open it readonly to avoid blocking of fail2ban to long time.

In scenario like this you would not need new jail and new action:

foreach jail, cidrp [from_statement_result] {
  fail2ban-client set $jail banip $cidrp.0/24
}

Proper solution should still unban all IPs matching this cidr/mask, but we're speaking about a workaround here.

@etron770
Copy link

etron770 commented Feb 11, 2020

Thank you very much, I will try this - better than re-parsing logs
The server load is 0.12 (12 cores) so in this case re-parsing logs is not a problem but dirty ..
In my opinion it will be inportant for the future.
actually I have three Class C networks bypassing all f2B rules:
45.125.66.0/24 Logfile -> #2627
141.98.10.0/24
185.36.81.0/24
tryining to test usernames

@cepheid666
Copy link
Contributor

Just to remind that @XaF has a very similar script that will find problem subnets and ban the entire /24, see #927 (comment). @toreit also posted a script, see #927 (comment). Perhaps these can help.

@etron770
Copy link

etron770 commented Feb 12, 2020

 select jail, rtrim(ip, replace(ip, '.', '')) as 'cidrp', count(ip) as 'tickets'
 from bans
 -- where enofban > now:
 where datetime(timeofban + bantime, 'unixepoch', 'localtime') > datetime('now', 'localtime')
 group by jail, cidrp
 having tickets >= 5
 order by tickets desc, jail

but .. i do not need the bans, because they bypass all ban rules with the Class C net.
I need the "FOUND"s

@sebres
Copy link
Contributor

sebres commented Feb 12, 2020

but .. i do not need the bans ... I need the "FOUND"s

But I told you already in the description: "basically I have not the bans only, but also failures, just current f2b-version does not have it.", so all my statements finding single failures would not help you with your stock fail2ban.

Well, one possibility were v.0.11.1 with enabled bantime.increment (#1460) and small initial maxretry (e. g. 1 or 2) and bantime (e. g. 5 seconds) or even with increased findtime.
In this case they going to be banned faster (and longer if become known as bad) and 5-10 seconds were also bearable on some false positive.

Another possibility would be an enhancement like requested in #2304 (which is not yet ready too at the moment).

If I get time, I'll try to fulfill a back-porting of my observer to provide regular solution for that.

@9607835
Copy link

9607835 commented May 13, 2020

so many people talking about whois, why would any of you use whois?!? whois is completely useless! I have never used more than a single IP address (yes it changes every few months; irrelevant), yet whois always says that I am part of a '/24' subnet (or larger)!

#927 (comment)
finally a half intelligent recommendation, (if you ignore talk of using whois)...

here is the simplest solution that doesn't depend on whois
findtime = 600 ; 10 minutes
maxretry_perIP = 3
avgretry_perIP = .01
density = 0.0001 ; expected number of customer IPs per possible IP

you do not treat a subnet like a single ip address without adjusting the "maxretry"'s as well...
you DO NOT ban subnets by examining the number of individual IP or subnet bans, because I, as a hostile local Internet registry (LIR), could just do single use scans with my whole multiple subnet spanning network and never trigger ANY individual ip or subnet bans.
you DO NOT check just /24 bans since some networks could be /16. Do you want to allow hundreds of '/24' subnets the opportunity to be individually banned one at a time when you can just ban the whole /16 subnet at once?!?! same issue with whois, it is either too wide such as in my case, or it is to narrow in the case of a hostile chinese LIR (only a matter of time, if it has not happened already) covering several whois based subnets.

so just monitor every subnet level!
now the only question that remains is how should the maxretry vary with subnet size...
like this: maxretry_'/n'=maxretry*ceil(betaCDF^-1(.95;1+density*2^(32-n),1+(1-density)*2^(32-n)))

where p=betaCDF^-1(q;a,b) such that q=betaCDF(p;a,b) where a is the estimate of the number of expected real IPs in the subnet based on density=number_of_customers/2^(32-n).

The use of .95 as an upper bound would have to be increased based on the number of subnet sizes that are considered to avoid false positives. for example: .95^(1/32)=.998... (for IPv4).

As the size of the subnet increases maxretry_'/n'/2^(32-n) will decrease and gradually approach density*maxretry_perIP. this is appropriate since with a small subnet size there can be an increased chance that every IP in that subnet is a valid customer who should be given maxretry_perIP-1 chances, but as the subnet size increases the probability of every IP being a valid customer decreases... this is the whole point of using the beta/binomial distribution.

Since the ratio decreases as the subnet size increases, you should be concerned that a large subnet would get banned based on the actions of only a few smaller subnets, but keep in mind that the upper bound of the binomial distribution can be approximated by density+O(1/sqrt(2^(32-n))) due to the central limit theorem, and so the ratio decreases slowly compared to the size of the subnets.

We should also change maxretry into more of a statistical distribution, because the odds of all customers within a subnet entering wrong passwords in the same findtime period is exponentially small. maxretry should instead be based on two configuration parameters avgretry_perIP and maxretry_perIP(where maxretry_perIP is considered the upper 95/99/etc.% tolerance interval of the distribution given by the number of events in a single findtime period with avgretry_perIP being the average number of events in that findtime period), that way we can reduce the expected number of retries based on subnet size. This can be done with an inverse poisson/gamma distribution via. maxretry=poissonCDF^-1(poissonCDF(maxretry_perIP;avgretry_perIP);2^(32-n)*avgretry_perIP)/2^(32-n). The poisson distribution assumes events are naturally independent, which could easily not be the case (like wrong passwords: when you enter a wrong password you are much more likely to enter a wrong password a second time), so other distributions with heavier tails should be used instead that do not converge so quickly like the cauchy distribution (which never converges >:-p ).

I hope this addresses everyone's concerns and that we can now focus on providing a flexible solution that requires only two new configuration parameters: density and avgretry_perIP; without requiring the tedious coding of whois parsing for every single possible whois format that can be provided, or the code upkeep required to accommodate new formats.

I'm not familiar enough with IPv6, so treating '/64' in the same way as an individual IPv4 address may be invalid... and may require something like whois or a third configuration parameter.

@sebres
Copy link
Contributor

sebres commented May 14, 2020

finally a half intelligent recommendation, (if you ignore talk of using whois)...

FWIW, just to clarify our intention, no one of us had really planned to use whois to identify a subnet (at most to determine and cache its country possibly, even to provide mechanism to increase ban factor by non target audience e. g. do a factorization depending on countries configuration).
For a subnet recognition is enough a simple self-adaptive algorithm with some burst, automatically increasing CIDR mask for a subnet depending on its banned IPs. Burst value can be used to exclude (or rather to find proper subnet) the IPs depending on it bantime, country factor, etc pp, e.g. IPs/subnets with high and low ban-time will be combined to different subnets.

But current main problem is not a recognition of a net, rather it is a good implementation of conjoining several banned tickets after such determination to the single ticket with resulting adjustment of all properties and reban them all as a single entry.

now the only question that remains is how should the maxretry vary with subnet size...

This is also not an issue at all, because v.0.11 already uses a formula to manipulate maxretry value depending on ban-count (as well as other factors). It is relative simply to extend that with other factor also considering subnet "malice" (or even to provide that for the user to configure by himself).

I hope this addresses everyone's concerns and that we can now focus on providing a flexible solution that requires only two new configuration parameters

As already said this will be rather secondary. Primary it is internal handling inside ban-manager and actions. And of course the time to implement. Because it is always a scarce resource.

@habazut
Copy link

habazut commented May 19, 2020

But current main problem is not a recognition of a net, rather it is a good implementation of conjoining several banned tickets after such determination to the single ticket with resulting adjustment of all properties and reban them all as a single entry.

For me it would be enough if the first IP that matches some whois or geoip criteria would start a ban of the whole /24 in which it resides and then the ban is hold as long as any IP in the range still is banned and when the last IP of the /24 is released then the whole range is released. I tried to do that with ugly ban and unban scripts but it is easy to get out of sync when much ban/unban action is going on in a subnet.

Harald.

@sebres
Copy link
Contributor

sebres commented May 19, 2020

For me it would be enough if the first IP that matches some whois or geoip criteria would start a ban of the whole /24

Well conditionally it is possible right now without any modification of fail2ban core functionality.
Just write a new action (like iptables-whois-subnet that would determinate a subnet from IP and finally ban that. So just copy iptables action, add some selector for example:
subnets=$(whois "<ip>" | awk '/^CIDR:/ {print $2;}') to retrieve subnets (or even set fixed mask like ip="<ip>/24") and ban every subnet in some iterator (same as actionban and actionunban where <ip> replaced by $ip).

and then the ban is hold as long as any IP in the range still is banned

If you ban a subnet you should have exactly one entry (single IP from the subnet) because other IPs would not be able to connect anymore. Single exception is if some IPs starting their attempts (and so generating failures) simultaneously, so one have to ignore the same banned subnet from following addresses.
That is what fail2ban already doing (with "Already banned" message), but it is not applicable to the subnet at the moment, because the ticket does not get expanded CIDR as an ID and fail2ban would check single IP address only (so such ignore is expected in action).

Also if the IP is not expanded to a subnet in fail2ban core (but in action only), ban-time-increment feature is impossible (because also applied for single IP only).

So better would be to implement something like actionexpand doing that (related to some configuration) e.g. return subnet and switch it in the ticket or even an actionless handling (with a configuration in jail) working regardless of the action.

@ruppel
Copy link

ruppel commented Jul 17, 2020

Since I setup my internet linux server, and checked the fail2ban logs, I deserve a solution for banning complete subnets.
I don't understand the solutions given in this thread, so I tried to implement something.
Here you can see my work: https://github.com/ruppel/fail2ban-recidive-subnet

@varac
Copy link

varac commented Sep 27, 2020

https://sven.rojek.de/posts/fail2ban-iprange-mit-blackliste-blocken looks like a good solution with plain fail2ban (article in german but english config snippets should make the approach clear).

@sbesl
Copy link

sbesl commented Dec 10, 2020

Just to add to some of the other solutions. I find it easiest to use below script that I hook into my actionban statements to capture CIDR from whois (yes, I prefer that over guessing some CIDR, but you could also skip most of it and stick /24 after the host) and sending it to syslog. That way I can setup a new jail to monitor those syslog entries with tweaked values for maxretry and findtime.

Here's the python3 script I use (pure python):

#!/usr/bin/python

import sys, subprocess, ipaddress, syslog

def narrowest_cidr(cidrA, cidrB):
    _ip, subA = cidrA.split('/')
    _ip, subB = cidrB.split('/')

    if int(subA) > int(subB):
        return cidrA
    else:
	return cidrB

bad_ip = sys.argv[1]
cidrs = []
inetnums = []
ret = None

whois = subprocess.run(['whois', bad_ip], text=True,
        stdout=subprocess.PIPE, check=True)

for line in whois.stdout.split('\n'):
    if 'CIDR:' in line:
        cidrs.append(line.replace('CIDR:', '').strip())
    if 'inetnum:' in line:
        inetnums.append(line.replace('inetnum:', '').strip())

if len(cidrs) >= 1:
    if len(cidrs) == 1:
        cidr = cidrs[0]
    else:
	cidr = narrowest_cidr(cidrs[0], cidrs[-1])
elif len(inetnums) > 0:
    if len(inetnums) == 1:
        inetnum = inetnums[0]
        startip, endip = inetnum.split(' - ')
        cidrs = [ipaddr for ipaddr in ipaddress.summarize_address_range(ipaddress.IPv4Address(startip), ipaddress.IPv4Address(endip))]
        if len(cidrs) == 1:
            cidr = cidrs[0]
        else:
            cidr = narrowest_cidr(cidrs[0], cidrs[-1])
else:
    cidr = "no CIDR found"

syslog.openlog(ident="suspectrange")
syslog.syslog("{} has CIDR {}".format(bad_ip, cidr))

@Strykar
Copy link
Contributor

Strykar commented Dec 23, 2022

What you're noticed as a mix - is one of many prerequirements that would help to implement this enhancement.

On the subject of determining when a group of offending IPs from a subnet have crossed a threshold for a /24 block..
Another idea is to take assistance from a exactly such a service since fail2ban does not know, most offer APIs to query IPs easily integrated into f2b Actions.

alexgarel added a commit to alexgarel/fail2ban that referenced this issue Sep 15, 2023
Adding the "flags interval;" in addr-set definition enables the manual ban of ip addresses interval using a subnet mask (eg: 10.0.0.0/24).

Relates to: 
- fail2ban#927
@zaphod77
Copy link

Distributed brute force attacks are really becoming an issue here.

I need a simple solution to detect the following.

  1. round robin single attempts. it goes through the entire cidr for each attempt. So we need to catch many failures from the same netblock in a short time, even if there was only one attempt per IP, and ban the entire netblock.
  2. shifting addresses after a ban. if a certain number of ip addresses in the same netblock get banned, again, ban the entire netblock.

Unfortunately, this would clearly allow for denial of service, as there are some very large essentially public blocks and the customer is likely to be in one of them. So we need an exception for netblocks that have registered recent successful logins, to avoid locking out the customer.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests