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/24 → 1814991
- GeoLite2-Country-Locations-en.csv — maps each geoname ID to a two-letter ISO country code. For example: 1814991 → CN (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 |