Block thousands of bad IPs with iptables and ipset, using a free list that updates itself

Block thousands of known attackers at the Linux firewall with ipset or nftables and a free list rebuilt every six hours. Tested scripts and measured numbers.

To block a long list of IP addresses on Linux, put them in an ipset set and drop the whole set with one iptables rule. Refresh it by loading a new copy beside the old one and swapping the two, so the firewall never runs half loaded. nftables does the same with sets of its own. In our test, FFraud's free list of 2,000 confirmed attackers loaded into ipset in 0.007 seconds and took about 64 KB of kernel memory.

#Why use ipset instead of one rule per address?

iptables examines the rules of a chain one after another until one matches (iptables manual). A list of 2,000 addresses as 2,000 rules means up to 2,000 checks for every packet that matches none of them. ipset keeps the addresses in a set inside the kernel, and one iptables rule matches the whole set (ipset and iptables manuals).

We loaded three lists into ipset and into an nftables set on October 23, 2026. The test ran on Linux kernel 6.6 with ipset 7.19 and nftables 1.0.9, and every set, rule and table was removed afterwards.

Time to load an IP list into the kernelSeconds to load each list into ipset (ipset restore) and into an nftables set (nft -f), measured October 23, 2026
  1. ipset, 2,000 IPs0.01 s
  2. ipset, 6,403 IPs0.02 s
  3. ipset, 1,556,296 IPs4.44 s
  4. nftables, 2,000 IPs0.02 s
  5. nftables, 6,403 IPs0.04 s
  6. nftables, 1,556,296 IPs6.71 s

Source: FFraud test on Linux 6.6 (WSL), ipset 7.19, nftables 1.0.9; FFraud free lists (05:17 UTC build) and open data (09:30 UTC build)

ListEntriesipset memory
webattack.txt2,00064 KB
All four lists6,403163 KB
Full open data1,556,29639.2 MB

The memory column is ipset's own count. Even the full IPv4 part of FFraud's open data fits: 39.2 MB in the kernel, against 736.6 MB for one nginx worker holding the same list in our nginx test. Matching was right in every case: each listed address was found in its set, and the documentation address 198.51.100.7 (RFC 5737) was not.

#Which free list should you load?

FFraud publishes four plain-text lists, strongest entries first, rebuilt every six hours. Three hold IPv4 addresses only; freships.txt also carried 27 IPv6 addresses on October 23, which the scripts below skip. For a server firewall that covers every port, badips.txt is the usual choice.

ListWhat it holdsIPs
badips.txtConfirmed abusive 6 or more times in 14 days2,000
webattack.txtSeen attacking web apps, confirmed 4 or more times2,000
worst.txtConfirmed abusive 10 or more times in 14 days500
freships.txtFirst seen in the last 7 days, confirmed 3 or more times2,500

A hash set holds at most 65,536 entries unless you raise maxelem when you create it (ipset manual). The free lists stay far below that. The full open data needs maxelem 2000000 and about four seconds per load.

#How do you set it up with ipset and iptables?

Install ipset first, with sudo apt install ipset on Debian and Ubuntu. Then save this script as /usr/local/bin/ffraud-ipset-blocklist and make it executable:

/usr/local/bin/ffraud-ipset-blocklist
#!/bin/sh
# Refresh the kernel blocklist "ffraud" from FFraud's free list and swap it in atomically.
# A failed download or a short list leaves the current set untouched.
set -eu
LIST_URL="https://ffraud.com/free/badips.txt"
SET=ffraud
ALLOW=/etc/ffraud-allowlist.txt   # your own addresses, one per line (optional)

tmp=$(mktemp)
trap 'rm -f "$tmp" "$tmp.raw"' EXIT

curl -fsS --max-time 30 "$LIST_URL" -o "$tmp.raw"
grep -E '^[0-9]{1,3}(\.[0-9]{1,3}){3}$' "$tmp.raw" \
  | if [ -s "$ALLOW" ]; then grep -v -x -F -f "$ALLOW"; else cat; fi > "$tmp" || true

n=$(wc -l < "$tmp")
if [ "$n" -lt 100 ]; then
  echo "only $n addresses in the download, keeping the current set" >&2
  exit 1
fi

# Load into a second set, then swap: the live set is never half full.
ipset create -exist "$SET" hash:ip family inet maxelem 65536
ipset create -exist "$SET-new" hash:ip family inet maxelem 65536
ipset flush "$SET-new"
sed "s/^/add $SET-new /" "$tmp" | ipset restore -exist
ipset swap "$SET-new" "$SET"
ipset destroy "$SET-new"

# Add the firewall rule once; later runs find it and leave it alone.
iptables -C INPUT -m set --match-set "$SET" src -j DROP 2>/dev/null \
  || iptables -I INPUT -m set --match-set "$SET" src -j DROP
echo "loaded $n addresses into $SET"
  • The swap. ipset swap exchanges the two sets in one step (ipset manual), so packets meet either the old list or the new one. It took 3 milliseconds in our test, even for the full list.
  • The guard. A download with fewer than 100 addresses, such as an error page, is refused and the current set stays.
  • Your addresses. Lines in /etc/ffraud-allowlist.txt never reach the set, so your own office or monitoring address cannot lock you out.
  • One rule. iptables -C checks for the rule first, so a second run does not add a second copy.

Run it at boot and every six hours

ipset keeps its sets in the running kernel, so a reboot starts without them. A systemd timer runs the script shortly after boot and then every six hours, and Persistent=true catches up on a run missed while the machine was off (systemd manual):

/etc/systemd/system/ffraud-ipset-blocklist.service and .timer
# /etc/systemd/system/ffraud-ipset-blocklist.service
[Unit]
Description=Refresh the FFraud kernel blocklist
Wants=network-online.target
After=network-online.target

[Service]
Type=oneshot
ExecStart=/usr/local/bin/ffraud-ipset-blocklist

# /etc/systemd/system/ffraud-ipset-blocklist.timer
[Unit]
Description=Refresh the FFraud kernel blocklist at boot and every six hours

[Timer]
OnBootSec=30s
OnCalendar=*-*-* 00/6:40:00
RandomizedDelaySec=10m
Persistent=true

[Install]
WantedBy=timers.target
shell
sudo chmod +x /usr/local/bin/ffraud-ipset-blocklist
sudo /usr/local/bin/ffraud-ipset-blocklist
sudo systemctl daemon-reload
sudo systemctl enable --now ffraud-ipset-blocklist.timer

#How do you do the same with nftables?

nftables has sets built in (nftables wiki), so no extra tool is needed. A file loaded with nft -f is applied as one atomic operation that replaces the old configuration (nftables wiki). This script builds such a file: the table, its rule, and the new contents of the set:

/usr/local/bin/ffraud-nft-blocklist
#!/bin/sh
# Refresh the nftables set "blocked" in table inet ffraud from FFraud's free list.
# nft applies the whole file as one transaction, so the set is replaced in one step or not at all.
set -eu
LIST_URL="https://ffraud.com/free/badips.txt"
ALLOW=/etc/ffraud-allowlist.txt   # your own addresses, one per line (optional)

tmp=$(mktemp)
trap 'rm -f "$tmp" "$tmp.raw" "$tmp.nft"' EXIT

curl -fsS --max-time 30 "$LIST_URL" -o "$tmp.raw"
grep -E '^[0-9]{1,3}(\.[0-9]{1,3}){3}$' "$tmp.raw" \
  | if [ -s "$ALLOW" ]; then grep -v -x -F -f "$ALLOW"; else cat; fi > "$tmp" || true

n=$(wc -l < "$tmp")
if [ "$n" -lt 100 ]; then
  echo "only $n addresses in the download, keeping the current set" >&2
  exit 1
fi

{
  echo 'table inet ffraud {'
  echo '  set blocked { type ipv4_addr; }'
  echo '  chain input { type filter hook input priority -10; policy accept; }'
  echo '}'
  echo 'flush chain inet ffraud input'
  echo 'add rule inet ffraud input ip saddr @blocked drop'
  echo 'flush set inet ffraud blocked'
  printf 'add element inet ffraud blocked { %s }\n' "$(paste -sd, "$tmp")"
} > "$tmp.nft"
nft -f "$tmp.nft"
echo "loaded $n addresses into inet ffraud blocked"

The table lives on its own, so it does not touch the rest of your nftables rules. Run it with the same timer, with ExecStart pointing at this script. Two runs in a row gave one rule and 2,000 addresses in our test.

#Does it work behind Cloudflare or a load balancer?

Behind Cloudflare, every connection to your server comes from Cloudflare, which acts as a reverse proxy. The visitor's address arrives only inside the CF-Connecting-IP header (Cloudflare docs), and a firewall never reads headers. A kernel blocklist then protects only the ports that skip the proxy, such as SSH. Block web visitors in the web server instead, with the real client address, as our nginx guide shows.

#What about ufw?

ufw is in part a front end for iptables-restore and keeps custom rules in /etc/ufw/before.rules (ufw manual). The script's rule sits at the top of the INPUT chain, so packets meet it before ufw's own chains. If a ufw reload ever removes it, the next timer run adds it back.

#How do you check that it works?

  1. Run sudo ipset list ffraud -t and read the number of entries.
  2. Pick an address from the list and run sudo ipset test ffraud with it. ipset answers that it is in the set.
  3. Run sudo iptables -L INPUT -v -n after a few hours. The packet counter on the match-set ffraud rule shows how much the set has dropped.

We tested both scripts on October 23, 2026: a normal run, a second run, an allowlisted address, a download that answered 404, and a download that was a web page in place of a list. The set stayed unchanged after each failure, and no rule was ever added twice.

#How do you undo it?

shell
sudo systemctl disable --now ffraud-ipset-blocklist.timer
sudo iptables -D INPUT -m set --match-set ffraud src -j DROP
sudo ipset destroy ffraud
# nftables version
sudo nft delete table inet ffraud

#Questions people ask

How do I completely block one IP address with iptables?

Drop what it sends with sudo iptables -I INPUT -s 203.0.113.9 -j DROP, and stop traffic towards it with sudo iptables -I OUTPUT -d 203.0.113.9 -j DROP. Replace the documentation address with the one you want to block.

How do I block a list of specific IPs with iptables?

Put the addresses in an ipset set and add one rule with -m set --match-set <set> src -j DROP. One rule per address works for a handful, and slows down as the list grows.

How do I use an ipset to block IPs?

Create a set with ipset create, add addresses with ipset add or ipset restore, then match it in iptables. For a list that changes, load a second set and swap it in, as the script above does.

Can ipset block whole countries?

Yes. A hash:net set holds network ranges, so a country's ranges from a geolocation database work with the same kind of rule. Country blocks also stop real visitors, so use them only where nobody from that country should reach you.

Will the blocklist survive a reboot?

The set itself does not, because it lives in kernel memory. The timer's OnBootSec runs the script 30 seconds after boot and rebuilds both the set and the rule.

To see why an address is on the list, look it up in the free IP blacklist check.

Sources

  1. ipset(8) manual, netfilter.org
  2. iptables-extensions(8): set match, netfilter.org
  3. iptables(8) manual, netfilter.org
  4. Sets, nftables wiki
  5. Atomic rule replacement, nftables wiki
  6. systemd.timer(5), freedesktop.org
  7. ufw-framework(8), Ubuntu
  8. Restoring original visitor IPs, Cloudflare
  9. RFC 5737: IPv4 Address Blocks Reserved for Documentation, IETF, January 1, 2010
  10. Free list badips.txt, build of 05:17 UTC, FFraud, October 23, 2026
  11. ip-fraud-database, build of 09:30 UTC, FFraud, October 23, 2026