Thank you for visiting!
My little window on internet allowing me to share several of my passions
Categories:
- OpenBSD
- Tunnel
- FreeBSD
- fapws
- Nvim
- Firewall
- got
- PEKwm
- Zsh
- VM
- High Availability
- vdcron
- My Sysupgrade
- Nas
- VPN
- DragonflyBSD
- Alpine Linux
- Openbox
- Desktop
- Security
- yabitrot
- nmctl
- Tint2
- Project Management
- Hifi
- Alarm
Most Popular Articles:
Last Articles:
Thanks to OpenBSD and Wireguard it's easy to setup a permanent connection with a remote machine behind a NAT
Posted on 2026-08-15 19:49:00 from Vincent in OpenBSD Tunnel

Increasingly, new ISP boxes bundling fiber, TV, and all the bells and whistles no longer allow inbound connections from the outside. Unfortunately, that's exactly what I need — I want to reach my home server from anywhere. Thanks to WireGuard as set up on OpenBSD, there's a very simple solution to this. This post explains how to do it.
Punching a WireGuard tunnel through NAT on OpenBSD
I recently needed a stable, always-on link between a machine sitting behind a NAT (no port forwarding, no static IP) and one of my servers with a public address. WireGuard is the obvious tool for this, and OpenBSD ships a native, in-kernel implementation (wg(4)), configured the OpenBSD way through hostname.if(5). No third-party daemon, no extra moving parts. Here's how I set it up, mostly as a note to my future self.
The topology
- server: public IP, reachable, will listen on UDP/49348
- client: behind a NAT, no inbound connectivity, will initiate the tunnel
Tunnel addressing:
- server: 10.10.0.1/24
- client: 10.10.0.2/24
Because the client is the one behind NAT, it has to be the one that opens the connection. The server just needs to be listening and know which peer (by public key) is allowed to talk to it.
Generating the keys
OpenBSD's base system doesn't ship a wg genkey equivalent, so the simplest route is to pull in wireguard-tools:
pkg_add wireguard-tools
Then, on each machine, generate a private/public keypair:
wg genkey | tee private.key | wg pubkey > public.key
private.keystays on that machine and only that machine. Never let it leave.public.keyis what you hand to the other side.
So at the end of this step you have four values in hand: the server's private and public key, and the client's private and public key. Each machine only ever needs its own private key plus the other side's public key.
Server configuration — /etc/hostname.wg0
inet 10.10.0.1 255.255.255.0 NONE
wgkey <server's private key>
wgport 49348
wgpeer <client's public key> wgaip 10.10.0.2/32
A few notes:
- wgport pins the UDP port the server listens on. Since the server is the reachable side, this is the port you'll open in pf.conf and, if applicable, on any upstream firewall/router.
- wgaip (allowed IPs) is scoped to a single /32 here, because we only expect this one peer to show up as 10.10.0.2. This also doubles as a basic routing/ACL rule: only traffic for that address, coming from that peer's key, is accepted.
- No wgendpoint on the server side — it doesn't need one, since it's not the one initiating the handshake. It just waits for the client to show up.
Client configuration — /etc/hostname.wg0
inet 10.10.0.2 255.255.255.0 NONE
wgkey <client's private key>
wgpeer <server's public key> wgendpoint <server's public IP> 49348 wgaip 10.10.0.0/24 wgpka 25
Here the roles are reversed:
- wgendpoint tells the client where to dial: the server's public IP and port.
- wgaip 10.10.0.0/24 is a bit wider on this side — it says "route the whole tunnel subnet through this peer," which makes sense since, from the client's point of view, the server is its only gateway into that network.
- wgpka 25 is the important bit for the NAT scenario, and it's the "refresh" setting I was trying to remember: persistent keepalive. Since the client sits behind NAT, the NAT device maintains a translation/mapping for the UDP session, and that mapping has a timeout. Without traffic, it silently expires, the server can no longer reach the client, and the tunnel goes dark until the client happens to send something again. wgpka 25 makes the client emit a keepalive packet every 25 seconds, which is comfortably under most NAT/firewall UDP timeouts (commonly 30–120s) and keeps the mapping — and the tunnel — alive indefinitely. This option only makes sense on the side that's behind NAT; the server doesn't need it.
Both hostname.wg0 files should be chmod 600, root-owned, since the private key sits in there in plaintext.
Firewall — pf.conf
On the server side:
wg_if="wg0"
# for wg
pass in on egress proto udp to port 49348
pass in on $wg_if
pass out on $wg_if
- The first rule opens the actual WireGuard handshake/data port on the public-facing interface (
egress), so the client can reach the server at all. - The other two simply let traffic flow freely once it's on the tunnel interface itself — WireGuard already authenticates and encrypts everything crossing
wg0, so there's no real need to filter again at that layer unless you want tighter internal segmentation.
The client-side pf.conf doesn't need an inbound rule for port 49348 at all, since it's behind NAT and initiates the connection outbound — that's the whole point of this setup.
Bringing it up
On both machines:
sh /etc/netstart wg0
or a reboot, if you prefer to test the full boot path.
Verifying it works
Check the interface came up and the handshake happened:
ifconfig wg0
You should see your tunnel address, and — once a handshake has occurred — a wgpubkey line along with peer/handshake details.
From the client, ping the server across the tunnel:
ping 10.10.0.1
And the reverse, from the server:
ping 10.10.0.2
If you have wireguard-tools installed, wg show gives a more detailed view than ifconfig, including latest handshake time and data transferred:
wg show wg0
A handshake timestamp that keeps refreshing every ~25 seconds (or shortly after) confirms wgpka is doing its job and the NAT mapping is being kept alive.
Conclusion
Thanks to OpenBSD, this is it — a minimal, kernel-native WireGuard tunnel between a NATed box and a public server, no extra daemons involved.