Effective IP blacklisting in OpenWRT

in

Emerging Threats publishes excellent, free IP blacklists for general usage at servers and routers, in formats suitable for use with iptables and other popular firewalls. Unfortunately, on low memory and small CPU devices, loading ~1.5k iptables rules is a performance killer — here's how to do this more efficiently using ipset.

TL;DR If you're just looking for the scripts, head straight to GitHub kravietz/blacklist-scripts. Code samples published here are outdated and mostly serve as proof of concept.

At least on my TP-Link TL-WR2543N home router this became a problem at some point — initially I was just using emerging-IPTABLES-ALL.rules which basically executes iptables 1600+ times to add filtering rules, one for each IP. With 5 Mbps link this worked well, but when I upgraded my link to fibre optic 30 Mbps the router suddenly started to choke, system load skyrocketed and connections timed out.

Ipset is much more efficient mechanism, designed exactly for the purpose of fast processing of large IP and CIDR sets. Instead of having 1600+ iptables rules, you have an IP set:

Name: ETip
Type: hash:ip
Header: family inet hashsize 1024 maxelem 65536 
Size in memory: 18412
References: 1
Members:
81.169.178.214
23.249.160.114
83.96.139.80
...1600 more...

And an iptables rule referring to it:

Chain ETBLOCKLIST (2 references)
 pkts bytes target     prot opt in     out     source               destination         
   35  2940 LOGNDROP   all  --  *      *       0.0.0.0/0            0.0.0.0/0           match-set ETip src,dst 

The following script will create all necessary sets, rules and fill them in with fresh Emerging Threats blocklist. My script actually creates two IP sets, one for unicast IPs, the other one for CIDR blocks (ET list containst both) — this is for purely technical purposes (this is simplified script for demonstation only, for production look at the

# create ipsets if they don't exist yet
if ! ipset list|grep ETip; then ipset create ETip hash:ip; fi
if ! ipset list|grep ETcidr; then ipset create ETcidr hash:net; fi
if ! iptables -L ETBLOCKLIST; then iptables -N ETBLOCKLIST; fi
if ! iptables -L ETDROP; then iptables -N ETDROP; fi
 
if ! iptables -L input|grep -q ETBLOCKLIST; then
  iptables -I input 1 -j ETBLOCKLIST
fi
 
if ! iptables -L forward|grep -q ETBLOCKLIST; then 
  iptables -I forward 1 -j ETBLOCKLIST               
fi                                                                 
 
if [ $(iptables -L ETBLOCKLIST|wc -l) -lt 4 ]; then                  
  iptables -F ETBLOCKLIST                                            
  iptables -A ETBLOCKLIST -m set --match-set ETip src,dst -j ETDROP  
  iptables -A ETBLOCKLIST -m set --match-set ETcidr src,dst -j ETDROP
fi                                                                    
 
if [ $(iptables -L ETDROP|wc -l) -lt 3 ]; then                        
  iptables -F ETDROP                                                  
  iptables -A ETDROP -j LOG --log-level INFO --log-prefix "ET BLOCK: "
  iptables -A ETDROP -j DROP
fi 
 
tmp=$(mktemp)
 
wget -q http://rules.emergingthreats.net/fwrules/emerging-Block-IPs.txt -O "$tmp"
 
lines=$(wc -l "$tmp"|cut -f1 -d" ")
currentline=0
 
exec <"$tmp"
 
read line
while [ $currentline -lt $lines ]; do
  read line
  if echo "$line" | egrep -q "^[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}$"; then
    ipset -q -! add ETip "$line"
  fi
  if echo "$line" | egrep -q "^[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}/[0-9]{2}$"; then
   ipset -q -! add ETcidr "$line"
  fi
  currentline=$(expr $currentline + 1)
done
 
rm "$tmp"

The script can be installed in the Network > Firewall > Custom Rules section of the OpenWRT admini interface, which ensures it will be executed at boot at each time firewall is changed. This is however not quite sufficient, as blocklists are frequently changing and need to be frequently updated to avoid attacks from new sources and blocking networks that are long time abandoned by attackers.

For that purpose we install the following into OpenWRT menu System > Scheduled Tasks:

01 01 * * * sh /etc/firewall.user

If you don't have ipset, you need to install it from standard OpenWRT packages:

# opkg update
Downloading http://downloads.openwrt.org/attitude_adjustment/12.09/ar71xx/generic/packages/Packages.gz.
Updated list of available packages in /var/opkg-lists/attitude_adjustment.
# opkg install ipset
Package ipset (6.11-2) installed in root is up to date.

P2P blacklist

While Emerging Threats is good for general server and client-side usage, you might also look at iBlocklist if you're into P2P file sharing.

SSH bruteforcing blacklist

You might also consider having a blacklist installed on Internet servers, especially to protect the SSH server from bruteforcing.

In such case I would also recommend using ipset. To make things easier you might use FWBuilder to manage your firewall and it natively supports ipset assuming you have the following packages installed:

apt-get install ipset  xtables-addons-common

You might also consider using blacklist published by www.blocklist.de that collects data from a large group of fail2ban users. This list is much larger than the one from Emerging Threats — as of 2014 it contains around 22'000 entries.

If you don't use FWBuilder, you can use the following script — if placed in /etc/cron.daily/blacklist it will refresh the blacklist on daily basis.

#!/bin/sh
 
# ipset is not instaleld
if [ ! -x /usr/sbin/ipset ]; then
    echo "ipset tools are not installed. Install them with:"
    echo "apt-get install ipset (Ubuntu, Debian)"
    echo "yum install ipset (Fedora, CentOS, RedHat)"
fi
 
# blacklist does not exist
if ! ipset list|grep -q blacklist; then
    ipset create blacklist hash:net
fi
 
# blacklist is wrong type (e.g. hash:ip)
if ! ipset list blacklist|grep -vq 'Type: hash:net'; then
    ipset destroy blacklist
    ipset create blacklist hash:net
fi
 
# always reset the ipset so that stale entries are removed
ipset flush blacklist
tmp=$(mktemp)
tmp2=$(mktemp)
 
curl -s -o "$tmp" --compressed http://rules.emergingthreats.net/fwrules/emerging-Block-IPs.txt
curl -s -o - --compressed https://www.blocklist.de/downloads/export-ips_all.txt >>"$tmp"
 
# get rid of empty lines, comments and duplicates
egrep '^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+' "$tmp" | sort -u >"$tmp2"
 
exec <"$tmp2"
 
read line
while [ "$line" ]; do
        ipset add blacklist "$line"
        read line
done
 
rm "$tmp" "$tmp2"
 
if ! iptables -nL | grep -q 'Chain blacklist'; then
    iptables -N blacklist
else
    iptables -F blacklist
fi
 
if ! iptables -L INPUT | grep -q blacklist; then
    iptables -I INPUT -m state --state NEW -j blacklist
fi
 
if ! iptables -nL blacklist | grep -q "match-set"; then
    iptables -A blacklist -m set --match-set blacklist src -j LOG -m limit --limit 1/minute --log-prefix 'BLACKLIST '
    iptables -A blacklist -m set --match-set blacklist src -j DROP
fi

Difference between hash:ip and hash:net

IP set types hash:ip and hash:net can both store IP addresses and whole CIDR blocks. The difference is how CIDR blocks are stored in each of them.

If you add a CIDR block to hash:ip it will be expanded and stored as individual IP addresses, each occupying one entry. Adding a /24 block will occupy 255 entries, adding a /16 block will occupy 65534 entries, each entry taking some memory.

On the other hand, adding a CIDR block to hash:net will just make one item, taking less memory.

Here's an example:

# ipset list blacklist | wc -l
22405
[root@capmanagement cron.daily]# ipset list blacklist | head
Name: blacklist
Type: hash:net
Header: family inet hashsize 8192 maxelem 65536 
Size in memory: 413584
References: 2

And the members of hash:net can be both IP addresses, and CIDR blocks:

Members:
194.71.224.191
46.227.70.203
108.62.59.176
219.70.241.58
...
116.146.0.0/15
202.21.64.0/19