Block whole networks cheaply with ipset and a bash script

Every ban in a firewall is a rule, and every rule is checked for every packet. A list of ten thousand addresses turns into ten thousand rules in order. ipset stores the addresses in a hash table instead, so one iptables rule checks the set in a single lookup, whatever its size.

Install ipset

bash
sudo apt update
sudo apt install -y ipset

Keep the list in a text file

One address or CIDR range per line. Comments start with #. Keep the file under version control, so every change has a reason.

text
# /etc/denylist.txt
203.0.113.45
198.51.100.0/24

Load the set with a script

The script creates the set if needed, rebuilds it from the file, and adds the single iptables rule that drops anything in the set. It reads the last line even when the file has no trailing newline.

bash
#!/usr/bin/env bash
# block-list.sh: load /etc/denylist.txt into an ipset and drop matches. Run as root.
set -euo pipefail

SET=denylist
LIST=/etc/denylist.txt

ipset create "$SET" hash:net -exist
ipset flush "$SET"

while read -r entry || [[ -n "$entry" ]]; do
  [[ -z "$entry" || "$entry" == \#* ]] && continue
  ipset add "$SET" "$entry" -exist
done < "$LIST"

iptables -C INPUT -m set --match-set "$SET" src -j DROP 2>/dev/null \
  || iptables -I INPUT 1 -m set --match-set "$SET" src -j DROP

echo "denylist entries: $(ipset list "$SET" | awk '/Number of entries/ {print $4}')"

Test it

Run the script, then check one address from the list and one outside it. ipset test exits 0 when the address is in the set.

bash
sudo bash block-list.sh
sudo ipset test denylist 198.51.100.7 && echo "198.51.100.7 is blocked"
sudo ipset test denylist 192.0.2.10 || echo "192.0.2.10 is not blocked"

Keep it across reboots

ipset sets live in memory, so a reboot empties them. Save the set once it's right, restore it at boot, and run the load script after that if the file has changed.

bash
sudo ipset save > /etc/ipset.rules
sudo ipset restore < /etc/ipset.rules

Keep the list short

A range can cover more people than the abuse you're blocking. Review every /24 before you add it, check your own logs for the addresses you care about, and remove entries that stop showing up. Keep the rules that protect SSH and the public site in the firewall script, not in this list.

Checking your own response headers by hand gets old fast. osec Scan grades them in seconds, free.

Flow: deny list, ipset load, one rule, lookup, save, review
One rule matches a whole set of addresses and ranges.

Sources

• ipset project documentation: https://ipset.netfilter.org/
• iptables set match (netfilter): https://ipset.netfilter.org/iptables-extensions.man.html