James Bush

| about | posts | projects | contact | etc

A Self-Hosted VPN Server With Wireguard

Jun. 28, 2021

Key⚷

There are many privacy-related reasons to use a VPN, and probably just as many companies willing to sell you a subscription. Some are good, others less so. After about 4 years of being mostly satisfied with a paid VPN service, I identified several reasons to build a self-hosted VPN server.

Despite paid services' privacy assurances, the only guarantee of solid privacy practices and implementation is self-hosting. Ideally, a paid VPN service should work without DNS leakage , but my paid service would leak DNS look-ups if I didn't manually configure a DNS server for each client. After about 4 years of use, many websites and applications (e.g., banking) also began to blacklist my provider's servers. This alone was a breaking point in favor of self-hosting. As an added benefit, a self-hosted VPS setup ran $5/month, which was less than half the VPN subscription (~$12/month). There is also no cap on the number of devices I can run.

For this implementation, we'll use Wireguard, a newer and relatively lightweight VPN tunneling application developed by Jason A. Donenfeld. [1] In my experience, it is extremely fast, reliable and, from what I've seen, cryptographically sound.


Step One: Deploy and Secure Cloud Sever

Among many cloud sever providers, I like Linode. A $5/month Nanode includes the Linux distro of your choice, 1 CPU, 25GB storage, and 1GB RAM, which is enough to run a decent single-home VPN setup. Linode has a detailed start-up guide on deploying a server. This walk-through is based on Ubuntu 20.04.

Pick a distribution and size. Then set a root password. Note the public IPv4 address given when the server is deployed. Open a terminal window on your local machine. SSH into your new server by entering:

ssh root@xx.xx.xx.xx

Enter the root password set at deployment and you’re in. The terminal should display:

root@localhost

Linode has a helpful guide on securing the server, so that topic is not covered here in detail. At a minimum, make sure to 1) create a limited, non-root user account with sudo privileges, 2) disable password login via SSH, 3) disable root access via SSH, 4) enable SSH pre-shared key login, 5) install/activate an additional firewall (this guide uses UFW), 6) install and activate fail2ban, and 7) close any unused ports. These are each covered in the Linode guide.

Before installing and configuring Wireguard, make sure to set rules in UFW that will keep the appropriate ports for SSH and the Wireguard service open:

sudo ufw allow 22/tcp
sudo ufw allow 51820/udp
sudo ufwenable

Install Wireguard on Server and Client

The first step is to install Wireguard which should be straightforward on a fresh installation of Ubuntu 20.04 or later.

sudo apt install wireguard

Next, the two major steps of setting up Wireguard are 1) configure the server and 2) configure the client.


Configure Wireguard Server

Root privileges will be required to access the Wireguard directory, /etc/wireguard. It’s wise to minimize the amount of work done as root to keep from accidentally breaking something, so we’ll only do a couple things. Elevate privilege to root and navigate to the Wireguard directory:

sudo -i 
cd /etc/wireguard

In the /etc/wiregaurd directory, create a configuration file with Nano (or Vim, etc.):

nano wg0.conf

The server configuration file will have two sections, one for the Wireguard interface, [Interface], and one for each client under [Peer]. (Note: server and client refer to eachother as "peer".) For the server configuration, each additional client will be listed under a new [Peer] heading.


    [Interface]
    Address = 10.0.0.1/24
    Address = fd86:ea04:1115::1/64
    SaveConfig = true
    PostUp = iptables -A FORWARD -i wg0 -j ACCEPT; iptables -t nat -A POSTROUTING -o eth0 -j MASQUERADE; ip6tables -A FORWARD -i wg0 -j ACCEPT; ip6tables -t nat -A POSTROUTING -o eth0 -j MASQUERADE
    PostDown = iptables -D FORWARD -i wg0 -j ACCEPT; iptables -t nat -D POSTROUTING -o eth0 -j MASQUERADE; ip6tables -D FORWARD -i wg0 -j ACCEPT; ip6tables -t nat -D POSTROUTING -o eth0 -j MASQUERADE
    ListenPort = 51820
    PrivateKey = <server private key>

    [Peer]
    PublicKey = <client public key >
    AllowedIPs = 10.0.0.2/32
    

Below is a breakdown of each of the fields. Under [Interface]:

Under [Peer]:

Additional peers must be specified separately. For example, after setting 10.0.0.2/32 as the first client, so additional client peers could be 10.0.0.3/32, 10.0.0.4/32, etc. There are a number of other configuration fields. See Wireguard’s documentation for more detail.

Next, save the configuration file by exiting the text editor and saving. It can be helpful to keep a backup of whatever configuration you are working with in /etc/wireguard, because when the configuration is loaded, sometimes Wireguard will change/add a few fields. You may also want to test different configurations and revert back to an earlier version. To create the backup:

cp wg0.conf wg0.conf.bak

Since the modifications we need to make the Wireguard directory are done, we can exit as root back to our normal account by entering exit in the terminal.

Next, bring up the configuration on the server:

sudo wg-quick up wg0

Note: wg-quick is a wrapper for Wireguard that contains much of the functionality needed for implementing configurations. Save the current configuration:

sudo wg-quick save wg0

To ensure the configuration starts when the server boots, add the configuration to systemd:

sudo systemctl enable wg-quick@wg0.service

Start the service now:

sudo systemctl start wg-quick@wg0.service

The server should be ready. Next, we'll configure the client.


Configure Wireguard on Client Side

I configured Wireguard on a Linux desktop. It is similar to setting up the server, but much easier. Windows and Mac have their own GUI clients. If you create a configuration file like below, load it into the Mac/Windows client and you should be good to go. But for a Debain-based Linux distribution, first install Wireguard:

sudo apt install wireguard

Elevate permissions to root:

sudo -i

Navigate to the Wireguard directory:

cd /etc/wireguard

Generate a public/private keypair in the local directory:

umask 077
wg genkey | tee privatekey | wg pubkey > publickey

Create a configuration file:

nano wg0.conf

Add the following the the configuration file:


    [Interface]
    Address = 10.0.0.3/32
    DNS = 1.1.1.1, 1.0.0.1
    ListenPort = 51820
    PrivateKey = <client private key>

    [Peer]
    PublicKey = <server public key>
    AllowedIPs = 0.0.0.0/0, ::/0
    

Under [Interface]:

Under [Peer]:

As with the server, save and exit the wg0.conf file and exit root.

Load the configuration:

sudo wg-quick up wg0

Save the configuration:

sudo wg-quick save wg0

To ensure that the configuration starts when your machine boots, add the configuration to systemd:

sudo systemctl enable wg-quick@wg0.service

Start the service now:

sudo systemctl start wg-quick@wg0.service

The client and server should now be connected. Let's test it. First, exit the SSH session with exit.


Testing & Troubleshooting

With the server and the client configurations complete and up, there are several ways to test the connection. On each machine, check the service is running:

sudo systemctl status wg-quick@wg0

The service should display something like:

● wg-quick@wg0.service - WireGuard via wg-quick(8) for wg0
     Loaded: loaded (/lib/systemd/system/wg-quick@.service; enabled; vendor present: enaled>
     Active: active (exited) since Mon 2021-06-28 13:31:56 PDT; 21h ago
       Docs: man:wg-quick(8)
             man:wg(8)
             https://www.wireguard.com/
             https://www.wireguard.com/quickstart/
             https://git.zx2c4.com/wireguard-tools/about/src/man/wg-quick.8
             https://git.zx2c4.com/wireguard-tools/about/src/man/wg.8
        Process: 1234 ExecStart=/usr/bin/wg-quick up wg0 (code=exited, status=0/SUCCESS)
      Main PID: 1234 (code=exited, status=0/SUCCESS)

Next, ping the server:

ping 10.0.0.1

This should return something like:

PING 10.0.0.1 (10.0.0.1) 56(84) bytes of data.
64 bytes from 10.0.0.1: icmp_seq=1 ttl=64 time=31.6 ms
64 bytes from 10.0.0.1: icmp_seq=2 ttl=64 time=31.7 ms

If you receive no response, there is likely a problem in the server or client configuration file. Scan each config file carefully to ensure client address matches and that your public and private keys are correct. You can take down the connection with:

sudo wg-quick down wg0

After checking each config file, restart the connection on the client and server again with:

sudo wg-quick up wg0

If you get a successful ping response, next, ping an outside server:

ping 1.1.1.1

If the ping returns a response, traffic is being properly forwarded to the internet through the server LAN. If it doesn't, check the configuration of the PostUp/PostDown fields in the server config file. Make sure the server's network interface (here we use eth0) matches server interface when you enter ip link show in a terminal window for the server.

Once receiving a successful ping response to an IPv4 address, try a domain name:

ping eff.org

If a ping to a domain name is successful, we know port forwarding is working, and DNS look-ups are working as well. At this juncture, by following the Linode guide and re-doing the process once, I still had issues. I was getting a ping response to outside servers using an IPv4 address, but not a domain name. I had a theory this was because the client did not have a DNS to lookup domains, so I used Wireguard’s DNS field and set the DNS to Cloudflare per above. So, if you can ping outside IPv4 addresses, but not domain names, try specifying an external DNS server.

Now, check the client's public IPv4 address:

curl icanhazip.com

The return address should match your server’s IPv4 address.

Next, make sure you're not getting a DNS leak on look-ups. Open a tab in Firefox and check DNS Leak Test. The IPv4 address and location should again match the server's. Run an "extended test" to check for a DNS leak. The DNS server provider display as the one set in the configuration file (we used DNS = 1.1.1.1, 1.0.0.1) for Cloudflare. In theory, the location of the DNS server should be near the geographic location of your VPS. If you receive more than one server listed or the server matches your ISP, you likely have a DNS leak. Double check the configuration files.

Speed can be tested with a number of services from desktop. Okoola’s works fine. One great thing I noticed with my setup was the difference in speed between my own VPN service versus my old VPN provider (nearly 10x faster). In fact, the new self-hosted VPN setup is comparable in terms of download/upload speed to no VPN at all. The latency is a bit higher than using no VPN, but it is still better than the old VPN service. Also, sites like Craigslist are no longer blacklisted.


Conclusion

For a couple days' work and bit of troubleshooting, I was able to build a self-hosted VPN server that was about 10 times faster than my old provider for half the price and no blacklisting.

For future improvments, I may change the configuration so that my home router connects to the VPN server directly and all attached devices do not need separate connections. If you've spotted any bugs in this implementation, please drop me a message.


Notes

[1] "WireGuard" and the "WireGuard" logo are registered trademarks of Jason A. Donenfeld.