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)
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: truein/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
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
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
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
AllowedIPson the clients tight to avoid sending all traffic through the VPS.