Blocking Access from or to Specific Countries Using FreeBSD and pf

Blocking Access from or to Specific Countries Using FreeBSD and pf

FreeBSD
6 min read

In recent times, there has been an exponential increase in malicious (or simply rude) traffic from specific countries. Alternatively, sometimes we simply do not need visitors from other parts of the world accessing our server for various reasons.

On FreeBSD, this operation is very simple, and I have been using a reliable and secure system to manage it automatically for a long time. Of course, as with all geolocation blocks, there is never certainty about the result, as sometimes certain IP blocks officially belong to one country but are actually used by another, or users can resort to VPNs to bypass these types of blocks. However, it remains a valid method to filter out unwanted traffic, especially when it comes to rogue bots that do not respect the robots.txt file and bombard our machines with repeated requests, generating real DDoS attacks.

Installation of ipdbtools

First, install the ipdbtools package (http://www.freshports.org/sysutils/ipdbtools):

pkg install ipdbtools

Next, download the updated global lists:

/usr/local/bin/ipdb-update.sh

Configuring pf

Then, modify the pf configuration. To do this, add the following line at the beginning of the filtering part of your firewall configuration (/etc/pf.conf):

block drop log quick from <blocked_countries>

At this point, simply type:

service pf reload

to reload the pf configuration and start considering the values in the <blocked_countries> table.

If you do not already have a pf.conf because the only requirement was this, simply insert this line in an empty pf.conf and then run:

service pf enable
service pf start

Updating the Blocked Countries List

Next, create a file (in my case, I called it /usr/local/sbin/update_blocked_countries.sh) with content similar to the one below. Replace "CC" with the country codes you want to block. For example, to block France, Germany, and Italy, insert "FR:DE:IT".

UPDATE: pf seems to have difficulty processing very large lists, resulting in errors. For this reason, I modified the following script to insert the lists in 'chunks,' in order to prevent the procedure from failing.

#!/bin/sh

# Original file containing the IP addresses
original_file="/var/db/blocked_countries.txt"

# Generate the blocked countries file
/usr/local/bin/ipup -p -t CC > $original_file

# Number of records per batch
batch_size=10000

# Temporary file for the current batch
temp_file="/tmp/blocked_countries_temp.txt"

# Initialize the line counter
line_count=0

# Function to add a batch of records
add_batch() {
    echo "Adding records from $temp_file to pf table..."
    /sbin/pfctl -t blocked_countries -T add -f "$temp_file"
    if [ $? -ne 0 ]; then
        echo "Error adding records from $temp_file. Exiting."
        exit 1
    fi
    # Empty the temporary file
    > $temp_file
}

# Replace the table with an empty file to avoid conflicts
echo -n > /tmp/empty_blocked_countries.txt
/sbin/pfctl -t blocked_countries -T replace -f /tmp/empty_blocked_countries.txt

# Read the original file line by line
while IFS= read -r line; do
    # Add the line to the temporary file
    echo "$line" >> "$temp_file"
    line_count=$((line_count + 1))

    # If we've reached the batch size, add the records and reset the counter
    if [ $line_count -ge $batch_size ]; then
        add_batch
        line_count=0
    fi
done < "$original_file"

# Add any remaining records
if [ $line_count -gt 0 ]; then
    add_batch
fi

echo "All records added successfully."

Make it executable:

chmod a+rx /usr/local/sbin/update_blocked_countries.sh

Running the command /usr/local/sbin/update_blocked_countries.sh will show the status of the operation, for example:

314159 addresses added.

This means that the list has been generated for the inserted country codes, and pf has updated the <blocked_countries> table.

It is advisable to update the lists occasionally since IP ranges "move" from one country to another.

Automating with Cron

We can use cron to perform this task. Edit the file /etc/crontab by adding a line like:

55 9 * * *    root    /usr/local/bin/ipdb-update.sh > /dev/null 2>&1 && /usr/local/sbin/update_blocked_countries.sh

In this example, at 9:55 every day, the databases will be updated, lists generated, and fed to pf.

This setup will also work correctly on a read-only FreeBSD system on UFS, as described in a previous article. The only precaution in this case is to ensure the crontab runs at every boot since, on every machine restart, the contents of /var will be erased and recreated from scratch. Therefore, also add a line like:

@reboot    root    /usr/local/bin/ipdb-update.sh > /dev/null 2>&1 && /usr/local/sbin/update_blocked_countries.sh

Additionally, in a read-only system, you need to add a mount for tmpfs for the /usr/local/etc/ipdb/IPRanges directory in the /etc/fstab file to make it writable:

tmpfs /usr/local/etc/ipdb/IPRanges/     tmpfs rw 0 0