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.

Solution Details

  • visit ipdeny and download the “aggregated zone file” for each country to be blocked (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

Create a shellscript with commands to set up ipsets and iptables-rules:

mkdir /etc/block_by_country
cd /etc/block_by_country

# 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:
awk '{print "ipset add block4_by_country", $0}' < zonefile >> add.sh

# 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:
awk '{print "ipset add block6_by_country", $0}' < zonefile >> add.sh

# 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: