How-To: Self Hosting by Hiding Your Home IP with a VPS + WireGuard Relay (Keeping Real Client IPs)

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:

  1. A visitor resolves your domain and gets the VPS public IP.
  2. The VPS DNATs ports 80/443 into the tunnel. It does not SNAT, so the visitor’s real source IP is kept.
  3. Your home router receives the packet on its WireGuard interface and port-forwards it to the server.
  4. The server replies to the visitor’s IP. The home router sends that reply back up the tunnel, not out your ISP connection.
  5. 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.

  1. 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
  1. 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
  1. 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
  1. Replace /etc/nftables.conf with 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
  }
}
  1. 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.
  • iifname and oifname match 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 ruleset makes 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.

  1. Generate keys (operational mode). Put the public key in the VPS wg0.conf and run systemctl restart wg-quick@wg0 on the VPS.
generate pki wireguard key-pair
  1. 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.

  1. 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
  1. 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.

  1. 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.

  1. 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).

  1. Install WireGuard under System > Package Manager if it isn’t already.
  2. 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.
  3. Create the tunnel. VPN > WireGuard > Tunnels > Add: enable it, generate
    keys, and copy the public key into the VPS wg0.conf. Leave the tunnel
    address blank; it is set on the interface in step 5.
  4. Add the peer. VPN > WireGuard > Peers > Add:
  • Tunnel: the one from step 3
  • Endpoint: VPS_IP, port 51820
  • 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.
  1. 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, IP 10.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 rules reply-to. The default route stays on WAN because of step 2.
  • MTU 1420, MSS 1420. pfSense subtracts the 40-byte TCP/IPv4 header from the
    MSS field, so this clamps to 1380.
  1. Set gateway monitoring. System > Routing > Gateways, edit WG_VPS_GW:
    monitor 10.99.0.1 or disable monitoring.
  2. 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
  1. Check rule placement. The pass rules must sit on the WG_VPS tab.
    Remove or disable any matching pass rule on the WireGuard group tab:
    group rules are matched first and don’t get reply-to, so replies would
    leave via WAN.
  2. 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)
.

  1. 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
  1. 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/0 installs a
    default route into the tunnel.
  • Gateway: 10.99.0.1
  • Save, then copy the generated public key into the VPS wg0.conf.
  1. Enable WireGuard under VPN > WireGuard > General.
  2. Assign the interface. Interfaces > Assignments: add wg0 as WG_VPS.
    Open it, enable it, set IPv4 and IPv6 Configuration Type to None, MTU 1420, MSS 1420 (OPNsense subtracts 40, so this clamps to 1380). Save and
    apply, then restart WireGuard.
  3. 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
  1. 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 set reply-to on it.
  1. 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-to from
    it. Setting it on the rule is what sends replies back up the tunnel.
  1. Check rule placement. Keep the pass rule on the WG_VPS tab, not on an
    interface group such as WireGuard: group rules are evaluated before
    interface rules and don’t carry this reply-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.

  1. 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.
  2. Switch the A records to VPS_IP. Remove AAAA records unless you’ve also
    built the IPv6 path.
  3. Retest from a phone on cellular. The access log should show the carrier’s
    IP, not 10.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.

Sources

4 Likes

I like that you have a VYOS configuration option in there.

1 Like

Looks like your previous post complaining about AI posts was meant to be nuanced. But I liked the write up either way. It’s fun to chew on a slightly new ideas. Like the previous AI post, I haven’t thought about this stuff in a while.

It seems you really want to hide your server’s IP for DNS lookups. From my casual observation, this is the primary reward. Since you are not masquerading your post route traffic on the VPS, traceroute should still find your servers IP. Not that it matters. If somebody wanted to be nefarious, having your physical server’s IP isn’t that big of deal when you port forward them right to it. You could add rate limit rules to your vps nftables config to stop some ddos attacks on your home WAN & hardware.

At the end of the day this is basic port forwarding using a vps (with a wg link for fun). Alternatively, you could just run this in a container on the home router and save money. Yeah, a ddos would saturate my local internet link instead of the vps, but to be fair they could probably saturate the vps link too. And since your http traffic routes through the home link, if the vps link is fatter than the home link, you’ll still saturate the home link first. Not to mention, your hosting hardware might buckle before the link is saturated. So just avoid being ddos’ed. :slight_smile:

On another side note, I looked at the vyos config just for fun and it brought back very old memories. Why network guys like those overly verbose cisco commands is beyond me now. How that is better than a clean readable nftables or systemd-networkd config file? Even the pfsense steps are more cumbersome to read and edit. If you have a good argument let me know. But I’d take plan debian over vyos all the time.

All said, your setup looks cool and I enjoyed the post.

You assume I used AI because this how-to is lengthy. Actually it’s because this isn’t a straight forward flip a few switches and you are done. I wanted to be thorough as possible and help users understand what is going on. Ever see Wendell from level 1 techs how-to’s?

Why would I make a post about AI slop in forums and turn around and make an AI generated post?

2 Likes

I get why you used AI for that post. If you had burned the mental energy and time to type that all out yourself, that would have been… sad. You are against AI slop posting, not AI posting in general. I get it. And you enjoy being the AI slop cop on this forum. If that keeps you engaged, great.

Looking back on this posts, I realized that I probably missed your whole point of doing this. You must be doing this for cgnat situations (dynamic DNS could easily be solved with a simple script). That wasn’t explicitly stated in your post, but that make all this effort make a lot more sense. I forget about the poor bastards behind cgnat. If I were stuck in that situation, I’d do pretty much exactly what you did here.