Blocking Network Connections By Country

Categories: Linux

The Problem

The logs on my Linux (Ubuntu 16.04 LTS) server show continuous attempts to log in via SSH; they are unlikely to get in that way but it is still annoying and a little worrying if I make any slip-up in configuration. In addition, I’m getting a fair bit of spam to my personal account. Using fail2ban helps, but not as much as I had hoped.

Running “whois” on the IP addresses reported in the logs almost always shows the source is one of two countries, so I have decided (at least as a temporary experiment) to just block all network connections from those countries. That is perhaps a little unfair on honest open-source developers and Linux users who are now denied access to this wonderful blog, but I suspect it won’t bring either country down. A minor variant of the rules below could make an exception for port 80, but I don’t care enough at the moment.

Unfortunately, I couldn’t find a good tutorial on how to set this up. There are fragments of answers on the iptables website, and on various stackoverflow discussions, but nothing complete. Here’s how I ended up doing it.

Solution Overview

There are some websites that publish collections of IP-address-ranges for various purposes, including per-country mappings.

The iptables network filtering built into Linux can block by address, but there are two problems: the list of addresses is extremely long, and iptables doesn’t automatically get restored after server reboot. Fortunately, the ipset extension enables iptables to efficiently determine if an address is within a set. And systemd can be used to restore the configuration on reboot.

In summary, the complete solution is to:

  • download the current country/address-range mappings;
  • generate a shell-script which builds an IPSET that holds the undesired address-ranges and registers the IPSET with iptables;
  • write a systemd one-shot-service unit file that executes that script when desired (eg on server boot)

The country/address mappings are not automatically updated, but that doesn’t bother me. This is a “good enough” solution, it doesn’t need to be perfect or 100% up-to-date at all times.

Warning: installing the routes can take 30 seconds or so, depending on how many countries you choose to ban. As this needs to be run on boot, it does delay reboots somewhat.

Solution Details

  • visit ipdeny and download the “aggregated zone file” for all countries (both IP4 and IP6 versions).
  • install ipset if not already done: apt install ipset
  • create a directory to hold the work and convert zonefiles to iptables-script (see below)
  • build the systemd unit file (see below)
  • start service

All the following steps must be executed as root.

The iptables/ip6tables infrastructure is used rather than nftables/nft. While nftables may be the future of network-filtering in Linux, it still appears to be a work-in-progress.

Convert Zonefiles to iptables-script

Perform the following steps.

mkdir -p /etc/block_by_country/ipv4
mkdir -p /etc/block_by_country/ipv6
cd /etc/block_by_country

Now unpack file all-zones.tar.gz (from ipdeny) in directory ipv4, and file ipv6-all-zones.tar.gz in directory ipv6.

Now create a script with the following commands:

# countries to block - adjust according to your requirements
COUNTRIES="aa bb cc"; 

# Create set of ip4 addresses
echo "ipset create block4_by_country hash:net family inet" > add.sh

# For each ipv4 zonefile, create commands to add the content to the set:
for country in $COUNTRIES; do
  awk '{print "ipset add block4_by_country", $0}' < ipv4/$country.zone >> add.sh
done

# Create set of ip6 addresses
echo "ipset create block6_by_country hash:net family inet6" >> add.sh

# For each ipv6 zonefile, create commands to add the content to the set:
for country in $COUNTRIES; do
  awk '{print "ipset add block6_by_country", $0}' < ipv6/$country.zone >> add.sh
done

# Create command to add the ipsets to the global filtering rules:
cat >> add.sh << EOF
iptables -N BLOCK_BY_COUNTRY
iptables -A BLOCK_BY_COUNTRY -m state --state ESTABLISHED,RELATED -j RETURN
iptables -A BLOCK_BY_COUNTRY -m set --match-set block4_by_country src -j DROP
iptables -I INPUT -p tcp -j BLOCK_BY_COUNTRY

ip6tables -N BLOCK_BY_COUNTRY
ip6tables -A BLOCK_BY_COUNTRY -m state --state ESTABLISHED,RELATED -j RETURN
ip6tables -A BLOCK_BY_COUNTRY -m set --match-set block6_by_country src -j DROP
ip6tables -I INPUT -p tcp -j BLOCK_BY_COUNTRY
EOF

Note that the DROP commands can be fine-tuned to block (or allow) access to specific ports if desired.

Also create a shellscript that removes the rules again; not absolutely necessary but useful:

# Create script to disable rules
cat > remove.sh << EOF
iptables -D INPUT -p tcp -j BLOCK_BY_COUNTRY
iptables -F BLOCK_BY_COUNTRY
iptables -X BLOCK_BY_COUNTRY
ipset destroy block4_by_country

ip6tables -D INPUT -p tcp -j BLOCK_BY_COUNTRY
ip6tables -F BLOCK_BY_COUNTRY
ip6tables -X BLOCK_BY_COUNTRY
ipset destroy block6_by_country
EOF

Define the systemd Unit File

echo > /etc/systemd/system/block_by_country.service << EOF
[Unit]
Description=Block network connections by country

[Service]
Type=oneshot
RemainAfterExit=yes
ExecStart=/bin/sh /etc/block_by_country/add.sh
ExecStop=/bin/sh /etc/block_by_country/remove.sh

[Install]
WantedBy=network.target
EOF

Start the service

The following command will execute the script, but won’t enable it on reboot (good for testing without permanently locking yourself out :-)

systemctl start block_by_country

The following command will ensure the service is started on each reboot:

systemctl enable block_by_country

Testing the service

The following commands might be useful to verify that the rule is active:

  • iptables -t filter --list INPUT
  • iptables -t filter --list BLOCK_BY_COUNTRY
  • ipset list

Results

Before blocking by country, fail2ban was blocking approximately 400 separate IP addresses per day which were attempting to login via SSH (grep Failed /var/log/auth.log | grep "Jan 01" | wc -l). After blocking just two large countries (from which I do not need any SSH or SMTP traffic), fail2ban triggered only around 50 times per day. So problem not completely solved, but reduced to around 10% of what it was - which is well worth having. Presumably my email spam has been reduced correspondingly.

References

The following links gave me some hints towards the above solution:

Other sources of address-lists: