If you live in Costa Rica and run a home lab, you have probably hit the same wall I did: your ISP gives you a private IP behind Carrier-Grade NAT (CGNAT). No port forwarding, no inbound connections, no way to expose anything you host at home to the public internet. Calling support gets you nowhere — there is no static IP option on residential plans, and even business plans are inconsistent depending on the area.
This post walks through the setup I ended up with: an OPNsense VM in Miami acting as a WireGuard server with a public IPv4, and my home lab in Cartago dialing out to it as a WireGuard client. Inbound traffic hits Miami, gets forwarded over the tunnel, and lands on services running in my house — just as if I had a normal public IP.
What CGNAT actually breaks
Before fixing the problem, it helps to understand it.
A normal residential ISP gives you a public IPv4 address on your router’s WAN interface. You can port-forward tcp/443 to a server inside, and anyone on the internet can reach it.
With CGNAT, your router gets something like 100.64.x.x (the official CGNAT range, RFC 6598). The ISP then NATs many customers behind a smaller pool of real public IPs. You cannot port-forward anything because the public IP is not yours — it is shared, and the ISP controls the NAT table. Dynamic DNS does not help. UPnP does not help. The connection can only be opened from the inside out.
This is fine for browsing Reddit. It is not fine if you want to host a blog, run a Plex server reachable from outside, expose Home Assistant to your phone over Tailscale-without-Tailscale, or — like me — run a private blog like bizr.net from a VM at home.
The architecture
The fix is conceptually simple: rent a small VPS somewhere with a real public IP, and use it as a reverse proxy / tunnel endpoint. WireGuard is ideal because it is fast, stateless-feeling, and built into the Linux kernel.
Internet
│
│ inbound :443, :80, etc.
▼
┌────────────────────────────────┐
│ Miami VPS (public IPv4) │
│ OPNsense 25.x │
│ – WireGuard server :51820 │
│ – NAT port-forward rules │
└────────────┬───────────────────┘
│ WireGuard tunnel (UDP 51820)
│ 10.10.0.0/24
▼
┌────────────────────────────────┐
│ Home lab in Cartago, CR │
│ Behind ISP CGNAT │
│ WireGuard client on OPNsense │
│ or directly on the server │
└────────────────────────────────┘
Traffic to bizr.net resolves to the Miami public IP. Miami receives it on :443, a port-forward rule rewrites the destination to the home lab’s tunnel address (e.g. 10.10.0.2), and WireGuard pushes the packet down the encrypted tunnel to the house. The home server replies through the tunnel, Miami SNATs it back out, and the client never knows there was a hop.
Why OPNsense on both ends
You can do this with plain wg-quick on a Linux VPS and iptables rules. I used to. It works. But once you have more than one service, the iptables rules get ugly, and troubleshooting at 2am from your phone is painful.
OPNsense gives you a few things for free:
- a clean web UI for WireGuard peers, NAT rules, and firewall rules
- proper state tracking and live connection visibility
- aliases so you can group ports and IPs sanely
- backups you can restore in five minutes if the VPS dies
- the same mental model on both ends, since I run OPNsense at home too
The trade-off is RAM: OPNsense wants at least 2 GB. A 2 GB / 1 vCPU VPS in Miami runs about $6–$10/month depending on provider. I use Vultr because their Miami region has been stable for me and they support custom ISO uploads, which you need to install OPNsense.
Provisioning the Miami VPS
A few things matter when picking the VPS:
- Custom ISO support. OPNsense is not a one-click app on most providers. You will upload the ISO and install it yourself via the VNC console.
- A real public IPv4. Sounds obvious, but confirm it is not itself behind any provider NAT.
- Low latency to Costa Rica. Miami is the natural choice — most CR traffic already transits there. Expect 40–70 ms RTT.
- UDP not blocked. WireGuard is UDP. Some budget VPS providers shape or block non-TCP traffic. Vultr, Linode, Hetzner (their US locations), and DigitalOcean are all fine.
After installation, do the boring-but-critical steps first:
- set a strong root password and disable password SSH, key-only
- enable the OPNsense firewall on the WAN with the default “block all inbound” stance
- open only
udp/51820(WireGuard) andtcp/22from your admin IP for now - update to the latest OPNsense release before configuring anything else
Setting up the WireGuard server in OPNsense (Miami)
In OPNsense, WireGuard lives under VPN → WireGuard. The model has two parts: an Instance (the local server) and Peers (the clients).
1. Create the instance
Under VPN → WireGuard → Instances, add a new instance:
- Name:
wg-cgnat-bypass - Listen port:
51820 - Tunnel address:
10.10.0.1/24— this is the Miami side of the tunnel - MTU:
1420is the sane default for WireGuard over Ethernet; drop to1380if you see weird packet loss - Private key: click the cogwheel to auto-generate. Copy the public key — you will need it on the home side.
Save and enable.
2. Add the home lab as a peer
Under Peers, add a new peer:
- Name:
home-cartago - Public key: the public key you generate on the home side (we will get to that)
- Allowed IPs:
10.10.0.2/32— only let this peer use that single tunnel address - Endpoint: leave empty. Miami should never try to dial home, because home is behind CGNAT and cannot accept inbound packets. Home dials Miami.
- Keepalive: leave empty on the server side. The client will send keepalives.
Link the peer to the instance and save.
3. Assign the WireGuard interface
This is the step people miss and then spend three hours debugging.
Go to Interfaces → Assignments, find the wg0 (or whatever WireGuard created) device in the dropdown, click + to assign it. Open the new interface, enable it, leave the IPv4 configuration on “None” (the tunnel address is set on the instance, not here), and save + apply.
You now have a real interface OPNsense can write firewall and NAT rules against. Without this assignment, the tunnel will come up but you cannot route anything through it from the GUI.
4. Firewall rule on the WireGuard interface
Under Firewall → Rules → WireGuard (the new tab that appeared after assigning), add a rule:
- Action: Pass
- Protocol: any
- Source:
10.10.0.0/24 - Destination: any
This allows traffic from the tunnel into Miami. Without it, packets arrive but get dropped.
Setting up the home side
Two options here:
- run WireGuard directly on the server you want to expose (simplest, one service)
- run WireGuard on your home OPNsense and route inbound traffic to internal LAN hosts (cleaner, scales to many services)
I run it on home OPNsense. The setup mirrors Miami:
- Instance, tunnel address
10.10.0.2/24, no listen port required (it dials out) - Peer for Miami:
- Public key: the Miami instance public key
- Allowed IPs:
10.10.0.0/24— and add0.0.0.0/0only if you want to route all internet traffic out through Miami (I do not — adds latency and burns VPS bandwidth) - Endpoint:
your.miami.public.ip:51820 - Keepalive:
25seconds — this is the magic number. The home side must send something every ~30s or the ISP’s CGNAT mapping expires and inbound packets stop arriving
Generate the home keypair, paste the public key back into Miami’s peer config, save both sides.
Assign the WireGuard interface on home OPNsense too, enable it, add a firewall rule on the tunnel interface allowing 10.10.0.0/24 to your LAN destinations (or just any while testing).
Within ~30 seconds of enabling both sides, the handshake should complete. Check VPN → WireGuard → Status in Miami — you should see a recent handshake timestamp and bytes in/out incrementing.
If it does not handshake: 99% of the time it is a firewall rule, an MTU issue, or a public key copy-paste with a stray newline.
Forwarding inbound traffic to the home lab
Now the actual point of the exercise. Let’s say bizr.net resolves to the Miami public IP and runs on a Caddy reverse proxy on a home LAN server at 192.168.10.20:443.
On Miami OPNsense, go to Firewall → NAT → Port Forward and add:
- Interface: WAN
- Protocol: TCP
- Destination: WAN address
- Destination port range:
443 - Redirect target IP:
192.168.10.20(the LAN IP of the home server — Miami will route this through the tunnel because the home LAN is reachable via WireGuard) - Redirect target port:
443 - NAT reflection: disable
- Tick “Filter rule association: Add associated filter rule” so OPNsense auto-creates the pass rule on WAN.
Two more things make this actually work:
- On Miami, under System → Routes → Configuration, make sure there is a route for your home LAN (e.g.
192.168.10.0/24) via the WireGuard interface’s tunnel gateway. OPNsense usually figures this out from the peer’s Allowed IPs, but verify. - On the home OPNsense, under Peer → Allowed IPs for the Miami peer, include
10.10.0.0/24so return traffic knows to go back through the tunnel. If you want Miami to be able to reach your whole home LAN, you would also add192.168.10.0/24to the Miami peer’s Allowed IPs on the Miami side — but that gives Miami access to your LAN, so think about whether you want that.
Repeat the port-forward block for any other service. I run one for :80 (redirect to https), one for :443, and a non-standard port for SSH that I rate-limit aggressively.
Performance, latency, and what to expect
A few numbers from my setup:
- Latency overhead: Cartago → Miami direct is ~55 ms. Through the tunnel it is ~60 ms. WireGuard’s overhead is genuinely tiny.
- Throughput: I get about 180–220 Mbps through the tunnel on a 300/300 home line. The bottleneck is OPNsense CPU on a 1 vCPU VPS doing the crypto. If you need more, give the VPS 2 vCPUs and you will saturate a gigabit.
- TTFB to bizr.net from outside CR: ~80–120 ms. Perfectly fine for a blog. For latency-sensitive things (gaming, voice), think harder about the geography.
Things I wish I had known sooner
- The keepalive on the client side is non-negotiable. Without it, ICE will drop the CGNAT mapping after a few minutes of idle and the tunnel goes one-way-dead.
- Do not enable NAT reflection on the Miami WAN. It causes weird loops when you try to reach
bizr.netfrom inside the tunnel. - Back up your OPNsense config weekly (System → Configuration → Backups). The whole reason this is resilient is that the VPS is replaceable in 20 minutes if you have a recent config.
- Use a DNS provider with low TTL on the A record pointing to Miami. If you ever need to move VPS providers, you want propagation in minutes, not hours.
- Monitor the handshake. I have a small script that hits the WireGuard status API every 5 minutes and pages me if the last handshake is older than 3 minutes. Saved me twice.
Why not Cloudflare Tunnel or Tailscale Funnel?
Both are great, and for a lot of people they are the right answer. I picked the OPNsense + WireGuard route because:
- I want the public IP to be mine, not a CDN’s. Some services I run do not like being behind Cloudflare.
- I want raw TCP/UDP on arbitrary ports, not just HTTP(S).
- I already run OPNsense and enjoy understanding the whole stack end to end. This is a homelab blog. The point is partly to learn.
If you just want to expose a single web service and never think about it again, Cloudflare Tunnel is one cloudflared install away and is genuinely excellent. Pick the boring solution when it fits.
Wrapping up
CGNAT used to feel like a hard limit on what I could do from my home lab in Costa Rica. It is not — it is a $7/month and one weekend problem. The setup above has been running for a while now with effectively zero unplanned downtime, and it is the foundation for everything else I host, including this blog.
Next post I will write about the reverse-proxy layer that sits behind this (HA-Proxy with automatic ACME, internal-only services on a separate VLAN, and how I think about exposing things safely). If you have questions or are stuck on a specific part of the setup, the contact form on bizr.net reaches me directly.