Tunnel WireGuard via Websockets
Setting up WireGuard vpn to work in restricted networks that block UDP traffic.
Basic Idea
- Run wstunnel to tunnel UDP traffic to vpn server
- Configure local
wg-quick
to uselocalhost
as endpoint
Sounds easy, and it’s not hard, but there are some gotchas to be aware off:
- Have to do your own routing setup
- Possible issues with DNS when
wstunnel
needs to re-connect
Prerequisite
I’m going to assume that you have already got WireGuard working over UDP with
Linux server in the cloud and MacOS client. If not, there are plenty of guides
on-line. For me the main stumbling block was not realising early enough that
even though WireGuard itself doesn’t have a concept of server/client, the way
you configure the server and the way you configure clients is actually
somewhat different. On a client side you have a single peer with the public
key of your server and with Endpoint
pointing to the domain of your server,
you also probably want to configure DNS.
Something like this example:
[Interface]
PrivateKey = oCkFT5ZmTJZapiCm2zZ/vNRhdVRFhKFhnkVFKRJW+2U=
Address = 10.10.0.2/24
DNS = 1.1.1.1
DNS = 8.8.8.8
[Peer]
PublicKey = xWdX6PjqZPG+So5ndzgjBa3OxSEgPA5Exi+GMLknHWA=
AllowedIPs = 0.0.0.0/0, ::0
Endpoint = yourhost.tld:51820
The important part above is AllowedIPs = 0.0.0.0/0, ::0
, which tells
wg-quick
to route all the traffic (v4 and v6) through the tunnel when setting
up the connection. Also you should avoid using SaveConfig
option on the client
side as it will overwrite domain name of the server with IP address, which is
probably not what you want.
On the server side things might look something like this:
[Interface]
PrivateKey = AL7WeXT59GebMA5RLnI97fMarjKS1dnSFIDCLhxTymE=
Address = 10.10.0.1/24
PostUp = iptables -A FORWARD -i %i -j ACCEPT; iptables -t nat -A POSTROUTING -o ens3 -j MASQUERADE
PostDown = iptables -D FORWARD -i %i -j ACCEPT; iptables -t nat -D POSTROUTING -o ens3 -j MASQUERADE
ListenPort = 51820
[Peer]
PublicKey = kAodaLwCyX6t4Olxh0r6/ohxoIvYTQ24QIT/sijAAB0=
AllowedIPs = 10.10.0.2/32
[Peer]
...
Note differences in the [Interface]
section, it includes PostUp/PostDown
rules to setup/tear down packet forwarding from the wireguard interface (%i
)
to your main network interface (ens3
in this case). There is also ListenPort
directive and no DNS
.
In the [Peer]
section, AllowedIPs
is set to the value of Interface.Address
in the client config file, also Endpoint
is omitted. Each peer has to have
unique address, and different from that of a server.
UDP Tunnel
Head over to wstunnel releases and download linux version for your server and MacOS version for the client.
On a server we run
wstunnel -v -s wss://0.0.0.0/ --restrictTo 127.0.0.1:51820
This will listen for a TLS connection on port 443 and will only forward packets destined to a localhost and wireguard port.
Client will run this:
wstunnel -v --udp --udpTimeoutSec -1 -L 127.0.0.1:51820:127.0.0.1:51820 wss://yourhost.tld/
This will listen on port 51820
on localhost only and forward these packets to
port 51280
on yourhost.tld
.
Nginx as Proxy
If you would like to run webserver on the same machine that runs wstunnel
then
you don’t want port 443
to be used solely for UDP tunnelling. With nginx
,
websockets tunnelling is possible with a configuration similar to below:
Sample Nginx Config (click to expand)
server {
server_name yourhost.tld;
listen 443 ssl;
# ssl config
ssl on;
ssl_certificate /etc/letsencrypt/live/yourhost.tld/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/yourhost.tld/privkey.pem;
# more SSL option here
# ...
location / {
# .. whatever your usual site has
}
# Websocket reverse proxy
#
# Using random string here helps with reducing abuse potential
# it's a kind of pre-shared secret between client and nginx proxy
# On a client add `--upgradePathPrefix E7m5vGDqryd55MMP`
location /E7m5vGDqryd55MMP/ {
proxy_pass http://127.0.0.1:33344;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $http_host;
proxy_set_header X-Real-IP $remote_addr;
proxy_connect_timeout 10m;
proxy_send_timeout 10m;
proxy_read_timeout 90m;
send_timeout 10m;
}
}
You will then run wstunnel
server on port 33344
, binding to localhost
and
without TLS:
wstunnel --server ws://127.0.0.1:33344 --restrictTo=127.0.0.1:51820
You probably want to make this auto-start on your server:
wstunnel: Systemd Service File
Copy to /etc/systemd/system/wstunnel.service
[Unit]
Description=Tunnel WG UDP over websocket
After=network.target
[Service]
Type=simple
User=nobody
ExecStart=/usr/local/bin/wstunnel -q --server ws://127.0.0.1:33344 --restrictTo=127.0.0.1:51820
Restart=no
[Install]
WantedBy=multi-user.target
then:
systemctl enable wstunnel
systemctl start wstunnel
Extra advantage you gain by using nginx
as reverse proxy is a kind of
“authentication”. Only requests made to
https://yourhost.tld/{longish-random-string}/...
will be forwarded to
wstunnel
server, and since we are using TLS {longish-random-string}
shouldn’t be visible to any middleman. You also get access logs and proper TLS
(wstunnel
only has one hard-coded certificate).
Configure wg-quick to use UDP Tunnel
Copy wg0.conf
into wg1.conf
and make this change
[Peer]
PublicKey = xWdX6PjqZPG+So5ndzgjBa3OxSEgPA5Exi+GMLknHWA=
AllowedIPs = 0.0.0.0/0, ::0
- Endpoint = yourhost.tld:51820
+ Endpoint = 127.0.0.1:51820
If at this point you try wg-quick up wg1
, things won’t work for the following reasons:
wg-quick
will route all traffic through tunnel including traffic toyourhost.tld
- We can solve this by adding custom route to
yourhost.tld
inPreUp
- We can solve this by adding custom route to
wg-quick
will route all traffic to127.0.0.1
through your default gateway, and so won’t be able to talk towstunnel
in the first place- No easy solution to this one, have to disable routing within
wg-quick
altogether withTable = off
option, then setup routing manually
- No easy solution to this one, have to disable routing within
wstunnel
will fail to connect due to DNS failure, since DNS traffic will be routed through a tunnel that hasn’t been established yet. This might not happen right away due to DNS caching, but will become a problem when trying to re-connect- Option 1: use IP address of the server on a client side, downside no
vhost
routing if usingnginx
, noTLS
verification. - Option 2: write current server IP to
/etc/hosts
- Option 3: run
dnsmasq
on client side, configurednsmasq
rather than/etc/hosts
- Option 1: use IP address of the server on a client side, downside no
I have written
this script
that helps with setting up correct routes, launching wstunnel
and updating /etc/hosts
. To use it copy it to /etc/wireguard/
directory and add the following to your [Interface]
section of wg1.conf
[Interface]
PrivateKey = oCkFT5ZmTJZapiCm2zZ/vNRhdVRFhKFhnkVFKRJW+2U=
+ Table = off
+ PreUp = source /etc/wireguard/wstunnel.sh && pre_up %I
+ PostUp = source /etc/wireguard/wstunnel.sh && post_up %i %I
+ PostDown = source /etc/wireguard/wstunnel.sh && post_down %i %I
Setup script reads configuration from /etc/wireguard/{wg_interface}.wstunnel
REMOTE_HOST=yourhost.tld
REMOTE_PORT=51820
UPDATE_HOSTS='/etc/hosts'
# if using nginx with custom prefix for added security, configure it here
WS_PREFIX='E7m5vGDqryd55MMP'
# Can change local port of the wstunnel, don't forget to change Peer.Endpoint
#LOCAL_PORT=${REMOTE_PORT}
# If using dnsmasq can supply other file than /etc/hosts
# UPDATE_HOSTS='/usr/local/etc/dnsmasq.d/hosts/tunnels'
# Will send -HUP to dnsmasq to reload hosts
# USING_DNSMASQ=1
Save above to /etc/wireguard/wg1.wstunnel
and customise. Then all you have to
do is sudo wg-quick up wg1
. Behind the scenes wstunnel.sh
will:
- Obtain current IP of
yourhost.tld
- Update
/etc/hosts
with current IP ofyourhost.tld
- Add custom route to
yourhost.tld
via default gateway - Launch client side of
wstunnel
(asnobody
) - Route all traffic through wireguard tunnel
- Clean up when you are done
sudo wg-quick down wg1
- stop tunnel app
- cleanup
/etc/hosts
Limitations
Currently Wi-Fi disconnects are likely to cause non-recoverable errors and will require bringing wireguard interface down and then back up manually. This is because route to the server is set up once and is not updated as you connect to a new router with possibly different gateway.
I have only needed this on MacOS, Linux client will be very similar, but will likely need some changes here and there.
Notes on Security
It’s important to use --restrictTo=127.0.0.1:51820
option of wstunnel
on the
server as wstunnel
is not authenticated and you don’t want to let others use
your machine as a proxy. With that option “bad guys” would only be able to send
UDP packets to a port that is already open anyway and will appear silent unless
they have your private key.
Don’t run wstunnel
as root
, systemd file above launches wstunnel
as nobody
.
Make sure that wstunnel
on a client side listens on localhost
only, and
doesn’t run as root
.
Make sure files under /etc/wireguard/
are accessible by root
only,
wg-quick
runs as root
and so is wstunnel.sh
, and it sources wg1.wstunnel
as root
also, so make sure they are not writable by anyone except root
.
Notes on Debugging
netstat -nr -f inet
display routing table for ipv4- Use
nc
(netcat) for testing UDP tunnel- server
nc -u -l 51820
(stop wireguard first) - client
nc -u 127.0.0.1 51820
- server
- Check your public ip:
curl http://api.ipify.org/
- If using
nginx
view logs:sudo tail -f /var/log/nginx/access.log
- Change
-q
to-v
on the server, thensudo journalctl -fu wstunnel
wg-quick
is just a bash script