WireGuard Over TCP

While you can tunnel any IP-based protocol (TCP, UDP, ICMP, SCTP, IPIP, GRE, etc) inside of WireGuard, WireGuard itself uses UDP for its own transport. If you need to use WireGuard in a restrictive network environment that blocks UDP to or from external sources — but does allow TCP — you can use udp2raw to set up a TCP tunnel through which you can use WireGuard.

This article will show you how to set up a WireGuard Point to Point connection through a udp2raw tunnel, where we have one endpoint, Endpoint A, behind a UDP-restricted NAT trying to connect through WireGuard to a private HTTP service hosted on the other endpoint, Endpoint B. Endpoint B is also behind NAT, but has TCP port 443 forwarded to it from a publicly-accessible IP address of 203.0.113.2.

The diagram below illustrates this scenario:

Wireguard point-to-point over TCP

The private HTTP service is running on TCP port 8080 of Endpoint B. We’ll set up WireGuard and udp2raw such that the Endpoint A will be able to access this service using Endpoint B’s WireGuard IP address, 10.0.0.2 (ie with a URL of http://10.0.0.2:8080/).

The same udp2raw settings will also work for most other WireGuard topologies, like Point to Site or Hub and Spoke. If, however, you need to use WireGuard for the default route on a peer (eg using AllowedIPs = 0.0.0.0/0 to send all traffic from the peer to the Internet through the WireGuard tunnel), you will need to add an explicit route for the other WireGuard endpoint; we’ll cover this in the Point to Internet section.

Point to Point

First, install udp2raw on both endpoints. If your OS package manager does not include a udp2raw package, you can download the source code and build it yourself. Alternately, you can download a pre-built Linux binary from the upd2raw releases page — the udp2raw_binaries.tar.gz link from this page this page contains binaries for a variety of different Linux architectures, like arm, amd64, etc. Or you can download a Windows or Mac binary from the udp2raw-multiplatform releases page — on that page, the udp2raw_mp_binaries.tar.gz link includes binaries for both Windows and Mac.

In this guide, we’ll refer to the udp2raw binary as just udp2raw, as if you had placed the platform-specific binary at /usr/bin/udp2raw on each endpoint; but you may need to adjust that to refer to the actual binary name or full path to the binary that you are using.

Next, follow the basic WireGuard Point to Point Configuration guide to install WireGuard on both endpoints, generate a WireGuard key pair for each, and set up a basic WireGuard configuration file at /etc/wireguard/wg0.conf on each endpoint.

Endpoint A

Once you’ve set up the basic WireGuard configuration for Endpoint A, you can adjust it to use udp2raw. First, add the following settings to the [Interface] section of the /etc/wireguard/wg0.conf file on Endpoint A:

MTU = 1342
PreUp = udp2raw -c -l 127.0.0.1:50001 -r 203.0.113.2:443 -k "shared secret" -a >/var/log/udp2raw.log 2>&1 &
PostDown = killall udp2raw || true

Replace 203.0.113.2:443 with the public IP address and TCP port that Endpoint A will use to connect to Endpoint B, and replace shared secret with some actual secret value.

Udp2raw won’t work to tunnel packets over the Internet with a data payload of 1375 bytes or larger, so we need to lower the MTU (Maximum Transmission Unit) on the WireGuard interface. An MTU of 1342 is as high as we can use for the WireGuard interface in this case, so that’s what we’ll use here (but you’ll need to lower this even further if any link between your two endpoints has a lower MTU than the standard 1500 value).

We’ll run udp2raw via a PreUp command. Udp2raw by default expects to be attached to a terminal, so we’ll add >/var/log/udp2raw.log 2>&1 & to the end of the PreUp command to send its output to a log file at /var/log/udp2raw.log, and run it in the background (so it won’t block wg-quick’s startup).

This is what each udp2raw flag from above does:

-c

Runs udp2raw in “client” mode, where it initiates TCP connections to the udp2raw server specified by the -r flag (one endpoint must be running in “client” mode, and the other in “server” mode).

-l 127.0.0.1:50001

Listens for UDP packets from the local WireGuard interface on UDP port 50001. You can change this port to whatever you want — it just needs to match the local WireGuard’s Endpoint setting (as we’ll discuss below).

-r 203.0.113.2:443

Connects to the remote udp2raw server that’s listening on the public IP address 203.0.113.2 on TCP port 443. Change this to use Endpoint B’s actual public IP address and TCP port.

In our scenario, the router for Site B is forwarding TCP port 443 connections from the Internet for IP address 203.0.113.2 to Endpoint B’s Site B LAN address of 192.168.200.22. In other situations, Endpoint B might have a public static IP address directly routable from the Internet, in which case you can use that IP address here instead. In general, the IP address you specify here should be a publicly-routable IP address — not a private IP address.

-k "shared secret"

Sets the secret used to authenticate the client to the udp2raw server. Change this to an actual secret value (a good way to come up with one is to run the wg genpsk command). This value must be the same on the udp2raw client and server.

-a

Sets up iptables rules to shield the Linux network stack from trying to process the fake TCP packets generated by udp2raw (skip this on non-Linux platforms).

The PostDown command will shut down all running instances of udp2raw (particularly including the one we started with the PreUp command).

Next, change the Endpoint setting in the [Peer] section of Endpoint A’s /etc/wireguard/wg0.conf file to this:

Endpoint = 127.0.0.1:50001

This will send the WireGuard tunnel through the udp2raw client we set up with the above PreUp command. Use the same value for the Endpoint setting as you use for the -l flag of the udp2raw client.

The complete WireGuard configuration file for Endpoint A should now look like this:

# /etc/wireguard/wg0.conf

# local settings for Endpoint A
[Interface]
PrivateKey = AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEE=
Address = 10.0.0.1/32

# send wg through udp2raw
MTU = 1342
PreUp = udp2raw -c -l 127.0.0.1:50001 -r 203.0.113.2:443 -k "shared secret" -a >/var/log/udp2raw.log 2>&1 &
PostDown = killall udp2raw || true

# remote settings for Endpoint B
[Peer]
PublicKey = fE/wdxzl0klVp/IR8UcaoGUMjqaWi3jAd7KzHKFS6Ds=
Endpoint = 127.0.0.1:50001
AllowedIPs = 10.0.0.2/32

Endpoint B

On Endpoint B, add the following settings to the [Interface] section of its /etc/wireguard/wg0.conf file.

# receive wg through udp2raw
MTU = 1342
PreUp = udp2raw -s -l to the udp2raw server -r 127.0.0.1:51822 -k "shared secret" -a >/var/log/udp2raw.log 2>&1 &
PostDown = killall udp2raw || true

Replace 192.168.200.22:443 with Endpoint B’s actual IP address and TCP port that will accept TCP connections from Endpoint A, and replace shared secret with the same secret value you used for Endpoint A above:

Like Endpoint A, we need to lower the MTU for Endpoint B’s WireGuard interface; 1342 should work in most cases (but use the same MTU value as you used for Endpoint A if you need to lower it).

Also like Endpoint A, we’ll run udp2raw via a PreUp command, adding >/var/log/udp2raw.log 2>&1 & to the end of the PreUp command to send its output to a log file at /var/log/udp2raw.log, and run it in the background (so it won’t block wg-quick startup).

The first three flags we set for udp2raw need to be different on Endpoint B than on Endpoint A, however. This is what each udp2raw flag on Endpoint B does:

-s

Runs udp2raw in “server” mode, where it listens for TCP connections on the local IP address and port specified by the -l flag (one endpoint must be running in “client” mode, and the other in “server” mode).

-l 192.168.200.22:443

Listens for TCP packets from udp2raw clients at the IP address of 192.168.200.22 on TCP port 443.

In our scenario, the router for Site B is forwarding TCP port 443 connections from the Internet for IP address 203.0.113.2 to Endpoint B’s Site B LAN address of 192.168.200.22. In other situations, Endpoint B might have a public static IP address directly routable from the Internet, in which you can use that IP address here instead.

In general, the IP address you specify here should be the primary IP address of Endpoint B’s public-facing Ethernet interface (which may be a private IP address). Run ip -brief address on Endpoint B to see the list of its addresses.

-r 127.0.0.1:51822

Sends connections tunneled through udp2raw to the local WireGuard interface listening at UDP port 51822. You can change this port to whatever you want — it just has to match the local WireGuard interface’s ListentPort setting.

-k "shared secret"

Sets the secret used to authenticate clients to the udp2raw server. Use the same secret on Endpoint B as you used for Endpoint A above.

-a

Sets up iptables rules to shield the Linux network stack from trying to process the fake TCP packets generated by udp2raw (skip this on non-Linux platforms).

The PostDown command will shut down all running instances of udp2raw (including the one we started with the PreUp command).

The complete WireGuard configuration file for Endpoint B should now look like this:

# /etc/wireguard/wg0.conf

# local settings for Endpoint B
[Interface]
PrivateKey = ABBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBFA=
Address = 10.0.0.2/32
ListenPort = 51822

# receive wg through udp2raw
MTU = 1342
PreUp = udp2raw -s -l 192.168.200.22:443 -r 127.0.0.1:51822 -k "shared secret" -a >/var/log/udp2raw.log 2>&1 &
PostDown = killall udp2raw || true

# remote settings for Endpoint A
[Peer]
PublicKey = /TOE4TKtAqVsePRVR+5AA43HkAK5DSntkOCO7nYq5xU=
AllowedIPs = 10.0.0.1/32

Test It Out

Start up the WireGuard interface on each endpoint (eg sudo wg-quick up wg0), start up the private HTTP service on Endpoint B, and then try to connect to it from Endpoint A. In our scenario, Endpoint B’s private WireGuard IP address is 10.0.0.2, and the private HTTP service is running on it at TCP port 8080, so we can run the following cURL command on Endpoint A to access it:

$ curl 10.0.0.2:8080
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">
<html>
...

Initiate Connections From Endpoint B

If you need to allow Endpoint B (the udp2raw server) to initiate connections to Endpoint A (the udp2raw client) — for example, if the private HTTP service in our scenario was running on Endpoint A instead of Endpoint B — add the following PersistentKeepalive setting to the [Peer] section of Endpoint A’s config file:

# allow Endpoint B to initiate connections
PersistentKeepalive = 120

This will cause the WireGuard interface on Endpoint A to attempt to connect the WireGuard interface on Endpoint B on startup, and send a keepalive packet every 120 seconds afterwards. Udp2raw will automatically send a heartbeat request from Endpoint A to Endpoint B every second for 2 to 3 minutes after each WireGuard keepalive, which will keep the NAT hole to Endpoint A open until the next WireGuard keepalive (thus allowing Endpoint B to initiate connections to Endpoint A).

Point to Internet

If you set AllowedIPs = 0.0.0.0/0 on Endpoint A, so as to use its WireGuard connection with Endpoint B to access the Internet (or other wide range of networks), you’ll need to add an explicit route on Endpoint A for Endpoint B (otherwise Endpoint A will try to send the udp2raw tunnel itself through the WireGuard tunnel, creating an infinite loop of nested tunnels).

Run the ip route command on Endpoint A, and you’ll see output like the following:

$ ip route
default via 192.168.1.1 dev eth0 proto dhcp metric 100
192.168.1.0/24 dev eth0 proto kernel scope link src 192.168.1.11 metric 100

The default via 192.168.1.1 dev eth0 line indicates that the system uses 192.168.1.1 as its default gateway, connected through its eth0 network interface. To make sure Endpoint A will use this gateway for its udp2raw tunnel with Endpoint B, instead of the WireGuard tunnel, you need to add an explicit route for Endpoint B’s public IP address (203.0.113.2), like the following, using the same gateway and interface as the default route:

203.0.113.2 via 192.168.1.1 dev eth0

You can add another PreUp command to the WireGuard configuration on Endpoint A to add this route on wg-quick startup, like the following:

PreUp = ip route add 203.0.113.2 via 192.168.1.1 dev eth0
PostDown = ip route del 203.0.113.2 via 192.168.1.1 dev eth0

The full configuration of Endpoint A for point-to-Internet would then look like this:

# /etc/wireguard/wg0.conf

# local settings for Endpoint A
[Interface]
PrivateKey = AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEE=
Address = 10.0.0.1/32
DNS = 9.9.9.9

# route public IP of Endpoint B through LAN gateway
PreUp = ip route add 203.0.113.2 via 192.168.1.1 dev eth0
PostDown = ip route del 203.0.113.2 via 192.168.1.1 dev eth0

# send wg through udp2raw
MTU = 1342
PreUp = udp2raw -c -l 127.0.0.1:50001 -r 203.0.113.2:443 -k "shared secret" -a >/var/log/udp2raw.log 2>&1 &
PostDown = killall udp2raw || true

# remote settings for Endpoint B
[Peer]
PublicKey = fE/wdxzl0klVp/IR8UcaoGUMjqaWi3jAd7KzHKFS6Ds=
Endpoint = 127.0.0.1:50001
AllowedIPs = 0.0.0.0/0, ::/0

You don’t need to do anything special on Endpoint B (the udp2raw server) for point-to-Internet over udp2raw — just make sure you enable packet forwarding and masquerading, similar to the configuration for Host β in the basic WireGuard Point to Site Configuration guide.

Troubleshooting

Udp2raw logs

If the tunnels don’t appear to be working, first check the output of udp2raw, which we redirected into the /var/log/udp2raw.log file on each endpoint. On Endpoint A, you should see output similar to the following when udp2raw first starts up:

$ less -R /var/log/udp2raw.log
[2022-02-15 04:02:48][INFO]argc=9 udp2raw -c -l 127.0.0.1:50001 -r 203.0.113.2:443 -k shared secret -a
[2022-02-15 04:02:48][INFO]parsing address: 127.0.0.1:50001
[2022-02-15 04:02:48][INFO]its an ipv4 adress
[2022-02-15 04:02:48][INFO]ip_address is {127.0.0.1}, port is {50001}
[2022-02-15 04:02:48][INFO]parsing address: 203.0.113.2:443
[2022-02-15 04:02:48][INFO]its an ipv4 adress
[2022-02-15 04:02:48][INFO]ip_address is {203.0.113.2}, port is {443}
[2022-02-15 04:02:48][INFO]important variables: log_level=4:INFO raw_mode=faketcp cipher_mode=aes128cbc auth_mode=md5 key=shared local_addr=127.0.0.1:50001 remote_addr=203.0.113.2:443 socket_buf_size=1048576
[2022-02-15 04:02:48][WARN]you can run udp2raw with non-root account for better security. check README.md in repo for more info.
[2022-02-15 04:02:48][INFO]remote_ip=[203.0.113.2], make sure this is a vaild IP address
[2022-02-15 04:02:48][INFO]const_id:fd21137c
[2022-02-15 04:02:48][INFO]run_command iptables -N udp2rawDwrW_fd21137c_C0
[2022-02-15 04:02:48][INFO]run_command iptables -F udp2rawDwrW_fd21137c_C0
[2022-02-15 04:02:48][INFO]run_command iptables -I udp2rawDwrW_fd21137c_C0 -j DROP
[2022-02-15 04:02:48][INFO]run_command iptables -I INPUT -s 203.0.113.2 -p tcp -m tcp --sport 443 -j udp2rawDwrW_fd21137c_C0
[2022-02-15 04:02:48][WARN]auto added iptables rules
[2022-02-15 04:02:48][INFO]source_addr is now 192.168.1.11
[2022-02-15 04:02:48][INFO]using port 16692

And then output like the following when it connects to the udp2raw server:

[2022-02-15 04:05:18][INFO]state changed from client_idle to client_tcp_handshake
[2022-02-15 04:05:18][INFO](re)sent tcp syn
[2022-02-15 04:05:18][INFO]state changed from client_tcp_handshake to client_handshake1
[2022-02-15 04:05:18][INFO](re)sent handshake1
[2022-02-15 04:05:18][INFO]changed state from to client_handshake1 to client_handshake2,my_id is 25ad52ec,oppsite id is 28c7f475
[2022-02-15 04:05:18][INFO](re)sent handshake2
[2022-02-15 04:05:18][INFO]changed state from to client_handshake2 to client_ready

On Endpoint B, you’ll see similar output when udp2raw starts up:

$ less -R /var/log/udp2raw.log
[2022-02-15 04:05:13][INFO]argc=9 udp2raw -s -l 192.168.200.22:443 -r 127.0.0.1:51822 -k shared secret -a
[2022-02-15 04:05:13][INFO]parsing address: 192.168.200.22:443
[2022-02-15 04:05:13][INFO]its an ipv4 adress
[2022-02-15 04:05:13][INFO]ip_address is {192.168.200.22}, port is {443}
[2022-02-15 04:05:13][INFO]parsing address: 127.0.0.1:51822
[2022-02-15 04:05:13][INFO]its an ipv4 adress
[2022-02-15 04:05:13][INFO]ip_address is {127.0.0.1}, port is {51822}
[2022-02-15 04:05:13][INFO]important variables: log_level=4:INFO raw_mode=faketcp cipher_mode=aes128cbc auth_mode=md5 key=shared local_addr=192.168.200.22:443 remote_addr=127.0.0.1:51822 socket_buf_size=1048576
[2022-02-15 04:05:13][WARN]you can run udp2raw with non-root account for better security. check README.md in repo for more info.
[2022-02-15 04:05:13][INFO]remote_ip=[127.0.0.1], make sure this is a vaild IP address
[2022-02-15 04:05:13][INFO]const_id:74593e80
[2022-02-15 04:05:14][INFO]run_command iptables -N udp2rawDwrW_74593e80_C0
[2022-02-15 04:05:14][INFO]run_command iptables -F udp2rawDwrW_74593e80_C0
[2022-02-15 04:05:14][INFO]run_command iptables -I udp2rawDwrW_74593e80_C0 -j DROP
[2022-02-15 04:05:14][INFO]run_command iptables -I INPUT -d 192.168.200.22 -p tcp -m tcp --dport 443 -j udp2rawDwrW_74593e80_C0
[2022-02-15 04:05:14][WARN]auto added iptables rules
[2022-02-15 04:05:14][INFO]now listening at 192.168.200.22:443

And output like the following when it receives a connection from a udp2raw client:

[2022-02-15 04:05:18][INFO][198.51.100.1:35241]received syn,sent syn ack back
[2022-02-15 04:05:18][INFO][198.51.100.1:35241]got packet from a new ip
[2022-02-15 04:05:18][INFO][198.51.100.1:35241]created new conn,state: server_handshake1,my_id is f03ad003
[2022-02-15 04:05:18][INFO][198.51.100.1:35241]changed state to server_handshake1,my_id is f03ad003
[2022-02-15 04:05:18][INFO][198.51.100.1:35241]received handshake oppsite_id:c2d91811  my_id:f03ad003
[2022-02-15 04:05:18][INFO][198.51.100.1:35241]oppsite const_id:4d4e1d3e
[2022-02-15 04:05:18][INFO][198.51.100.1:35241]changed state to server_ready

If you don’t see any handshake messages in the udp2raw logs, you need to fix your udp2raw configuration.

WireGuard MTU

If connections work when they consist solely of small packets (like from ping or dig), but they don’t with large packets (like web browsing or remote desktop), you probably have an MTU problem.

You’ll also see warnings like the following in your udp2raw logs if you didn’t configure the MTU setting for WireGuard as directed above:

[2022-02-15 03:48:57][WARN]huge packet, data_len > 1800,dropped
[2022-02-15 03:51:50][WARN]huge packet,data len=1452 (>=1375).strongly suggested to set a smaller mtu at upper level,to get rid of this warn

If packets are sized too big to fit through the connection between Endpoint A and B, you’ll need to lower WireGuard’s MTU setting. If an MTU of 1342 doesn’t work, try dropping it to 1280 (the lowest legal value for IPv6). If 1280 works, try increasing it until the connection stops working. You generally want the highest MTU you can make work, since a higher MTU enables the connection to use fewer packets (and fewer packets means faster and less jitter).