Block Countries by IP on Debian Trixie with nftables and xtables-addons

Debian Trixie uses nftables as its default firewall. If you’re used to iptables, the commands still work — but they go through an iptables-nft compatibility shim that translates them to nftables rules under the hood. For country-based IP blocking, the cleanest approach is xtables-addons with its built-in GeoIP module. It lets you drop entire countries at the kernel level — traffic never reaches your web server. 🔐

Check Your Current Firewall

First, confirm nftables is active:

1
nft list ruleset

If you get output (even empty table blocks), you’re on nftables. If the command isn’t found, install it:

1
2
apt install nftables
systemctl enable --now nftables

Install xtables-addons and GeoIP Tools

1
apt install xtables-addons-common libtext-csv-xs-perl curl

xtables-addons provides the -m geoip match extension. The Perl module is needed by the GeoIP database download script.

Download the GeoIP Database

MaxMind is the company behind GeoLite2 — the free IP geolocation database that xtables-addons uses. MaxMind offers three GeoLite2 databases: Country, City, and ASN. For country-based blocking, you only need GeoLite2 Country — the City and ASN databases serve different purposes (detailed location data and ISP lookup, respectively) and are not used by xt_geoip_build. You need a free account to download it. There’s no credit card required.

Sign up at https://www.maxmind.com/en/geolite2/signup. After confirming your email and logging in, go to Account → Manage License Keys → Generate new license key. Copy it immediately — it’s only shown once.

The GeoIP data lives in /usr/share/xt_geoip/. You have two paths to get it there:

Path A — Manual download

Download the CSV zip directly using your license key, unzip it, then build the binary database:

1
2
3
4
5
6
7
8
9
# Download the GeoLite2 Country CSV (replace YOUR_LICENSE_KEY)
curl -O "https://download.maxmind.com/app/geoip_download?edition_id=GeoLite2-Country-CSV&license_key=YOUR_LICENSE_KEY&suffix=zip"
unzip 'GeoLite2-Country-CSV*.zip'

# Create the target directory
mkdir -p /usr/share/xt_geoip

# Build the binary database from the unzipped folder
/usr/lib/xtables-addons/xt_geoip_build -D /usr/share/xt_geoip GeoLite2-Country-CSV_*/

Path B — geoipupdate (recommended)

The geoipupdate tool handles downloads and updates automatically. Install it, then edit /etc/GeoIP.conf:

1
apt install geoipupdate

Open /etc/GeoIP.conf and set these three values (your AccountID is the number shown on the MaxMind dashboard under Account → Account Information):

1
2
3
AccountID YOUR_ACCOUNT_ID
LicenseKey YOUR_LICENSE_KEY
EditionIDs GeoLite2-Country

Then run geoipupdate to fetch the database. It downloads to /usr/share/GeoIP/ by default, so you still need to convert it to the binary format xtables-addons expects:

1
2
3
4
5
geoipupdate

# Convert the downloaded .mmdb to xtables-addons binary format
mkdir -p /usr/share/xt_geoip
/usr/lib/xtables-addons/xt_geoip_build -D /usr/share/xt_geoip /usr/share/GeoIP/

How the Database Lookup Works

It helps to understand what xt_geoip_build actually does with those CSV files, and how the kernel uses the result at packet time.

The ZIP contains two key files:

  • GeoLite2-Country-Blocks-IPv4.csv — maps CIDR ranges to a numeric geoname ID. For example: 1.0.1.0/241814991
  • GeoLite2-Country-Locations-en.csv — maps each geoname ID to a two-letter ISO country code. For example: 1814991CN (China)

xt_geoip_build reads both files at build time, joins them on the geoname ID, and produces compact binary files (.iv4 and .iv6) in /usr/share/xt_geoip/. The geoname IDs are resolved away — the binary files are just sorted arrays of IP ranges, each tagged directly with a country code.

At packet time, the xt_geoip kernel module does a binary search over those ranges to find which one contains the source IP, reads the country code off that entry, and compares it against your –source-country list. No userspace process is involved — it all happens in-kernel on every packet.

The ZIP also includes location files for other languages (Locations-es.csv, Locations-zh-CN.csv, Locations-de.csv, etc.). These are pure localization — the same geoname IDs and ISO codes, just with country names translated. xt_geoip_build only needs the ISO codes so it doesn’t matter which language file it reads. The translated names exist for applications that display country names to users in their language.

Block Countries with iptables (nft shim)

Since xtables-addons plugs into the iptables extension system, use iptables syntax with the -m geoip module. The –source-country flag takes two-letter ISO country codes:

1
2
3
4
5
# Block inbound traffic from Russia, Turkey, China, North Korea
iptables -I INPUT -m geoip --source-country RU,TR,CN,KP -j DROP

# Same for IPv6
ip6tables -I INPUT -m geoip --source-country RU,TR,CN,KP -j DROP

Check it landed:

1
iptables -L INPUT -v --line-numbers

Make It Persistent Across Reboots

iptables rules don’t survive a reboot by default. Save them:

1
2
apt install iptables-persistent
netfilter-persistent save

Rules are saved to /etc/iptables/rules.v4 and /etc/iptables/rules.v6 and restored automatically on boot.

Keep the GeoIP Database Fresh

MaxMind updates GeoLite2 twice a week. Add a cron job to refresh and reload:

1
2
3
4
5
# /etc/cron.weekly/update-geoip
#!/bin/bash
geoipupdate
/usr/lib/xtables-addons/xt_geoip_build -D /usr/share/xt_geoip /usr/share/GeoIP/
netfilter-persistent reload
1
chmod +x /etc/cron.weekly/update-geoip

Quick Reference: ISO Country Codes

A few commonly blocked ones:

Country Code
Russia RU
Turkey TR
China CN
North Korea KP
Iran IR
Brazil BR

Full list at wikipedia.org/wiki/ISO_3166-1_alpha-2.

That’s it — once the rules are in place and persistent, your server silently drops packets from those regions before Apache or WordPress ever sees them. 🎉

How to Test and Validate the Rules

After setting up the rules, you want to confirm they actually work — not just that the commands ran without errors. Here are a few practical ways to validate. 🧪

1. Check the Rule Is Loaded

Confirm the geoip rule exists in the INPUT chain with hit counters:

1
iptables -L INPUT -v --line-numbers

Look for a line referencing geoip with your country codes. The pkts and bytes columns start at zero — they’ll increment as matching traffic hits the rule.

2. Simulate a Packet from a Blocked IP with xtables-addons

You can test whether a specific IP would be matched using iptables with the –source flag and a known IP from a blocked country. Pick a well-known public IP from that country (e.g. a Russian DNS server like 77.88.8.8 — Yandex DNS):

1
2
# Check if the rule matches a known Russian IP
iptables -C INPUT -s 77.88.8.8 -m geoip --source-country RU -j DROP

Exit code 0 means the rule matches. Exit code 1 means it doesn’t exist or doesn’t match.

3. Watch the Packet Counter Increment

Use watch to monitor the rule counters in real time while you simulate traffic:

1
watch -n1 'iptables -L INPUT -v --line-numbers'

In a second terminal, use hping3 to send a spoofed packet from a blocked IP range:

1
2
3
apt install hping3
# Send 5 SYN packets spoofed as coming from a Russian IP
hping3 -S -c 5 -a 77.88.8.8 localhost

Watch the pkts counter on the DROP rule increment in the first terminal. If it goes up, the rule is working.

4. Use a VPN to Test from a Blocked Country

The most realistic test: connect to a VPN exit node in one of your blocked countries (many free/trial VPNs have Russian or Turkish servers) and try to reach your server. You should get a connection timeout — not a refused connection, a timeout, because DROP silently discards the packet rather than sending a TCP RST back.

If you’d rather get a clear rejection instead of a silent drop, swap DROP for REJECT during testing — it sends an ICMP port-unreachable back, making it easier to confirm the block is working. Switch back to DROP for production (less information leakage).

5. Check xtables-addons GeoIP Lookup Directly

Verify the GeoIP database is loaded and resolves countries correctly:

1
2
3
4
5
6
# Check the database files exist
ls /usr/share/xt_geoip/

# Load the module manually if needed
modprobe xt_geoip
lsmod | grep geoip

If lsmod shows xt_geoip, the kernel module is loaded and the database is accessible.

Summary: Validation Checklist

Check Command Expected result
Rule exists iptables -L INPUT -v geoip DROP rule visible
Module loaded lsmod | grep geoip xt_geoip listed
DB files present ls /usr/share/xt_geoip/ .iv4/.iv6 files present
Packet counter watch iptables -L INPUT -v + hping3 pkts counter increments
Real-world test VPN to blocked country Connection timeout
This entry was posted in Linux and tagged , , , . Bookmark the permalink.

Comments are closed.