Overview
Your domain points at a cheap VPS; the VPS forwards traffic through a WireGuard tunnel to your home router, and your servers still see the real visitor IP. Your home IP never appears in DNS, and you open no inbound ports at home.
How a request flows:
- A visitor resolves your domain and gets the VPS public IP.
- The VPS DNATs ports 80/443 into the tunnel. It does not SNAT, so the visitor’s real source IP is kept.
- Your home router receives the packet on its WireGuard interface and port-forwards it to the server.
- The server replies to the visitor’s IP. The home router sends that reply back up the tunnel, not out your ISP connection.
- The VPS reverses the DNAT and the reply leaves from the VPS public IP.
Why most guides lose the client IP: they add an SNAT/masquerade rule on the VPS so replies naturally return through the tunnel. It works, but every visitor then appears as the VPS tunnel IP in your logs, fail2ban, CrowdSec and game servers. This guide skips the SNAT and solves the return path on the home router instead (step 4). That is the only hard part, and each router handles it differently.
Prerequisites and example values
You need a small Linux VPS with a static public IPv4 address (any provider; a Linode Nanode is plenty), a home router that runs WireGuard, and DNS control for your domain. Substitute your own values for the examples below everywhere hey appear.
| Item | Example value |
|---|---|
| VPS public IP | VPS_IP |
| VPS public interface | eth0 (check with ip a) |
| VPS tunnel IP | 10.99.0.1/30 |
| Home router tunnel IP | 10.99.0.2/30 |
| WireGuard port | UDP 51820 |
| Home server (reverse proxy) | 192.168.1.10 |
| Forwarded ports | TCP 80, 443 |
| Home router LAN interface | eth1 (VyOS) or LAN |
| Policy routing table / connmark (VyOS) | 101 / 100 |
Pick a tunnel subnet that doesn’t overlap anything at home, and a routing table
number not already used by another policy.
Part 1: VPS setup (same for every home router)
The VPS is the WireGuard listener and a dumb packet forwarder: DNAT in, no SNAT. These steps assume Debian or Ubuntu.
- Install packages and enable forwarding.
apt update && apt install -y wireguard nftables
echo 'net.ipv4.ip_forward = 1' > /etc/sysctl.d/99-relay.conf
sysctl --system
- Generate the VPS keys. Save the public key for the home router.
cd /etc/wireguard
umask 077; wg genkey | tee privatekey | wg pubkey > publickey
cat publickey
- Create
/etc/wireguard/wg0.conf. The home router’s public key comes from
Part 2.
[Interface]
Address = 10.99.0.1/30
ListenPort = 51820
PrivateKey = <contents of /etc/wireguard/privatekey>
[Peer]
# home router
PublicKey = <home router public key>
AllowedIPs = 10.99.0.2/32
- Replace
/etc/nftables.confwith the full ruleset: VPS protection, the
DNAT into the tunnel, and forwarding.
#!/usr/sbin/nft -f
flush ruleset
table inet relay {
chain input {
type filter hook input priority filter; policy drop;
ct state established,related accept
iif "lo" accept
meta l4proto icmp accept
tcp dport 22 accept # Allow SSH
udp dport 51820 accept # Allow WG from home
}
chain prerouting {
type nat hook prerouting priority dstnat;
iifname "eth0" tcp dport { 80, 443 } dnat ip to 10.99.0.2
}
chain forward {
type filter hook forward priority filter; policy drop;
tcp flags syn tcp option maxseg size set rt mtu
ct state established,related accept
iifname "eth0" oifname "wg0" ct status dnat accept
}
}
- Start everything.
systemctl enable --now nftables
systemctl enable --now wg-quick@wg0
nft list ruleset
Notes:
- There is deliberately no SNAT or masquerade rule. Adding one “fixes”
routing but replaces every visitor IP with 10.99.0.1.iifnameandoifnamematch by name, so the ruleset loads at boot even
before wg0 exists. Start order doesn’t matter, and restarting either service
never breaks the other.flush rulesetmakes this file the VPS’s entire firewall. If you later run
Docker or anything else that adds its own nftables rules on the VPS, switch
to per-table resets instead.- The VPS peer has no
Endpoint. It learns the home router’s current address
from the handshake, so a dynamic home IP or CGNAT is fine.
Router configurations
VyOS Configuration
VyOS has no automatic reply-to, so you mark
connections that arrive on wg0 and policy-route their replies back into the tunnel. Tested on VyOS 1.4+ with the zone-based firewall.
- Generate keys (operational mode). Put the public key in the VPS
wg0.confand runsystemctl restart wg-quick@wg0on the VPS.
generate pki wireguard key-pair
- Create the tunnel (configure mode).
set interfaces wireguard wg0 address 10.99.0.2/30
set interfaces wireguard wg0 private-key <VyOS private key>
set interfaces wireguard wg0 ip adjust-mss clamp-mss-to-pmtu
set interfaces wireguard wg0 peer vps public-key <VPS public key>
set interfaces wireguard wg0 peer vps address VPS_IP
set interfaces wireguard wg0 peer vps port 51820
set interfaces wireguard wg0 peer vps allowed-ips 0.0.0.0/0
set interfaces wireguard wg0 peer vps persistent-keepalive 25
allowed-ips 0.0.0.0/0 lets packets with real internet source IPs in. VyOS does not create routes from allowed-ips, so your default route is untouched.
- Port-forward to the server.
set nat destination rule 100 inbound-interface name wg0
set nat destination rule 100 protocol tcp
set nat destination rule 100 destination port 80,443
set nat destination rule 100 translation address 192.168.1.10
- Route replies back through the tunnel. Connections arriving on wg0 get connmark 100; replies from the LAN carrying that mark use table 101, whose only route is a default via wg0.
set policy route WG-MARK interface wg0
set policy route WG-MARK rule 10 state new
set policy route WG-MARK rule 10 set connection-mark 100
set policy route WG-RETURN interface eth1
set policy route WG-RETURN rule 10 connection-mark 100
set policy route WG-RETURN rule 10 set table 101
set protocols static table 101 route 0.0.0.0/0 interface wg0
An interface takes only one policy route. If eth1 already has one, add the rule to that policy with a low rule number (for example rule 5) instead of creating WG-RETURN. Use a table number and connmark no other policy uses.
- Allow the traffic through the firewall. Use option A if you run the zone-based firewall, option B if you use the plain forward filter. Firewall rules see traffic after DNAT, so both match the server’s inside address.
Option A: zone-based firewall. Give wg0 its own zone. Rename LAN to match
your LAN zone.
set firewall zone WG member interface wg0
set firewall zone WG default-action drop
set firewall ipv4 name WG-LAN default-action drop
set firewall ipv4 name WG-LAN rule 10 action accept
set firewall ipv4 name WG-LAN rule 10 state established
set firewall ipv4 name WG-LAN rule 10 state related
set firewall ipv4 name WG-LAN rule 20 action accept
set firewall ipv4 name WG-LAN rule 20 connection-status nat destination
set firewall ipv4 name WG-LAN rule 20 protocol tcp
set firewall ipv4 name WG-LAN rule 20 destination address 192.168.1.10
set firewall ipv4 name WG-LAN rule 20 destination port 80,443
set firewall zone LAN from WG firewall name WG-LAN
set firewall ipv4 name LAN-WG default-action drop
set firewall ipv4 name LAN-WG rule 10 action accept
set firewall ipv4 name LAN-WG rule 10 state established
set firewall ipv4 name LAN-WG rule 10 state related
set firewall zone WG from LAN firewall name LAN-WG
Option B: forward filter. Add one accept rule for the port-forwarded traffic. Pick a rule number that sits before any drop rule in your forward chain.
set firewall ipv4 forward filter rule 50 action accept
set firewall ipv4 forward filter rule 50 inbound-interface name wg0
set firewall ipv4 forward filter rule 50 connection-status nat destination
set firewall ipv4 forward filter rule 50 protocol tcp
set firewall ipv4 forward filter rule 50 destination address 192.168.1.10
set firewall ipv4 forward filter rule 50 destination port 80,443
The server’s replies also need to pass. If your forward filter has a drop default and no established/related accept rule yet, add the global state policy:
set firewall global-options state-policy established action accept
set firewall global-options state-policy related action accept
If your forward filter’s default action is accept, option B isn’t strictly needed, but the explicit rule documents the intent and keeps working if you tighten the default later.
- Commit.
commit
save
Verify with show interfaces wireguard wg0 (recent handshake), show ip route table 101, and the rule 20 counter in show firewall ipv4 name WG-LAN (or rule 50 in show firewall ipv4 forward filter). Keep firewall global-options source-validation off or loose; strict mode drops the tunneled packets.
PFSense Configuration
pfSense solves the return path for you:
firewall rules on an assigned WireGuard interface that has an upstream gateway get reply-to, so replies exit the interface the connection came in on (Netgate docs). No policy routing is needed. This follows the [Lawrence Systems CGNAT guide (Hosting Behind CGNAT With Wireguard) for the pfSense side, with the VPS SNAT rule removed (Part 1 already omits it).
- Install WireGuard under System > Package Manager if it isn’t already.
- Pin the default gateway. System > Routing > Gateways: set Default
gateway IPv4 to your WAN gateway, not Automatic. Otherwise pfSense may
choose the tunnel as the default route. - Create the tunnel. VPN > WireGuard > Tunnels > Add: enable it, generate
keys, and copy the public key into the VPSwg0.conf. Leave the tunnel
address blank; it is set on the interface in step 5. - Add the peer. VPN > WireGuard > Peers > Add:
- Tunnel: the one from step 3
- Endpoint:
VPS_IP, port51820 - Keep Alive:
25 - Public Key: the VPS public key
- Allowed IPs:
0.0.0.0/0. This is needed for real client source IPs, and
pfSense never creates routes from Allowed IPs, so it doesn’t hijack traffic.
- Assign and configure the interface. Interfaces > Assignments: add
tun_wg0, then open it:
- Enable, description
WG_VPS - IPv4 Configuration Type: Static,
10.99.0.2/30 - IPv4 Upstream Gateway: click Add a new gateway, name it
WG_VPS_GW, IP10.99.0.1, and leave Default gateway unchecked. Selecting an upstream
gateway makes pfSense treat this as a WAN-type interface, which is what gives
its rulesreply-to. The default route stays on WAN because of step 2. - MTU
1420, MSS1420. pfSense subtracts the 40-byte TCP/IPv4 header from the
MSS field, so this clamps to 1380.
- Set gateway monitoring. System > Routing > Gateways, edit
WG_VPS_GW:
monitor10.99.0.1or disable monitoring. - Port-forward. Firewall > NAT > Port Forward > Add:
- Interface:
WG_VPS - Protocol: TCP
- Destination:
WG_VPS address, ports 80 and 443 (one rule each, or a port
alias) - Redirect target:
192.168.1.10, same ports - Filter rule association: add associated filter rule
- Check rule placement. The pass rules must sit on the
WG_VPStab.
Remove or disable any matching pass rule on the WireGuard group tab:
group rules are matched first and don’t getreply-to, so replies would
leave via WAN. - Confirm System > Advanced > Firewall & NAT > Disable reply-to is
unchecked.
Verify under Status > WireGuard (handshake), then watch Diagnostics > States while loading the site: the state shows the real client IP on WG_VPS.
OPNSense Configuration
OPNsense uses reply-to like pfSense, but because the WireGuard interface has no IP configuration of its own, you set reply-to explicitly on the pass rule. WireGuard is built in since 24.1. Menu names follow the [OPNsense WireGuard docs (WireGuard Selective Routing to External VPN Endpoint — OPNsense documentation)
.
- Add the peer. VPN > WireGuard > Peers > +:
- Public Key: the VPS public key
- Allowed IPs:
0.0.0.0/0 - Endpoint Address:
VPS_IP, Endpoint Port:51820 - Keepalive:
25
- Add the instance. VPN > WireGuard > Instances > + (advanced mode on):
- Tunnel Address:
10.99.0.2/30 - Peers: the peer from step 1
- Disable Routes: checked. Without this, Allowed IPs
0.0.0.0/0installs a
default route into the tunnel. - Gateway:
10.99.0.1 - Save, then copy the generated public key into the VPS
wg0.conf.
- Enable WireGuard under VPN > WireGuard > General.
- Assign the interface. Interfaces > Assignments: add
wg0asWG_VPS.
Open it, enable it, set IPv4 and IPv6 Configuration Type to None, MTU1420, MSS1420(OPNsense subtracts 40, so this clamps to 1380). Save and
apply, then restart WireGuard. - Create the gateway, named
WG_VPS_GW. System > Gateways > Configuration > Add:
- Interface:
WG_VPS, IP address:10.99.0.1, Far Gateway checked - Monitor IP:
10.99.0.1, or disable monitoring - Upstream Gateway unchecked, and a priority number higher than your WAN
gateway so it never becomes the default
- Port-forward. Firewall > NAT > Destination NAT (named Port Forward in
older releases) > Add:
- Interface:
WG_VPS, Protocol: TCP - Destination:
WG_VPS address, ports 80 and 443 - Redirect target:
192.168.1.10, same ports - Filter rule association: None. You create the pass rule yourself in the
next step so you can setreply-toon it.
- Create the pass rule with reply-to. Firewall > Rules >
WG_VPS> Add:
- Action: Pass, Protocol: TCP
- Destination:
192.168.1.10, ports 80 and 443 (NAT runs before filtering, so
match the inside address) - reply-to:
WG_VPS_GW(in the rule’s advanced or source-routing options,
depending on release) - The interface has no IP configuration, so OPNsense can’t infer
reply-tofrom
it. Setting it on the rule is what sends replies back up the tunnel.
- Check rule placement. Keep the pass rule on the
WG_VPStab, not on an
interface group such as WireGuard: group rules are evaluated before
interface rules and don’t carry thisreply-to.
Verify under VPN > WireGuard > Status (handshake) and Firewall > Diagnostics > States: the forwarded connection shows the visitor’s real IP.
DNS cutover, testing and troubleshooting
Cut DNS over only after the tunnel is tested against the VPS IP directly.
- Test before cutover: from outside your network, run
curl -vk --resolve yourdomain.com:443:VPS_IP https://yourdomain.com. The page should load and
your server’s access log should show the client’s real IP. - Switch the A records to
VPS_IP. Remove AAAA records unless you’ve also
built the IPv6 path. - Retest from a phone on cellular. The access log should show the carrier’s
IP, not10.99.0.1.
| Symptom | Likely cause | Fix |
|---|---|---|
| No handshake | VPS firewall or cloud firewall blocking UDP 51820, wrong key, or missing keepalive on the home side | Check wg show on both ends; open UDP 51820 in any provider firewall |
| Handshake OK, connections hang at SYN | Replies leaving via your ISP instead of the tunnel | Recheck the return path: VyOS policy route and table; pfSense upstream gateway on WG_VPS and no rule on the group tab; OPNsense reply-to set on the pass rule |
| Packets reach wg0 but vanish | Home peer Allowed IPs too narrow, or strict rp_filter / source validation | Allowed IPs 0.0.0.0/0; loose or disabled reverse-path filtering |
| Small pages load, large ones stall | MTU / MSS | Confirm MSS clamping on both ends; try MTU 1380 |
Logs show 10.99.0.1 |
An SNAT/masquerade rule on the VPS | Remove it (nft list ruleset on the VPS) |
| Works until the VPS reboots | Forwarding or WireGuard not enabled at boot | sysctl net.ipv4.ip_forward, systemctl is-enabled wg-quick@wg0 |
Useful captures: tcpdump -ni wg0 port 443 on the VPS, and monitor traffic interface wg0 filter "port 443" on VyOS. Replies should leave on wg0; replies
on your WAN interface mean the return path isn’t matching.
Leak checklist and security notes
The relay hides your IP only from now on; anything that already exposed it, or
still talks to the internet directly, can give it away.
- Historical DNS: passive DNS services have already recorded old A records.
That can’t be erased; only an IP change from your ISP fully retires it. - Other records: check every subdomain, MX and SPF record for anything still
pointing home. - Self-hosted mail: outbound mail sent from home carries your IP in its
headers. Relay it out through the VPS or a mail provider. - Leftovers: remove old router port forwards and dynamic DNS hostnames that
point at home. - Apps that phone home with your public IP (some game servers, federated
services) need their advertised address set to the VPS IP. - The VPS is your edge now: key-only SSH, default-deny input, unattended
security updates, and only the forwarded ports open. - Real IPs reach your servers: fail2ban, CrowdSec and rate limits now work
properly, but the servers are as exposed as if they sat on a public IP.
Firewall the tunnel interface to only the forwarded ports, as each router
section does. - TLS stays end to end: the VPS only forwards packets and never holds your
certificates, so the provider can’t read the traffic.