WireGuard Port Forwarding From the Internet
When you have a private server that’s not publicly accessible from the Internet (for example, because it’s behind NAT), but you want to expose a service running on it to public Internet traffic, you can do so via WireGuard — as long as you have another server that is publicly accessible from the Internet. This article will show you how.
Setting up a WireGuard connection between two servers and forwarding traffic from one to the other is usually pretty easy. The part that can be tricky is returning traffic back. For example, say you have a web app running on port 8080 of a private server behind NAT (Network Address Translation) at one site, and you want to make it accessible to the public Internet through port 2000 of a public server that has a domain name of public.example.com
:
On this private server, in order for the web app to work properly, it needs to be able to access a database server and a message queue on another internal network to which its connected; and you also need to be able to access it from a second internal network for administration via SSH. So the private server can’t simply send all its traffic through its WireGuard connection to the public server — it must selectively send only traffic that constitutes responses from its web app back to the source of the forwarded public traffic.
There are several different techniques you can use to send the return traffic back through the WireGuard connection from the private server to the public server. Which technique you need to use depends on what else the private server needs to access outside of its WireGuard connection. Following are the main techniques:
Basic Connection
The basic WireGuard connection between the two servers is similar to that of the WireGuard Point to Site With Port Forwarding configuration guide.
For this example, we’ll configure WireGuard on our private server like the following, using the public server’s public IP address of 203.0.113.2
to start up a WireGuard connection with the public server; and using a PersistentKeepalive
setting to keep the connection alive through the NAT fronting the private server:
# /etc/wireguard/wg0.conf # local settings for the private server [Interface] PrivateKey = AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEE= Address = 10.0.0.1 # remote settings for the public server [Peer] PublicKey = fE/wdxzl0klVp/IR8UcaoGUMjqaWi3jAd7KzHKFS6Ds= Endpoint = 203.0.113.2:51822 AllowedIPs = 10.0.0.2 PersistentKeepalive = 25
These settings are similar to “Endpoint A” from the WireGuard Point to Site With Port Forwarding guide.
On the public server, we need to turn on packet forwarding. We also need to add a firewall rule to set up port forwarding (also known as DNAT, Destination Network Address Translation) to translate the destination address of packets sent to TCP port 2000
of the public server to TCP port 8080
on the private server (10.0.0.1
). We’ll do this via PreUp
commands in its WireGuard config (for convenience):
# /etc/wireguard/wg0.conf # local settings for the public server [Interface] PrivateKey = ABBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBFA= Address = 10.0.0.2 ListenPort = 51822 # packet forwarding PreUp = sysctl -w net.ipv4.ip_forward=1 # port forwarding PreUp = iptables -t nat -A PREROUTING -i eth0 -p tcp --dport 2000 -j DNAT --to-destination 10.0.0.1:8080 PostDown = iptables -t nat -D PREROUTING -i eth0 -p tcp --dport 2000 -j DNAT --to-destination 10.0.0.1:8080 # remote settings for the private server [Peer] PublicKey = /TOE4TKtAqVsePRVR+5AA43HkAK5DSntkOCO7nYq5xU= AllowedIPs = 10.0.0.1
These settings are similar to “Host β” from the WireGuard Point to Site With Port Forwarding guide.
Note
|
With our example public server, |
If you’re using nftables instead of iptables on your public server, instead of running the above iptables command to set up port forwarding, you’d add a rule like tcp dport 2000 dnat ip to 10.0.0.1:8080
to a nat prerouting
chain in your nftables configuration. Following is a full example nftables config file that does this:
#!/usr/sbin/nft -f flush ruleset define pub_iface = "eth0" define wg_iface = "wg0" define wg_port = 51822 table inet filter { chain input { type filter hook input priority 0; policy drop; # accept all loopback packets iif "lo" accept # accept all icmp/icmpv6 packets meta l4proto { icmp, ipv6-icmp } accept # accept all packets that are part of an already-established connection ct state vmap { invalid : drop, established : accept, related : accept } # drop new connections over rate limit ct state new limit rate over 1/second burst 10 packets drop # accept all DHCPv6 packets received at a link-local address ip6 daddr fe80::/64 udp dport dhcpv6-client accept # accept all SSH packets received on a public interface iifname $pub_iface tcp dport ssh accept # accept all WireGuard packets received on a public interface iifname $pub_iface udp dport $wg_port accept # reject with polite "port unreachable" icmp response reject } chain forward { type filter hook forward priority 0; policy drop; # forward all packets that are part of an already-established connection ct state vmap { invalid : drop, established : accept, related : accept } # forward any incoming packets from a public interface that will go out through WireGuard iifname $pub_iface oifname $wg_iface accept # reject with polite "host unreachable" icmp response reject with icmpx type host-unreachable } } table inet nat { chain prerouting { type nat hook prerouting priority -100; policy accept; # rewrite destination address of TCP port 2000 packets to port 8080 on 10.0.0.1 iifname $pub_iface tcp dport 2000 dnat ip to 10.0.0.1:8080 } }
See the Point to Site With Port Forwarding section of the WireGuard Nftables configuration guide for a full explanation of these firewall rules.
This configuration will give you basic connectivity between the private server and the public server (test it out by running curl 10.0.0.1:8080
on the public server — you should see output from the web app on the private server). It won’t allow any Internet traffic to be forwarded between the two servers, however. To do so, you must add one of the techniques from the following sections.
Masquerading
The simplest way to allow return traffic to be forwarded back from the private server to the public server is to use SNAT (Source Network Address Translation) on the public server to translate the source address of forwarded packets to use the public server’s own IP address. When SNAT is used this way, it is known as “masquerading” (in this example, the public server is “masquerading” the identity of packets from the Internet with its own identity).
Because the private server receives these forwarded packets with their source address already rewritten to use the public server’s WireGuard IP address, no routing changes need to be made on the private server — the private server knows it can reply to these packets by sending traffic directly back to the public server. The public server’s firewall takes care of remembering the original traffic sources, and rewriting reply-packet destinations back to the original source IP addresses.
To apply masquerading with iptables, add the following PreUp
and PostDown
commands to the public server’s WireGuard config file:
# /etc/wireguard/wg0.conf # local settings for the public server [Interface] PrivateKey = ABBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBFA= Address = 10.0.0.2 ListenPort = 51822 # packet forwarding PreUp = sysctl -w net.ipv4.ip_forward=1 # port forwarding PreUp = iptables -t nat -A PREROUTING -i eth0 -p tcp --dport 2000 -j DNAT --to-destination 10.0.0.1:8080 PostDown = iptables -t nat -D PREROUTING -i eth0 -p tcp --dport 2000 -j DNAT --to-destination 10.0.0.1:8080 # packet masquerading PreUp = iptables -t nat -A POSTROUTING -o wg0 -j MASQUERADE PostDown = iptables -t nat -D POSTROUTING -o wg0 -j MASQUERADE # remote settings for the private server [Peer] PublicKey = /TOE4TKtAqVsePRVR+5AA43HkAK5DSntkOCO7nYq5xU= AllowedIPs = 10.0.0.1
Or if you’re using nftables instead of iptables, add the following nat postrouting
chain to the public service’s nftables config file:
#!/usr/sbin/nft -f flush ruleset # ... table inet nat { chain prerouting { type nat hook prerouting priority -100; policy accept; # rewrite destination address of TCP port 2000 packets to port 8080 on 10.0.0.1 iifname $pub_iface tcp dport 2000 dnat ip to 10.0.0.1:8080 } chain postrouting { type nat hook postrouting priority 100; policy accept; # masquerade all packets going out through WireGuard oifname $wg_iface masquerade } }
The downside of masquerading is that it hides the original source IP address of clients using the web app from the web app itself — all traffic the private server receives will use the public server’s WireGuard IP address (10.0.0.2
) as its source. But you should now at least be able to connect to the web app on the private server from any device over the Internet, using the public server’s hostname and port (eg curl public.example.com:2000
).
Static Routes
If you know ahead of time that your Internet traffic will only come from a few, static ranges of IP addresses (like say 198.51.100.87
and the range 192.0.2.144
to 192.0.2.147
), another option you can use is simply to define static routes for these IP addresses on the private server, so that it will always send traffic back to those IP addresses through the public server. This is the same approach used in the WireGuard Point to Site With Port Forwarding article, where we know all the traffic will come from Site B’s subnet (192.168.200.0/24
in that article).
All you have to do in this case is add the static IP addresses to the AllowedIPs
setting in the private server’s WireGuard config:
# /etc/wireguard/wg0.conf # local settings for the private server [Interface] PrivateKey = AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEE= Address = 10.0.0.1 # remote settings for the public server [Peer] PublicKey = fE/wdxzl0klVp/IR8UcaoGUMjqaWi3jAd7KzHKFS6Ds= Endpoint = 203.0.113.2:51822 AllowedIPs = 10.0.0.2, 198.51.100.87, 192.0.2.144/30 PersistentKeepalive = 25
When you do this, you’ll be able to connect to the web app on the private server using the public server’s hostname and port (eg curl public.example.com:2000
) from any host that uses one of those AllowedIPs
addresses as its public Internet address (such as 192.0.2.145
). The web app on the private server will see the original source IP address (eg 192.0.2.145
), but still will be able to correctly send replies back through the public server.
Default Route
If the private server doesn’t actually need to access a separate database and message queue (or if it could instead access them through WireGuard), and you didn’t actually need to to SSH into the private server for administration (or if you could instead SSH into it through WireGuard), you could let WireGuard take over the private server’s default route — and send all its traffic through WireGuard to the public server by default.
In that case, you’d simply set the AllowedIPs
setting to 0.0.0.0/0
in the private server’s WireGuard config:
# /etc/wireguard/wg0.conf # local settings for the private server [Interface] PrivateKey = AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEE= Address = 10.0.0.1 # remote settings for the public server [Peer] PublicKey = fE/wdxzl0klVp/IR8UcaoGUMjqaWi3jAd7KzHKFS6Ds= Endpoint = 203.0.113.2:51822 AllowedIPs = 0.0.0.0/0 PersistentKeepalive = 25
If you did this, you’d be able to connect to the web app on the private server from any host on the Internet, using the public server’s hostname and port (eg curl public.example.com:2000
). The web app on the private server would see the original source IP address, and would send replies (as well as all its other traffic) through its WireGuard connection to the public server.
Tip
|
If you use this technique, you usually should also add packet masquerading to the public server — but only to masquerade connections outbound to the Internet — not inbound to the WireGuard tunnel like the Masquerading section above. For example, if using iptables, you might add the following to the WireGuard configuration of the public server:
These rules won’t affect inbound connections that use port forwarding to reach to the private server; they will only affect outbound connections that are initiated by the private server itself (such as to download system updates, make DNS queries, etc). |
Default Route With Exceptions
If you know ahead of time that the private server’s internal traffic is limited to just a few subnets, you can let WireGuard take over the private server’s default route, and add a few static routes for your internal traffic as exceptions. This is the inverse of the Static Routes option described above — instead of enumerating the IP addresses or ranges you always want to send through the WireGuard tunnel, you enumerate the IP addresses or ranges you never want to send through the WireGuard tunnel.
This is perfect for the example scenario, where the database and message queue used by the private server are on the 10.11.12.0/24
subnet, and SSH access will only come from the 192.168.99.0/24
subnet. In this case, you’d set the private server’s WireGuard config to use an AllowedIPs
value of 0.0.0.0/0
, and add explicit routes on the private server for these internal networks — which you can also do via its WireGuard config file:
# /etc/wireguard/wg0.conf # local settings for the private server [Interface] PrivateKey = AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEE= Address = 10.0.0.1 # static route for database and message queue PreUp = ip route add 10.11.12.0/24 via 192.168.1.1 dev eth0 PostDown = ip route del 10.11.12.0/24 via 192.168.1.1 dev eth0 # static route for SSH access PreUp = ip route add 192.168.99.0/24 via 192.168.1.1 dev eth0 PostDown = ip route del 192.168.99.0/24 via 192.168.1.1 dev eth0 # remote settings for the public server [Peer] PublicKey = fE/wdxzl0klVp/IR8UcaoGUMjqaWi3jAd7KzHKFS6Ds= Endpoint = 203.0.113.2:51822 AllowedIPs = 0.0.0.0/0 PersistentKeepalive = 25
Make sure you use the private server’s actual default gateway and network interface instead of 192.168.1.1
and eth0
. You can determine the server’s default gateway by running the ip route
command — the line beginning with default
shows the default gateway:
$ ip route
default via 192.168.1.1 dev eth0 proto dhcp src 192.168.1.11 metric 100
192.168.1.0/24 dev eth0 proto kernel scope link src 192.168.1.11 metric 100
If you use this approach, you’ll be able to connect to the web app on the private server from any host on the Internet, using the public server’s hostname and port (eg curl public.example.com:2000
). The web app on the private server will see the original source IP address of all Internet traffic, but will still be able to correctly send replies back through the public server.
Policy Routing
The final option is to use policy routing. It’s also a really good fit for the example scenario. Usually, the way you use policy routing with WireGuard is by combining three things:
-
Add a
Table
setting to the[Interface]
section of your WireGuard config. This directs wq-quick to add routes to a custom table instead of your main routing table. -
Add your custom policy rules via the
ip rule add
command to direct selected traffic to use the custom table. -
Set
AllowedIPs = 0.0.0.0/0
for one of the peers in your WireGuard config. This will ensure that any traffic directed to use the custom routing table (and not matching theAllowedIPs
of some other peer) will be sent to that peer.
To return all traffic on the private server that originally entered through its WireGuard interface back through that same interface, use this for the private server’s WireGuard config:
# /etc/wireguard/wg0.conf # local settings for the private server [Interface] PrivateKey = AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEE= Address = 10.0.0.1 Table = 123 PreUp = ip rule add from 10.0.0.1 table 123 priority 456 PostDown = ip rule del from 10.0.0.1 table 123 priority 456 # remote settings for the public server [Peer] PublicKey = fE/wdxzl0klVp/IR8UcaoGUMjqaWi3jAd7KzHKFS6Ds= Endpoint = 203.0.113.2:51822 AllowedIPs = 0.0.0.0/0 PersistentKeepalive = 25
The above policy rule works because return traffic will use the same IP address for the source of its return packets as the original packets used for their destination address. The destination address of the packets forwarded from the public server to the private server were all translated to 10.0.0.1
by the public server; so reply packets generated by the private server will use 10.0.0.1
as their source address. The above policy rule matches these packets, and ensures they are routed via the custom 123
routing table; and wg-quick sets up the default route of this table to use the WireGuard interface.
Tip
|
To reference a custom routing table by name instead of by number, add an entry for it to your
You can then reference table
|
With this approach, you’ll be able to connect to the web app on the private server from any host on the Internet, using the public server’s hostname and port (eg curl public.example.com:2000
). The web app on the private server will see the original source IP address, but will also be able to correctly send replies back through the public server. All the private server’s other traffic, including traffic originating on the private server itself, will continue to use the server’s main routing table, unaffected by your WireGuard configuration.
Additional Techniques
See the WireGuard Port Forwarding From the Internet to Other Networks article for a few advanced examples of applying these techniques. That article also covers the Connection Marking technique, which you may need to use when forwarding connections to Docker containers or through multiple hops to other internal servers (or generally whenever the correct routing decision for return packets can only be made by remembering something about their original inbound packets or connection).