DEV Community

Gluecode
Gluecode

Posted on

Easy way to geo-fence your cloud VPS with nftables

In times when the internet is full of shitty traffic like port scanners, bots, brute forces, script kiddies and lots of scrapers - irrelevant countries for which you most likely do not want to offer your service and from which a lot of stupid traffic comes should be filtered out by default in order to minimize risks.

I often block everything on the relevant ports such as SSH (port 22) except my fixed IP address. Let's say you don't have a fixed IP available and still want to limit the possible sources for your cloud VPS. For example, you can say "I only allow connections from Germany" for my SSH port. How? I'll show you now!

I use debian / ubuntu based server. We fully rely on standard which are all available in the standard apt. As geo-fence firewall we use nftables.

Install all packages we need on your server.

apt install nftables python3 git 
Enter fullscreen mode Exit fullscreen mode

Get IP address definition and keep it up to date

First we need the definitions of IP address ranges, which are assigned to the countries. Thank god (🙏) someone has already done the work, and so we can use the following git repo pvxe/nftables-geoip to map all IP addresses to countries via the db-ip.com service.

git clone https://github.com/pvxe/nftables-geoip
cd nftables-geoip
Enter fullscreen mode Exit fullscreen mode

Download all IP definitions.

python3 nft_geoip.py --download --file-location location.csv
Enter fullscreen mode Exit fullscreen mode

Make the downloads includeable

Now we need to find out the import path of nftables. The easiest way is to type the following:

nft --help
Enter fullscreen mode Exit fullscreen mode

In the output check the line:
-I, --includepath Add to the paths searched for include files. Default is: /etc

You will find here the default include path. Copy the following downloaded files there:

cp geoip-def-all.nft /etc/
cp geoip-ipv4.nft /etc/
cp geoip-ipv6.nft /etc/
Enter fullscreen mode Exit fullscreen mode

Replace /etc/ with your include path if needed.

Later, a cronjob bash-script should be implemented here that regularly updates the definitions.

Define nftables filter file

nano /etc/nftables.conf
Enter fullscreen mode Exit fullscreen mode

Replace the default config with the following.

#!/usr/sbin/nft -f

flush ruleset

table inet filter {
        include "geoip-def-all.nft"
        include "geoip-ipv4.nft"
        include "geoip-ipv6.nft"

        # all countries
        define allcountries = { $AF ,$AX ,$AL ,$DZ ,$AS ,$AD ,$AO ,$AI ,$AQ ,$AG ,$AR ,$AM ,$AW ,$AU ,$AT ,$AZ ,$BS ,$BH ,$BD ,$BB ,$BY ,$BE ,$BZ ,$BJ ,$BM ,$BT ,$BO ,$BQ ,$BA ,$BW ,$BV ,$BR ,$IO ,$BN ,$BG ,$BF ,$BI ,$CV ,$KH ,$CM ,$CA ,$KY ,$CF ,$TD ,$CL ,$CN ,$CX ,$CC ,$CO ,$KM ,$CG ,$CD ,$CK ,$CR ,$CI ,$HR ,$CU ,$CW ,$CY ,$CZ ,$DK ,$DJ ,$DM ,$DO ,$EC ,$EG ,$SV ,$GQ ,$ER ,$EE ,$SZ ,$ET ,$FK ,$FO ,$FJ ,$FI ,$FR ,$GF ,$PF ,$TF ,$GA ,$GM ,$GE ,$DE ,$GH ,$GI ,$GR ,$GL ,$GD ,$GP ,$GU ,$GT ,$GG ,$GN ,$GW ,$GY ,$HT ,$HM ,$VA ,$HN ,$HK ,$HU ,$IS ,$IN ,$ID ,$IR ,$IQ ,$IE ,$IM ,$IL ,$IT ,$JM ,$JP ,$JE ,$JO ,$KZ ,$KE ,$KI ,$KP ,$KR ,$KW ,$KG ,$LA ,$LV ,$LB ,$LS ,$LR ,$LY ,$LI ,$LT ,$LU ,$MO ,$MG ,$MW ,$MY ,$MV ,$ML ,$MT ,$MH ,$MQ ,$MR ,$MU ,$YT ,$MX ,$FM ,$MD ,$MC ,$MN ,$ME ,$MS ,$MA ,$MZ ,$MM ,$NA ,$NR ,$NP ,$NL ,$NC ,$NZ ,$NI ,$NE ,$NG ,$NU ,$NF ,$MK ,$MP ,$NO ,$OM ,$PK ,$PW ,$PS ,$PA ,$PG ,$PY ,$PE ,$PH ,$PN ,$PL ,$PT ,$PR ,$QA ,$RE ,$RO ,$RU ,$RW ,$BL ,$SH ,$KN ,$LC ,$MF ,$PM ,$VC ,$WS ,$SM ,$ST ,$SA ,$SN ,$RS ,$SC ,$SL ,$SG ,$SX ,$SK ,$SI ,$SB ,$SO ,$ZA ,$GS ,$SS ,$ES ,$LK ,$SD ,$SR ,$SJ ,$SE ,$CH ,$SY ,$TW ,$TJ ,$TZ ,$TH ,$TL ,$TG ,$TK ,$TO ,$TT ,$TN ,$TR ,$TM ,$TC ,$TV ,$UG ,$UA ,$AE ,$GB ,$US ,$UM ,$UY ,$UZ ,$VU ,$VE ,$VN ,$VG ,$VI ,$WF ,$EH ,$YE ,$ZM ,$ZW }

        # all countries without $DE $AT $CH
        define allcountries_without_dach = { $AF ,$AX ,$AL ,$DZ ,$AS ,$AD ,$AO ,$AI ,$AQ ,$AG ,$AR ,$AM ,$AW ,$AU ,$AZ ,$BS ,$BH ,$BD ,$BB ,$BY ,$BE ,$BZ ,$BJ ,$BM ,$BT ,$BO ,$BQ ,$BA ,$BW ,$BV ,$BR ,$IO ,$BN ,$BG ,$BF ,$BI ,$CV ,$KH ,$CM ,$CA ,$KY ,$CF ,$TD ,$CL ,$CN ,$CX ,$CC ,$CO ,$KM ,$CG ,$CD ,$CK ,$CR ,$CI ,$HR ,$CU ,$CW ,$CY ,$CZ ,$DK ,$DJ ,$DM ,$DO ,$EC ,$EG ,$SV ,$GQ ,$ER ,$EE ,$SZ ,$ET ,$FK ,$FO ,$FJ ,$FI ,$FR ,$GF ,$PF ,$TF ,$GA ,$GM ,$GE ,$GH ,$GI ,$GR ,$GL ,$GD ,$GP ,$GU ,$GT ,$GG ,$GN ,$GW ,$GY ,$HT ,$HM ,$VA ,$HN ,$HK ,$HU ,$IS ,$IN ,$ID ,$IR ,$IQ ,$IE ,$IM ,$IL ,$IT ,$JM ,$JP ,$JE ,$JO ,$KZ ,$KE ,$KI ,$KP ,$KR ,$KW ,$KG ,$LA ,$LV ,$LB ,$LS ,$LR ,$LY ,$LI ,$LT ,$LU ,$MO ,$MG ,$MW ,$MY ,$MV ,$ML ,$MT ,$MH ,$MQ ,$MR ,$MU ,$YT ,$MX ,$FM ,$MD ,$MC ,$MN ,$ME ,$MS ,$MA ,$MZ ,$MM ,$NA ,$NR ,$NP ,$NL ,$NC ,$NZ ,$NI ,$NE ,$NG ,$NU ,$NF ,$MK ,$MP ,$NO ,$OM ,$PK ,$PW ,$PS ,$PA ,$PG ,$PY ,$PE ,$PH ,$PN ,$PL ,$PT ,$PR ,$QA ,$RE ,$RO ,$RU ,$RW ,$BL ,$SH ,$KN ,$LC ,$MF ,$PM ,$VC ,$WS ,$SM ,$ST ,$SA ,$SN ,$RS ,$SC ,$SL ,$SG ,$SX ,$SK ,$SI ,$SB ,$SO ,$ZA ,$GS ,$SS ,$ES ,$LK ,$SD ,$SR ,$SJ ,$SE ,$SY ,$TW ,$TJ ,$TZ ,$TH ,$TL ,$TG ,$TK ,$TO ,$TT ,$TN ,$TR ,$TM ,$TC ,$TV ,$UG ,$UA ,$AE ,$GB ,$US ,$UM ,$UY ,$UZ ,$VU ,$VE ,$VN ,$VG ,$VI ,$WF ,$EH ,$YE ,$ZM ,$ZW }

        chain geoip-mark-input {
                type filter hook input priority -1; policy accept;

                # Mark incomming packages with the country 
                meta mark set ip saddr map @geoip4
                meta mark set ip6 saddr map @geoip6
        }

        chain input {
                type filter hook input priority 0; policy accept;

                # example: traffic outside $DE, $AT, $CH will be blocked on port 22
                tcp dport 22 meta mark $allcountries_without_dach drop

                # example: traffic outside $DE, $AT, $CH will be completly blocked 
                meta mark $allcountries_without_dach drop

                # example: traffic from $UK will be blocked for port 443
                tcp dport 443 meta mark $UK drop
        }
}
Enter fullscreen mode Exit fullscreen mode

You have all the country codes in the ISO 3166-1 standard available. i.e. $US = USA, $DE = Germany, $RU = RUSSIA etc. ... Incoming traffic will be tagged with the country code.

And here you go.

systemctl restart nftables
Enter fullscreen mode Exit fullscreen mode

Now you can test your geo-fence with an VPN connection or similar.

More advanced

To make it more readable and easier to maintain, you can of course also block everything by default by replacing the following line in the input chain

type filter hook input priority 0; policy accept;
Enter fullscreen mode Exit fullscreen mode

with

type filter hook input priority 0; policy drop;
Enter fullscreen mode Exit fullscreen mode

Then use the following filter rule.

tcp dport 22 meta mark $DE accept
Enter fullscreen mode Exit fullscreen mode

This will allow only German traffic on port 22, for example.
Please take care, so that you do not exclude yourself. You also have to release your internal ipnet from the local LAN or similar depending on the environment you have.

Thanks for reading and stay safe.

Top comments (1)

Collapse
 
king_triton profile image
King Triton

Wow, this is an incredibly detailed guide! Thank you for breaking down the steps for setting up geo-fencing with nftables. As someone who has struggled with unwanted traffic on my servers, this solution looks both robust and straightforward. The script for regularly updating the IP definitions is a great touch, ensuring the firewall remains effective over time. I'll definitely be giving this a try on my own setup. Cheers!