WireGuard Transparent Tunnel
Sometimes you may have a network service that you want to expose to clients of a WireGuard network, as well as to clients not using WireGuard, using the same IP address regardless of whether or not they’re running WireGuard. In other words, the WireGuard tunnel is used “transparently” when up, and ignored when down. This article will show you how.
For example, say we have a webserver running on Endpoint B, with a public IP address of 203.0.113.2
, and a DNS name of app.example.com
. We want any client, like Endpoint C, to be able to access the webserver at https://app.example.com
over the Internet — but we also want Endpoint A to be able to access the webserver through its point-to-point WireGuard connection to Endpoint B when up, transparently using the same IP address and DNS name with WireGuard running as without:
If we have a simple Point-to-Point WireGuard Configuration set up between Endpoint A and Endpoint B, Endpoint A would be able to access the webserver on Endpoint B through WireGuard by using Endpoint B’s WireGuard address of 10.0.0.2
:
$ curl --insecure https://10.0.0.2
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">
<html>
...
Note
|
When attempting to access an HTTPS URL, the
|
However, when using Endpoint B’s public IP address (203.0.113.2
) and DNS name (app.example.com
), access from Endpoint A would still go over the public Internet instead of through the WireGuard tunnel:
$ curl https://app.example.com
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">
<html>
...
We can see this by checking the webserver’s access logs on Endpoint B — requests that were sent through the WireGuard tunnel will show up with Endpoint A’s WireGuard IP address (10.0.0.1
) instead of its public IP address (198.51.100.1
):
$ tail -F /var/log/nginx/access.log
192.0.2.3 - - [05/May/2023:00:10:17 +0000] "GET /" 200 28786 "-" "curl/7.81.0"
10.0.0.1 - - [05/May/2023:00:12:09 +0000] "GET /" 200 28786 "-" "curl/7.81.0"
198.51.100.1 - - [05/May/2023:00:12:31 +0000] "GET /" 200 28786 "-" "curl/7.81.0"
Transparent Access From Endpoint A
If we had already set up the WireGuard configuration on Endpoint A as described in the Point-to-Point WireGuard Configuration article, its configuration would look like the following:
# /etc/wireguard/wg0.conf
# local settings for Endpoint A
[Interface]
PrivateKey = AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEE=
Address = 10.0.0.1/32
ListenPort = 51821
# remote settings for Endpoint B
[Peer]
PublicKey = fE/wdxzl0klVp/IR8UcaoGUMjqaWi3jAd7KzHKFS6Ds=
Endpoint = 203.0.113.2:51822
AllowedIPs = 10.0.0.2/32
To allow Endpoint A to transparently use its WireGuard connection to Endpoint B to access the webserver on Endpoint B with Endpoint B’s public IP address and DNS name, we need to make four changes to this config file.
So shut down the WireGuard interface on Endpoint A (eg sudo wg-quick down wg0
), and make the following changes to Endpoint A’s WireGuard configuration:
Add Public IP to AllowedIPs
The first thing we need to do on Endpoint A is add Endpoint B’s public IP address to the AllowedIPs
setting in Endpoint A’s WireGuard configuration:
AllowedIPs = 10.0.0.2/32, 203.0.113.2/32
This would normally break Endpoint A’s WireGuard connection with Endpoint B, as the address in its Endpoint
setting is now also included in its AllowedIPs
setting (which would normally cause Endpoint A to try to route encrypted WireGuard traffic to Endpoint B back through the WireGuard interface again and again in an infinite loop).
Use Custom Routing Table
To avoid this breakage, we need to use a custom routing table for the WireGuard interface. We can do this by adding the Table
setting to the [Interface]
section of Endpoint A’s WireGuard config, setting it to any unused routing table number (such as 123
):
Table = 123
When we start up the interface, routes for all addresses included in the interface’s AllowedIPs
settings will be added to this custom table (instead of to the main routing table).
Tip
|
View the routes for a custom routing table by running the
|
By default, however, Endpoint A won’t route any traffic using this custom table.
Add Policy Routing for Public IP
So next we need to add some policy routing rules that tell Endpoint A when to use this custom routing table. We can add these rules via PreUp
or PostUp
scripts in Endpoint A’s WireGuard configuration, and tear them down via corresponding PostDown
or PreDown
scripts.
Tip
|
When editing |
We need to make sure encrypted WireGuard traffic sent to Endpoint B’s WireGuard listen port is routed out Endpoint A’s physical Ethernet interface as normal, but all other traffic sent to Endpoint B is routed out Endpoint A’s virtual WireGuard interface first (where it will be encrypted and tunneled to Endpoint B’s WireGuard listen port). We can accomplish this with two rules.
The first rule routes traffic to the listen port (51822
) of Endpoint B (203.0.113.2
) using Endpoint A’s normal (main
) routing table:
PreUp = ip rule add to 203.0.113.2 dport 51822 table main priority 455
PostDown = ip rule del to 203.0.113.2 dport 51822 table main priority 455
And the second rule routes all other traffic to Endpoint B using the custom routing table (123
) we configured via the Table
setting above:
PreUp = ip rule add to 203.0.113.2 table 123 priority 456
PostDown = ip rule del to 203.0.113.2 table 123 priority 456
Make sure to give the first rule a smaller priority
number than the second (455
vs 456
in this example), as rules with smaller priority
numbers are evaluated first.
Tip
|
View the policy routing rules on a host by running the
Custom rules usually should be set with a priority number that places them between the rule for the |
Add Route for WireGuard IP
At this point, we’ve done enough on Endpoint A to ensure that any traffic to Endpoint B’s public address of 203.0.113.2
will use be routed through WireGuard. However, we’ve broken Endpoint A’s ability to route traffic to Endpoint B’s WireGuard address of 10.0.0.2
. We can fix this with any one of three options:
Expand Address Subnet Mask
The first option to fix this is to expand the network prefix (aka subnet mask) specified for the WireGuard interface on Endpoint A to include Endpoint B’s WireGuard address. Instead of using a network prefix of /32
, we could use /30
:
Address = 10.0.0.1/30
This will cause iproute2 to automatically add a route for the 10.0.0.0/30
subnet (consisting of addresses 10.0.0.0
through 10.0.0.3
) when the WireGuard interface is started up and its address is assigned.
Tip
|
If Endpoint A was connected to more hosts than just Endpoint B through the same WireGuard interface, we might want to use a different subnet mask to include all those hosts in the same route. For example, if hosts with addresses from |
Add Route to Main Table
The second option would be to explicitly add a route for 10.0.0.2
to Endpoint A’s main routing table, via a PostUp
script:
PostUp = ip route add 10.0.0.2 dev wg0
Note
|
Since this command can be executed only after the interface is already up, we must run it in as |
Test It Out
After making all four changes, using the first option (Expand Address Subnet Mask) for the fourth change, the WireGuard configuration for Endpoint A will look like this:
# /etc/wireguard/wg0.conf
# local settings for Endpoint A
[Interface]
PrivateKey = AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEE=
Address = 10.0.0.1/30
ListenPort = 51821
Table = 123
PreUp = ip rule add to 203.0.113.2 dport 51822 table main priority 455
PostDown = ip rule del to 203.0.113.2 dport 51822 table main priority 455
PreUp = ip rule add to 203.0.113.2 table 123 priority 456
PostDown = ip rule del to 203.0.113.2 table 123 priority 456
# remote settings for Endpoint B
[Peer]
PublicKey = fE/wdxzl0klVp/IR8UcaoGUMjqaWi3jAd7KzHKFS6Ds=
Endpoint = 203.0.113.2:51822
AllowedIPs = 10.0.0.2/32, 203.0.113.2/32
Now start the interface up (eg sudo wg-quick up wg0
). If you haven’t already set up the WireGuard configuration on Endpoint B, follow the directions in the Point-to-Point WireGuard Configuration article to do so now.
Once WireGuard is up and running on both endpoints, we should be able to access Endpoint B using its IP address and public DNS name from Endpoint A, transparently through the WireGuard tunnel:
$ curl https://app.example.com
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">
<html>
...
We can verify that requests from Endpoint A are coming through the WireGuard tunnel by checking the webserver’s access logs on Endpoint B. Requests for https://app.example.com
from Endpoint A should now show up with Endpoint A’s WireGuard IP address (10.0.0.1
) instead of its public IP address (198.51.100.1
):
$ tail -F /var/log/nginx/access.log
192.0.2.3 - - [05/May/2023:00:10:17 +0000] "GET /" 200 28786 "-" "curl/7.81.0"
10.0.0.1 - - [05/May/2023:00:12:09 +0000] "GET /" 200 28786 "-" "curl/7.81.0"
198.51.100.1 - - [05/May/2023:00:12:31 +0000] "GET /" 200 28786 "-" "curl/7.81.0"
10.0.0.1 - - [05/May/2023:00:33:55 +0000] "GET /" 200 28786 "-" "curl/7.81.0"
Transparent Access to Both Endpoints
After making the above changes, traffic sent from Endpoint A to Endpoint B using Endpoint B’s public IP address (203.0.133.2
) as its destination address will now be routed through the WireGuard tunnel. However, the source address for this traffic will still use Endpoint A’s WireGuard IP address (10.0.0.1
).
If we want to alter this source address so that the source of the traffic appears from Endpoint B’s perspective to be using Endpoint A’s public address (198.51.100.1
), we need to do two more things:
-
Make the same changes to the WireGuard Configuration for Endpoint B that we made to Endpoint A
-
Add an SNAT (Source Network Address Translation) rule either to the Firewall for Endpoint A or the Firewall for Endpoint B
WireGuard Configuration for Endpoint B
On Endpoint B, shut down its WireGuard interface, and make the same changes we made to its configuration that we made on Endpoint A:
# /etc/wireguard/wg0.conf
# local settings for Endpoint B
[Interface]
PrivateKey = ABBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBFA=
Address = 10.0.0.2/30
ListenPort = 51822
Table = 123
PreUp = ip rule add to 198.51.100.1 dport 51821 table main priority 455
PostDown = ip rule del to 198.51.100.1 dport 51821 table main priority 455
PreUp = ip rule add to 198.51.100.1 table 123 priority 456
PostDown = ip rule del to 198.51.100.1 table 123 priority 456
# remote settings for Endpoint A
[Peer]
PublicKey = /TOE4TKtAqVsePRVR+5AA43HkAK5DSntkOCO7nYq5xU=
AllowedIPs = 10.0.0.1/32, 198.51.100.1/32
Since Endpoint A’s public IP address is 198.51.100.1
and its WireGuard listen port is 51821
, here we’ve used 198.51.100.1
in Endpoint B’s configuration where we used 203.0.113.2
in Endpoint A’s configuration, and 51821
where we used 51822
.
Firewall for Endpoint A
To set up a SNAT rule for Endpoint A’s traffic on Endpoint A’s own firewall, the rule must be added to the POSTROUTING
chain of the nat
table. We could do this with iptables via a PreUp
script in Endpoint A’s WireGuard config file like the following:
PreUp = iptables -A POSTROUTING -t nat -s 10.0.0.1 -d 203.0.113.2 -j SNAT --to-source 198.51.100.1
PostDown = iptables -D POSTROUTING -t nat -s 10.0.0.1 -d 203.0.113.2 -j SNAT --to-source 198.51.100.1
Firewall for Endpoint B
Alternatively, to set up a SNAT rule for Endpoint A’s traffic on Endpoint B’s firewall, the rule must be added to the INPUT
chain of the nat
table. We could do this with iptables via a PreUp
script in Endpoint B’s WireGuard config file like the following:
PreUp = iptables -A INPUT -t nat -s 10.0.0.1 -d 203.0.113.2 -j SNAT --to-source 198.51.100.1
PostDown = iptables -D INPUT -t nat -s 10.0.0.1 -d 203.0.113.2 -j SNAT --to-source 198.51.100.1
Test It Out
After making those changes for Endpoint B, and starting its WireGuard interface back up (eg sudo wg-quick up wg0
), we should still be able to access Endpoint B using its IP address and public DNS name from Endpoint A, transparently through the WireGuard tunnel:
$ curl https://app.example.com
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">
<html>
...
But now if we check the webserver’s access logs on Endpoint B, we should see requests sent through the WireGuard tunnel recorded with Endpoint A’s public IP address (198.51.100.1
) instead of its WireGuard IP address (10.0.0.1
):
$ tail -F /var/log/nginx/access.log
192.0.2.3 - - [05/May/2023:00:10:17 +0000] "GET /" 200 28786 "-" "curl/7.81.0"
10.0.0.1 - - [05/May/2023:00:12:09 +0000] "GET /" 200 28786 "-" "curl/7.81.0"
198.51.100.1 - - [05/May/2023:00:12:31 +0000] "GET /" 200 28786 "-" "curl/7.81.0"
10.0.0.1 - - [05/May/2023:00:33:55 +0000] "GET /" 200 28786 "-" "curl/7.81.0"
198.51.100.1 - - [05/May/2023:00:41:12 +0000] "GET /" 200 28786 "-" "curl/7.81.0"
However, now that all traffic sent through the WireGuard tunnel is transparently using the public IP addresses of Endpoint A and Endpoint B, how do we tell the difference between traffic sent through the tunnel and traffic sent over the public Internet? We can tell the difference by checking the network interface used by the traffic.
If we use tcpdump to monitor the wg0
interface of Endpoint A or Endpoint B, and run our cURL command again, we’ll see the request come through the WireGuard tunnel:
$ sudo tcpdump -niwg0
00:42:23.683002 wg0 Out IP 198.51.100.1.55764 > 203.0.113.2.443: Flags [S], seq 1216151227, win 62727, options [mss 1400,sackOK,TS val 1162632069 ecr 0,nop,wscale 6], length 0
00:42:23.692731 wg0 In IP 203.0.113.2.443 > 198.51.100.1.55764: Flags [S.], seq 1045553059, ack 1216151228, win 65535, options [mss 1400,sackOK,TS val 2488797916 ecr 1162632069,nop,wscale 10], length 0
...