Tunl

Setting up a server

Tunl is a client. It connects to a WireGuard server, and if you don't already have one, this page gets you from a bare VPS to a working tunnel. The first half takes about ten minutes. The second half is about not leaving the machine open afterwards, and is worth the extra twenty.

If someone else runs your VPN and handed you a .conf file, you don't need any of this. Import the file and you're done.

What you need

A Linux machine with a public IP address. Any small VPS will do; WireGuard uses almost no CPU. The commands below are for Debian and Ubuntu. Run them as root, or put sudo in front of each one.

Install WireGuard and make a key for the server

apt update && apt install -y wireguard

umask 077
wg genkey | tee /etc/wireguard/server.key | wg pubkey > /etc/wireguard/server.pub
cat /etc/wireguard/server.pub

That last line prints the server's public key. You'll paste it into Tunl in a moment.

Write the configuration

Create /etc/wireguard/wg0.conf. Replace SERVER_PRIVATE_KEY with the contents of /etc/wireguard/server.key, and eth0 with your machine's real network interface if it differs (ip route show default will tell you).

[Interface]
Address = 10.8.0.1/24
ListenPort = 51820
PrivateKey = SERVER_PRIVATE_KEY

PostUp   = iptables -A FORWARD -i %i -j ACCEPT; iptables -t nat -A POSTROUTING -o eth0 -j MASQUERADE
PostDown = iptables -D FORWARD -i %i -j ACCEPT; iptables -t nat -D POSTROUTING -o eth0 -j MASQUERADE

The two PostUp and PostDown lines are what let your traffic reach the internet through the server rather than stopping at it.

Turn on forwarding and start it

echo 'net.ipv4.ip_forward=1' > /etc/sysctl.d/99-wireguard.conf
sysctl --system

systemctl enable --now wg-quick@wg0
wg show

wg show should print an interface with a listening port and no peers yet. Make sure UDP port 51820 is open in your provider's firewall, since that is separate from the machine's own.

Add yourself as a peer

In Tunl, choose New Tunnel. It generates a key pair, keeps the private half on your Mac, and shows you two things: your public key, and the exact block the server needs. Fill in the server's public key and its address, then open What they add on the server and copy the block. It looks like this:

[Peer]
PublicKey = YOUR_PUBLIC_KEY_FROM_TUNL
AllowedIPs = 10.8.0.2/32

Append it to /etc/wireguard/wg0.conf and reload, or add it without touching the file at all:

wg set wg0 peer YOUR_PUBLIC_KEY_FROM_TUNL allowed-ips 10.8.0.2/32
wg-quick save wg0

Give each device its own key and its own address: 10.8.0.2, then 10.8.0.3, and so on. Sharing one key between machines works right up until both are connected at once, and then neither does.

Connect

Back in Tunl, click Connect. The Live section should show a handshake within a second or two. If it shows nothing, the usual causes are a closed UDP port at the provider, a typo in one of the keys, or an AllowedIPs on the server that doesn't match the address the client is using.

If the network you're on blocks UDP

Some hotel and corporate networks drop UDP entirely, and WireGuard cannot work without it. Tunl can carry the same encrypted traffic inside a WebSocket, but that needs a relay on the server.

Put it on port 443. That is the whole point. A network that blocks UDP will usually block an odd TCP port too; what it cannot block is the port every website uses. On 443 with a real certificate, your tunnel is indistinguishable from someone reading the news.

Install and run it

curl -sSL -o /tmp/wstunnel.tar.gz \
  https://github.com/erebe/wstunnel/releases/latest/download/wstunnel_linux_amd64.tar.gz
tar -xzf /tmp/wstunnel.tar.gz -C /usr/local/bin wstunnel
chmod +x /usr/local/bin/wstunnel

Run it under systemd so it comes back after a reboot. Create /etc/systemd/system/wstunnel.service:

[Unit]
Description=wstunnel relay for WireGuard
After=network-online.target

[Service]
ExecStart=/usr/local/bin/wstunnel server wss://0.0.0.0:443 \
  --websocket-mask-frame \
  --restrict-to 127.0.0.1:51820 \
  --tls-certificate /etc/letsencrypt/live/vpn.example.net/fullchain.pem \
  --tls-private-key /etc/letsencrypt/live/vpn.example.net/privkey.pem
Restart=always
DynamicUser=yes
AmbientCapabilities=CAP_NET_BIND_SERVICE
NoNewPrivileges=yes
ProtectSystem=strict
ProtectHome=yes

[Install]
WantedBy=multi-user.target
systemctl enable --now wstunnel
systemctl status wstunnel --no-pager

The two flags that decide whether it works

--websocket-mask-frame is required. Tunl uses Apple's networking stack, which masks WebSocket frames as the standard tells clients to, and wstunnel drops masked frames without this flag. It does so silently: the symptom is a tunnel that connects and then carries nothing at all, with no error anywhere to explain it.

--restrict-to is what keeps the relay from becoming a public proxy. wstunnel does not verify the destination a client asks for, so without this anyone who finds the port can use your server to reach anything, from your own private network to somebody else's. Point it at the WireGuard port and nothing else.

Certificates

Use a real certificate. certbot certonly --standalone -d vpn.example.net is enough if nothing else is on 443, and the paths above match where it puts them. Give the service permission to read them, or copy them somewhere it can and renew with a hook.

ws:// without TLS still gets past a firewall that only blocks UDP, and WireGuard's own encryption still protects your traffic either way. But the wrapper is then plainly a WebSocket rather than ordinary HTTPS, which defeats the reason for using it on a network that inspects what you connect to.

Sharing 443 with a website

If the server already runs a site on 443, put wstunnel behind the reverse proxy instead of in front of it. Route one path to wstunnel and leave everything else alone. In Caddy:

vpn.example.net {
    handle /tunnel/* {
        reverse_proxy 127.0.0.1:8080
    }
    handle {
        root * /var/www/html
        file_server
    }
}

Then run wstunnel on ws://127.0.0.1:8080 without its own TLS, and in Tunl give the full address including wstunnel's own path: wss://vpn.example.net/tunnel/v1/events. Tunl appends /v1/events for you when you give it a bare address, but it leaves an explicit path alone, which is what makes this arrangement possible.

Securing the server

Worth saying plainly, because a VPN guide that skips this leaves you worse off than before: all of your traffic now goes through this machine. If somebody gets into it, they are inside everything you do. The tunnel does not protect you from a compromised server, it delivers you to one.

The machine itself

This is the part that actually matters, and it has nothing to do with WireGuard. Almost every VPS that gets taken over is taken over through SSH with a guessable password. Use a key and turn passwords off:

ssh-copy-id root@vpn.example.net        # from your Mac, before you lock yourself out

# then on the server, in /etc/ssh/sshd_config:
PasswordAuthentication no
PermitRootLogin prohibit-password

systemctl restart ssh

Open a second terminal and confirm you can still log in before you close the first one. Everyone learns this the hard way once.

Close everything you did not open

ufw default deny incoming
ufw default allow outgoing
ufw allow 22/tcp
ufw allow 51820/udp
ufw allow 443/tcp        # only if you are running the WebSocket relay
ufw enable

Your provider probably has its own firewall in front of the machine as well. Both have to allow the port, and people lose an hour to this regularly.

Keys and their permissions

The package makes /etc/wireguard readable only by root, and wg genkey warns you if your umask would leave a key world readable. Take the warning seriously; a private key that anyone on the machine can read is not a private key. Check with:

stat -c '%a %n' /etc/wireguard/*

Both the directory and the key should read 700 and 600. Nothing needs to be backed up except the configuration, and if you lose a key the fix is to generate a new one and replace the peer.

Why each peer gets a /32

On the server, a peer's AllowedIPs is not a routing convenience. It is an access control list: WireGuard will accept a packet from that peer only if its source address falls inside that range. Give a peer 10.8.0.2/32 and it can only ever claim to be 10.8.0.2. Give it 0.0.0.0/0, as people sometimes do by copying the client's side of the configuration, and it can claim to be anything, including your other devices.

This is also why every device needs its own key. Two machines sharing one key work fine until both connect, and then the server has one route for two claimants and the traffic goes to whichever handshook last.

Peers can reach each other

By default anything on 10.8.0.0/24 can talk to anything else on it. If the tunnel is only yours, that is what you want. If you have added family or colleagues, and you would rather they not see each other's machines, block it:

iptables -I FORWARD -i wg0 -o wg0 -j DROP

Where your DNS goes

If your client configuration has no DNS line, your Mac keeps using whatever resolver the local network handed it, and the network you were trying to get away from still sees every name you look up. Set one that is reachable through the tunnel, and switch on encrypted DNS in Tunl so the queries are not readable in transit either.

Updates

apt install -y unattended-upgrades
dpkg-reconfigure -plow unattended-upgrades

An unattended server that nobody patches is the most common way a personal VPN turns into somebody else's.

What this does not protect you from

A self-hosted VPN moves the point where your traffic joins the internet from the cafe's router to a machine you rent, under your own name, with your own payment details. That is a real improvement on hostile Wi-Fi and on an ISP that logs. It is not anonymity, and it is worth being clear with yourself about which one you wanted.

Keeping it running

WireGuard has no accounts and no session state, so there is not much to maintain. Two things are worth doing: apply security updates to the server the way you would any other machine, and remove peers you no longer use with wg set wg0 peer THEIR_KEY remove followed by wg-quick save wg0. A key you have forgotten about is a key somebody else may still have.