WireGuard: VPS ↔ Homeserver

Why I built this

I wanted access to my home network from the outside without exposing every service. My ISP uses CGNAT, so inbound IPv4 is basically a no-go. The workaround: a small VPS as a public hub, WireGuard as the tunnel, and my homeserver as the gateway.

I started with an IPv6-only WireGuard setup on the Fritz!Box. It worked at home, but failed in networks without IPv6 (e.g. my university Wi-Fi). That limitation is why I moved to a VPS endpoint with stable IPv4. It also unlocks services that Cloudflared will not proxy (like Minecraft or other UDP-heavy traffic).

  • Private admin access: No open ports at home.
  • Selective exposure: Forward only the ports I actually need.
  • Consistent access: Works even on IPv4-only networks.
Two birds, one tunnel

One tunnel, two jobs: remote access into the LAN and targeted port forwarding for specific services like game servers.


Architecture overview

Hub-and-spoke: the VPS has public IPv4/IPv6, the homeserver connects outward and routes into the LAN. Clients always go through the VPS.

Client (Phone/Mac)
        |  WireGuard
        v
     VPS (Public IP)
        |  WireGuard + Routing
        v
 Homeserver (10.10.10.2)
        |  NAT into LAN
        v
  LAN (192.168.178.0/24)

Result: predictable routing and a single place to debug.


VPS prep (Oracle Cloud)

To make this work, I had to prep a few things first, including the WireGuard port and any ports I want to forward.

  • Enable IPv6: VCN /56, Subnet /64, route ::/0 to the Internet Gateway.
  • Firewall: Allow UDP 51820 for 0.0.0.0/0 and ::/0.
  • Port forwards: Open only the ports you actually forward (e.g. 25565, 24454).
  • Netplan: Set dhcp6: true in /etc/netplan/50-cloud-init.yaml.
If the handshake never shows up

Oracle + VirtIO can drop UDP due to checksum offloading. Fix it with: sudo ethtool -K enp0s6 rx-checksumming off (best via boot script).

WireGuard on the VPS

File: /etc/wireguard/wg0.conf. Keys are placeholders.

[Interface]
Address = 10.10.10.1/24
ListenPort = 51820
MTU = 1360
PrivateKey = <VPS_PRIVATE_KEY>

# 1) Open WireGuard port (force rule to the top)
PostUp = iptables -I INPUT 1 -p udp --dport 51820 -j ACCEPT
PostDown = iptables -D INPUT -p udp --dport 51820 -j ACCEPT

# 2) Client-to-client traffic
PostUp = iptables -I FORWARD 1 -i wg0 -o wg0 -j ACCEPT
PostDown = iptables -D FORWARD -i wg0 -o wg0 -j ACCEPT

# 3) General forwarding
PostUp = iptables -I FORWARD 1 -i wg0 -j ACCEPT
PostDown = iptables -D FORWARD -i wg0 -j ACCEPT

# 4) NAT for VPN traffic
PostUp = iptables -t nat -A POSTROUTING -o wg0 -j MASQUERADE
PostDown = iptables -t nat -D POSTROUTING -o wg0 -j MASQUERADE

[Peer]
# Homeserver as LAN gateway
PublicKey = <HOMESERVER_PUBLIC_KEY>
AllowedIPs = 10.10.10.2/32, 192.168.178.0/24

[Peer]
# Admin client (phone)
PublicKey = <PHONE_PUBLIC_KEY>
AllowedIPs = 10.10.10.4/32

[Peer]
# Admin client (MacBook)
PublicKey = <MACBOOK_PUBLIC_KEY>
AllowedIPs = 10.10.10.5/32

Also set net.ipv4.ip_forward=1 in /etc/sysctl.conf.


Homeserver as gateway

The homeserver routes VPN traffic into the LAN. That needs NAT on the LAN interface.

[Interface]
Address = 10.10.10.2/24
PrivateKey = <HOMESERVER_PRIVATE_KEY>

# NAT: mask VPN traffic into the LAN
PostUp = iptables -t nat -A POSTROUTING -o <LAN_IF> -j MASQUERADE
PostDown = iptables -t nat -D POSTROUTING -o <LAN_IF> -j MASQUERADE

[Peer]
PublicKey = <VPS_PUBLIC_KEY>
Endpoint = <VPS_PUBLIC_IP>:51820
PersistentKeepalive = 25
AllowedIPs = 10.10.10.0/24

<LAN_IF> is enp86s0 for me. Without NAT, LAN replies never return.


Client config (split tunnel)

Goal: only home network traffic goes through the VPN, everything else stays local.

I keep AllowedIPs tight so I can still run a second VPN on top and avoid pushing all my traffic through the VPS.

[Interface]
PrivateKey = <CLIENT_PRIVATE_KEY>
Address = 10.10.10.5/24
DNS = 9.9.9.9

[Peer]
PublicKey = <VPS_PUBLIC_KEY>
Endpoint = <VPS_PUBLIC_IP>:51820
AllowedIPs = 10.10.10.0/24, 192.168.178.0/24
PersistentKeepalive = 25
Need a full tunnel?

For full tunnel in public Wi-Fi, use AllowedIPs = 0.0.0.0/0, ::/0. I keep this optional to avoid turning the VPS into a bottleneck.


Port forwarding examples

The VPS can forward ports directly to the homeserver. Example for Minecraft + voice chat:

# Minecraft (TCP 25565)
PostUp = iptables -t nat -A PREROUTING -p tcp --dport 25565 -j DNAT --to-destination 10.10.10.2:25565
PostDown = iptables -t nat -D PREROUTING -p tcp --dport 25565 -j DNAT --to-destination 10.10.10.2:25565
PostUp = iptables -I FORWARD 1 -i enp0s6 -o wg0 -p tcp --dport 25565 -j ACCEPT
PostDown = iptables -D FORWARD -i enp0s6 -o wg0 -p tcp --dport 25565 -j ACCEPT

# Simple Voice Chat (UDP 24454)
PostUp = iptables -t nat -A PREROUTING -p udp --dport 24454 -j DNAT --to-destination 10.10.10.2:24454
PostDown = iptables -t nat -D PREROUTING -p udp --dport 24454 -j DNAT --to-destination 10.10.10.2:24454
PostUp = iptables -I FORWARD 1 -i enp0s6 -o wg0 -p udp --dport 24454 -j ACCEPT
PostDown = iptables -D FORWARD -i enp0s6 -o wg0 -p udp --dport 24454 -j ACCEPT

Ports and services are examples — the pattern stays the same: DNAT + FORWARD.


Troubleshooting

Symptom Cause Fix
No handshake UDP 51820 blocked upstream Open UDP 51820 in OCI (IPv4 + IPv6) and confirm the VPS firewall allows it.
Handshake ok, no ping iptables order or missing forward rules Insert rules at the top and allow wg0 forwarding so replies can flow back.
No LAN access IP forwarding or NAT missing Enable IP forwarding and MASQUERADE on the homeserver LAN interface.

Lessons learned

  • IPv6 solves CGNAT, but not every network speaks it yet.
  • iptables rule order matters more than I expected.
  • Keep AllowedIPs on the clients tight to avoid sending all traffic through the VPS.