Configuring Raspberry Pi as a VPN gateway using NordVPN with best server selection (Pi-hole with DoH setup optional)

Configuring Raspberry Pi as a VPN gateway using NordVPN with best server selection (Pi-hole with DoH setup optional)
Photo by Petter Lagson / Unsplash
💡
This has not been tested in a while. Check NordVPN official page for connectivity options.
Update (Nov/2020): Made a few modifications to make the script work again.

This is going to be a quick tutorial (/self-reference) to set up Raspberry Pi as a VPN router for NordVPN. I ran into so many issues (!) specially with the kill-switch part, so just wanted to write this up in case someone else is looking for the same. Obviously there are better ways of getting this done documented out there, but I wanted a custom setup with a few modifications as you will see. As an add-on, I installed Pi-hole for ad & malicious domain blocking with DNS over HTTPS (DoH), with the DNS queries now tunneled over VPN.

Disclaimer: I’m doing this on a fresh RPi installation with no firewall rules setup etc. So make sure to adapt the steps if you plan to do it on an existing RPi. Also the code snippets will need to be amended to include input validations and sanitizations if you want to run it on any critical / production system. Also hardcoding of credentials in plaintext is not that great either, but pursued in the interest of a test lab setup only and may not be fully suitable for real world use. Finally I don’t endorse or recommend NordVPN by writing this post. The ‘gateway’ method is pretty much the same for every VPN (be it self-hosted or 3rd party) and the whole purpose is to serve as a guide to configure the Raspberry Pi.

Requirements

  • The RPi will be connected to the LAN network with a static IP address. Those devices wanting to use the RPi gateway should be able to edit the ‘gateway’ field under network settings and must be able to connect to the VPN seamlessly.
  • The RPi will use NordVPN as the provider with a kill-switch to disable Internet in the event of VPN failure. NordVPN provides the kill-switch feature with their apps but not for RPi. Correction - NordVPN provides this. Thanks to u/4grams on Reddit for pointing that out.
  • Ability to find the best server from NordVPN and establish the connection
  • Ad blocking using Pi-hole for the devices using the gateway
  • DNS over HTTPS (on Pi-hole)

Hardware

Configuration

Set up RPi
Connect to the RPi using a tool like PuTTY
Run raspi-config utility to resize the partition and reboot

Configure static IP address for the RPI

Open /etc/network/interfaces and add the below lines. Make sure to adapt the info for your network setup.

auto eth0
    iface eth0 inet static
    address 192.168.0.100
    netmask 255.255.255.0
    gateway 192.168.0.1
Install updates, upgrades..
sudo apt update && sudo apt upgrade -y && sudo apt dist-upgrade -y && sudo apt autoremove -y
Install necessary packages
sudo apt install openvpn iptables-persistent python-requests -y

While installing iptables-persistent, it might ask if you want to backup the iptables rules. Go ahead and select Yes. This will be overwritten anyway at a later stage.

NordVPN setup

Download and unzip the NordVPN config files to /etc/openvpn

cd /etc/openvpn
sudo su
wget https://downloads.nordcdn.com/configs/archives/servers/ovpn.zip
unzip ovpn.zip
cp ovpn_udp/*.ovpn .
rm -rf ovpn_tcp ovpn_udp # Only using the UDP connections here, so removing the TCP folder

Create a file named /etc/openvpn/login.txt and insert the NordVPN credentials.

Chmod the login.txt file to 600 so that it is only accessible by root user

sudo chmod 600 /etc/openvpn/login.txt

Edit the NordVPN’s ovpn files to use login.txt. This is done by replacing auth-user-pass with auth-user-pass login.txt Here is a small python code snippet to do that.

#!/usr/bin/env python

import fileinput
import glob
import os

os.chdir("/etc/openvpn")

file_list = glob.glob("*.ovpn")

for item in file_list:
    for line in fileinput.input(item, inplace = 1):
        print line.replace("auth-user-pass", "auth-user-pass login.txt"),
Now, which NordVPN location do you want to connect to? You need to find the corresponding URL.

Visit the NordVPN server tools page where you can find the most optimal server

Open Developer Tools in your browser and click on XHR

Now select the country that you want to use for the VPN connection and observe the XHR request that gets generated. Right click on the XHR request and copy the URL. The URL will be used in the next step.

e.g. for Australia, the URL would be

https://nordvpn.com/wp-admin/admin-ajax.php?action=servers_recommendations&filters={"country_id":13}
NordVPN connection script

Now we need to tell the RPi to connect to the NordVPN server on boot. Here is a Python script that does that. Save it on your RPi - e.g. /home/pi/vpn/connector.py.

Make sure you fill in the url field in script from previous step.

#!/usr/bin/env python

import os
import json
import subprocess
import time
import requests

os.chdir("/etc/openvpn")

# Command to kill any running instances of OpenVPN
kill_command = "sudo killall openvpn"

# URL to the NordVPN server connection tool obtained from the browser
url = "" # Insert URL here

def start_openvpn_connection():
    response = requests.get(url)

    if len(response.text) != 2:
        nvpn_response = json.loads(response.text)
        vpn_info = nvpn_response[0]
        vpn_info_hostname = vpn_info["hostname"]
        vpn_file = vpn_info_hostname + ".udp1194.ovpn"

        # Command to start Openvpn
        ov_command = "sudo openvpn --config " + vpn_file

        # Start the NordVPN connection
        subprocess.Popen(ov_command.split())

if __name__ == "__main__":
    subprocess.Popen(kill_command.split())
    time.sleep(2)
    start_openvpn_connection()

Run the script at startup. There are various ways to do this. Here we will be using rc.local.

Open /etc/rc.local and insert the script before ‘exit 0’ line.

Find NordVPN server IPs

In the next step, we will be locking down the RPi to block all outgoing traffic and allow only NordVPN IPs. The NordVPN IPs will be used to fetch the best server for the location you opted. (This is not a very scalable approach to hardcode IPs but for the sake of this tutorial, we will use this. If NordVPN changes their IPs in future, make sure you update the iptables rules.)

Use nslookup to find NordVPN IPs (IMPORTANT: Make sure that you run nslookup when you set this up so that you are putting in the valid IP addresses for NordVPN servers)

Add NordVPN IP addresses to hosts file and block outbound DNS

Open /etc/hosts file and add the below lines

104.18.229.229  nordvpn.com
104.18.230.229  nordvpn.com
Set up IP forwarding

Open /etc/sysctl.conf and uncomment the below line.

net.ipv4.ip_forward=1

Run the following command to enable the changes.

sysctl -p /etc/sysctl.conf
Iptables rules for allowing SSH, routing traffic, kill-switch, block outbound DNS whitelist NordVPN IPs..

Note: This will flush all your existing iptables rules.

Note: Make sure to replace 192.168.0.0/24 with the RPi’s LAN subnet from where you expect the devices to connect and use the gateway.

Note: Make sure you replace the IP addresses for NordVPN as well.

Save the following into a bash file and execute or run one by one.

sudo iptables -F
sudo iptables -A INPUT -p tcp --dport 22 -m conntrack --ctstate NEW,ESTABLISHED -j ACCEPT
sudo iptables -A OUTPUT -p tcp --sport 22 -m conntrack --ctstate ESTABLISHED -j ACCEPT

sudo iptables -t nat -A POSTROUTING -o tun0 -j MASQUERADE
sudo iptables -A FORWARD -i tun0 -o eth0 -m state --state RELATED,ESTABLISHED -j ACCEPT
sudo iptables -A FORWARD -i eth0 -o tun0 -j ACCEPT

sudo iptables -A OUTPUT -o tun0 -m comment --comment "vpn" -j ACCEPT
sudo iptables -A OUTPUT -o eth0 -p icmp -m comment --comment "icmp" -j ACCEPT
sudo iptables -A OUTPUT -d 192.168.0.0/24 -o eth0 -m comment --comment "lan" -j ACCEPT
sudo iptables -A OUTPUT -o eth0 -p udp -m udp --dport 1194 -m comment --comment "allow vpn traffic" -j ACCEPT
sudo iptables -A OUTPUT -o eth0 -p udp -m udp --dport 123 -m comment --comment "ntp" -j ACCEPT
sudo iptables -A OUTPUT -p tcp -d 104.18.229.229 --dport 443 -m comment --comment "nordvpn IP" -j ACCEPT
sudo iptables -A OUTPUT -p tcp -d 104.18.230.229 --dport 443 -m comment --comment "nordvpn IP" -j ACCEPT
sudo iptables -A OUTPUT -o eth0 -j DROP
sudo iptables -I FORWARD -i eth0 ! -o tun0 -j DROP

sudo iptables-save | sudo tee /etc/iptables/rules.v4

Reboot the Pi and make sure the VPN gateway is working. If you run into issues, check /var/log/syslog to see the errors.

Also kill the openvpn process and ensure that the kill-switch works.

Optional - Pi-hole with DNS over HTTPS

Install Pi-hole (make sure to select eth0 as the listening interface)

Install cloudflared and configure Pi-hole to use it. The Pi-hole configuration to use DoH needs to be done from the Pi-hole’s web UI.

Once Pi-hole is configured to use cloudflared, the DNS queries will now be tunneled over HTTPS over NordVPN.

NordVPN has its own ad blockers, I simply wanted to use Pi-hole for the ad blocking.

Speedtest results

All tests were averaged out over three runs on a 100 Mbps / 24 Mbps line. The VPN connection was made to the closest geographical server.

Connection method VPN enabled Download (Mbps) Upload (Mbps)
RPi -> router No 77.8 3.5
RPi -> router Yes 61.0 3.5
Mobile device -> RPi -> router Yes 45.1 23.5

Hope you find the tutorial useful. If you have any queries, suggestions etc. please post a comment and I will try to answer.

References:

https://dietpi.com/phpbb/viewtopic.php?t=2185

Update 1 (31/12/2019):

Change log:

  • Removed redundant iptables rules
  • Blocked outbound DNS queries on port 53, added NordVPN IPs to hosts file
Update 2 (31/12/2019):

Change log:

  • Added Speedtest results

Update 3 (01/11/2020):

Change log:

  • Fixed script to make it work again