Jun. 28, 2021
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.
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
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.
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]
:
true
will save the configuration
automatically when taken up/down.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.
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]
:
[Peer]
configuration—this will be the client’s address on the
server LAN./etc/wireguard
directory. You can
copy them from the command line with cat publickey
and
cat privatekey
while in the Wireguard directory.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
.
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.
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.
[1] "WireGuard" and the "WireGuard" logo are registered trademarks of Jason A. Donenfeld.