WireGuard Port Forwarding

I am assuming that you have already setup WireGuard both on the internal and on the external server. See this page for more details on the topic.

Using WireGuard for port-forwarding between an external, public accessible host, and an internal non-accessible host (like behind a CGNAT) is a less known topic and i will cover it here.

You will need to use NFTables, see here for more details to better understand the following.

Assumptions:

What we need to do is create nftable rules to ensure that:

Note: forwarding ports for HTTP and HTTPS is only required if you don't plan to host a reverse proxy on the external server, which is the recomended approach. See this page for more details on chaining proxies.

Note: see here on why i use port 8443 instead of port 443 on the internal server. This is to differentiate internal connections, which have lesser security, from external connections which get additional SSO layers.

Note: if you forward HTTPS, you must also forward HTTP (80), otherwise Let's Encrypt will be unable to renew your certificates.

What we need:

Create the wg table:

nft add table ip wg

Create the base chains:

nft 'add chain ip wg prerouting { type nat hook prerouting priority -100 ; }'
nft 'add chain ip wg postrouting { type nat hook postrouting priority 100 ; }'

Create the in-bound rules:

# nft 'add rule ip wg prerouting iifname enp1s0 tcp dport 80 counter' # <<- optional for debugging purposes
# nft 'add rule ip wg prerouting iifname enp1s0 tcp dport 443 counter' # <<- optional for debugging purposes
# nft 'add rule ip wg prerouting iifname enp1s0 tcp dport 2022 counter' # <<- optional for debugging purposes
nft 'add rule ip wg prerouting iifname enp1s0 dnat to tcp dport map { 2022 : 10.100.0.1 . 22 }'
#nft 'add rule ip wg prerouting iifname enp1s0 dnat to tcp dport map { 80 : 10.100.0.1 . 80 }' optional
#nft 'add rule ip wg prerouting iifname enp1s0 dnat to tcp dport map { 443 : 10.100.0.1 . 8443 }' optional

Create the SNAT return rule (the counter rule is only for debugging, you can omit that rule):

# nft 'add rule ip wg postrouting ip daddr 10.100.0.1 counter' # <<- optional for debugging purposes
nft 'add rule ip wg postrouting ip daddr 10.100.0.1 masquerade'

This is the resulting NFTables setup:

nft list table wg
table ip wg {
        chain prerouting {
                type nat hook prerouting priority dstnat; policy accept;
                iifname "enp1s0" tcp dport 443 counter packets 100 bytes 5712
                iifname "enp1s0" tcp dport 2022 counter packets 0 bytes 0
                iifname "enp1s0" tcp dport 80 counter packets 0 bytes 0
                iifname "enp1s0" dnat ip to tcp dport map { 2022 : 10.100.0.1 . 22 }
                iifname "enp1s0" dnat ip to tcp dport map { 80 : 10.100.0.1 . 80 }
                iifname "enp1s0" dnat ip to tcp dport map { 443 : 10.100.0.1 . 8443 }
        }
 
        chain postrouting {
                type nat hook postrouting priority srcnat; policy accept;
                ip daddr 10.100.0.1 counter packets 390 bytes 25945
                ip daddr 10.100.0.1 masquerade
        }
}