Bypassing DPI: How to Tunnel WireGuard over WebSockets with WSTunnel

Networking tutorial - IT technology blog
Networking tutorial - IT technology blog

The Problem with UDP in Restricted Networks

WireGuard is incredibly fast, but it has a glaring weakness in corporate environments: it only uses UDP. Most enterprise firewalls and Deep Packet Inspection (DPI) systems are set to a “deny-all” policy for UDP traffic. They often block everything except DNS queries on port 53 to prevent unauthorized tunneling.

If you have ever seen your VPN status stuck at “Handshake did not complete” while on office Wi-Fi, you have hit this wall. In my testing at a client site with a strict Palo Alto Networks firewall, standard WireGuard failed 100% of the time. To get around this, we need to disguise our traffic as something the firewall already trusts: standard HTTPS web traffic.

By wrapping WireGuard UDP packets inside a WebSocket stream, we can trick the firewall. Since WebSockets initiate via a standard HTTP/1.1 or HTTP/2 handshake, the traffic looks like a normal browser connection to Slack, Discord, or a trading platform.

Understanding the WSTunnel Architecture

We use WSTunnel, a high-performance relay written in Rust, to handle the heavy lifting. Instead of connecting directly to your server’s UDP port, your client will talk to a local “bridge” that converts the traffic into WebSockets. The flow works like this:

  • Client Side: WireGuard Client → Local WSTunnel (UDP 51820) → WebSocket (TCP 443) → Internet.
  • Server Side: Internet → WSTunnel Server (TCP 443) → WireGuard Server (Local UDP 51820).

To a DPI engine, this looks like an encrypted TLS connection. It sees a standard WebSocket upgrade header and moves on. This effectively hides your VPN traffic inside a tunnel that the firewall is usually forced to allow.

Step 1: Installing WSTunnel

WSTunnel is a portable binary. You need it on both your remote server and your local machine. You can grab the latest release from GitHub.

# Download the binary (example for Linux x64) 
wget https://github.com/erebe/wstunnel/releases/download/v9.7.1/wstunnel-x64-linux.tar.gz
tar -xvf wstunnel-x64-linux.tar.gz
sudo mv wstunnel /usr/local/bin/

Verify the installation by running wstunnel --version. It should return the version number without errors.

Step 2: Configuring the WireGuard Server

Your server configuration is standard, but it should listen locally. Let’s assume your server uses the internal IP 10.0.0.1.

Edit /etc/wireguard/wg0.conf:

[Interface]
PrivateKey = <SERVER_PRIVATE_KEY>
Address = 10.0.0.1/24
ListenPort = 51820

[Peer]
PublicKey = <CLIENT_PUBLIC_KEY>
AllowedIPs = 10.0.0.2/32

Bring the interface up:

sudo wg-quick up wg0

Step 3: Launching the WSTunnel Server

Now, start the WSTunnel server. It will listen on TCP port 443 and forward incoming WebSocket traffic to your WireGuard port.

wstunnel server --listen http://0.0.0.0:443 --restrict-to 127.0.0.1:51820

Note: If you already run a web server like Nginx on port 443, you will need to run WSTunnel on a different port (like 8443) or use a reverse proxy to route traffic based on a specific URL path.

Step 4: Setting Up the Client Bridge

On your laptop or local machine, you need to create the other end of the tunnel. This command tells WSTunnel to listen on a local UDP port and send everything to your server over a secure WebSocket.

wstunnel client --listen udp://127.0.0.1:51820 --to wss://your-server-ip:443

Now, any data sent to 127.0.0.1:51820 on your local machine is automatically wrapped in TLS and sent to your server.

Step 5: Configuring the WireGuard Client

The final step is to point your WireGuard client at the local bridge. This is where the magic happens.

Edit your local client.conf:

[Interface]
PrivateKey = <CLIENT_PRIVATE_KEY>
Address = 10.0.0.2/24
# Important: Lower MTU to 1300 to avoid fragmentation
MTU = 1300

[Peer]
PublicKey = <SERVER_PUBLIC_KEY>
# Point to your local WSTunnel bridge
Endpoint = 127.0.0.1:51820
AllowedIPs = 0.0.0.0/0
PersistentKeepalive = 25

Why change the MTU? Standard Ethernet uses an MTU of 1500 bytes. WireGuard normally uses 1420. However, since we are adding WebSocket, TCP, and TLS headers, the packet size grows. If you leave it at 1420, your packets will be too large and get dropped or fragmented. 1300 is a safe “Goldilocks” value that ensures your connection remains stable.

Automating with Systemd

To ensure the tunnel starts automatically on your server, create a service file at /etc/systemd/system/wstunnel.service:

[Unit]
Description=WSTunnel Server
After=network.target

[Service]
ExecStart=/usr/local/bin/wstunnel server --listen http://0.0.0.0:443 --restrict-to 127.0.0.1:51820
Restart=always
User=root

[Install]
WantedBy=multi-user.target

Enable it with sudo systemctl enable --now wstunnel.

Performance and Potential Pitfalls

While this method is excellent for bypassing blocks, it introduces the “TCP-over-TCP” problem. WireGuard is designed for UDP, which handles packet loss gracefully. When you wrap it in TCP (via WebSockets), both layers will try to retransmit lost packets simultaneously. On a high-latency connection, this can cause a “TCP Meltdown” where speeds drop significantly.

To keep things running smoothly, always use wss:// (encrypted) rather than ws://. Plain WebSockets are easy for modern firewalls to identify and throttle. TLS encryption makes your traffic indistinguishable from a standard HTTPS session.

Final Thoughts

By pairing WireGuard with WSTunnel, you gain the security of a modern VPN with the reach of standard web traffic. It is an essential setup for anyone working in locked-down environments. Just remember to monitor your MTU settings and keep your PersistentKeepalive active to prevent the firewall from timing out your connection.

Share: