WireGuard on FreeBSD: A 30-Minute Setup

Published on

FreeBSD WireGuard VPN Networking

Why WireGuard

WireGuard fits in your head. The protocol is small enough to read in an evening, the userland config file is half a screen, and the Linux/FreeBSD kernel modules are well-audited. Compared to OpenVPN — TLS, certificates, MTU bargaining, weeks of "why does my phone disconnect every 30 seconds" — WireGuard is a relief.

On FreeBSD, the kernel module is in base since 13.0 (if_wg(4)) and the userland tools live in net/wireguard-tools. Setup takes about as long as reading this post.

The Mental Model

WireGuard is point-to-point at the protocol level: every endpoint is a "peer", identified by a public key. There's no client/server distinction in the protocol — the labels are about who initiates and who has a static IP. A "VPN server" is just a peer with a public WAN address that other peers connect to.

Each peer has:

Install

$ pkg install wireguard-tools
$ kldload if_wg                 # load now
$ sysrc kld_list+=if_wg         # load on boot

Confirm the kernel module is up:

$ kldstat | grep wg
 12    1 0xffffffff82800000   12340  if_wg.ko

Generate Keys for the Router

$ umask 077
$ mkdir -p /usr/local/etc/wireguard
$ cd /usr/local/etc/wireguard
$ wg genkey | tee privatekey | wg pubkey > publickey
$ cat publickey
3v9ZZ...= 

That public key is what every peer will need. The private key never leaves the router.

Server Config

# /usr/local/etc/wireguard/wg0.conf

[Interface]
PrivateKey = <contents of privatekey>
ListenPort = 51820
Address    = 10.66.66.1/24

# Each [Peer] block is one client
[Peer]
# laptop
PublicKey  = <laptop public key>
AllowedIPs = 10.66.66.10/32

[Peer]
# phone
PublicKey  = <phone public key>
AllowedIPs = 10.66.66.11/32

On FreeBSD, AllowedIPs on the server side is also a routing table entry. Anything inside 10.66.66.10/32 is routed to the laptop peer. Don't make the AllowedIPs overlap between peers; the kernel will complain and you'll lose your afternoon.

Bring It Up

# /etc/rc.conf
wireguard_enable="YES"
wireguard_interfaces="wg0"
$ service wireguard start
$ wg show wg0
interface: wg0
  public key: 3v9ZZ...
  private key: (hidden)
  listening port: 51820

peer: laptop-pubkey-here
  allowed ips: 10.66.66.10/32

peer: phone-pubkey-here
  allowed ips: 10.66.66.11/32

pf Rules: Listener and Forwarding

# Allow the WireGuard listener on the WAN
pass in on $ext_if proto udp from any to ($ext_if) port 51820 keep state

# Allow VPN clients into the LAN, NAT'd as the router
pass in  on wg0     from 10.66.66.0/24 to any keep state
pass out on $ext_if from 10.66.66.0/24 to any keep state

# If you want VPN clients to reach the trusted LAN unmodified
nat on $ext_if from 10.66.66.0/24 to any -> ($ext_if)

The main pf ruleset already handles outbound NAT for RFC1918 — if you tagged the VPN subnet into $rfc1918 or your nat source set, you don't need a separate rule.

Client Configs

Generate the client's keys on the client (never on the server):

$ wg genkey | tee laptop.privatekey | wg pubkey > laptop.publickey

Then build a config the client can import:

# laptop.conf
[Interface]
PrivateKey = <laptop privatekey>
Address    = 10.66.66.10/32
DNS        = 10.10.10.1

[Peer]
PublicKey         = <router public key>
Endpoint          = vpn.example.com:51820
AllowedIPs        = 0.0.0.0/0, ::/0
PersistentKeepalive = 25

On the client side, AllowedIPs means "send traffic for these destinations into the tunnel". 0.0.0.0/0, ::/0 means "everything" — full tunnel. If you want split tunnel, list only the LAN ranges you want to reach.

Phone Setup

The official WireGuard apps for iOS and Android both read QR codes. Generate one from the client config:

$ pkg install qrencode
$ qrencode -t ansiutf8 < phone.conf

Hold the phone up, scan, accept. The phone is on the VPN. Total elapsed time: about 30 seconds.

DNS Through the Tunnel

If you set DNS = 10.10.10.1 in the client config (your router's unbound), every DNS query from the client goes through the tunnel. This is usually what you want — it means the client's queries aren't visible to the coffee shop wifi, and clients can resolve your internal hostnames.

Don't forget to allow that traffic on the router-side pf:

pass in on wg0 proto { tcp udp } from 10.66.66.0/24 to (self) port domain keep state

Persistent Keepalive: When You Need It

If a client is behind NAT (most phones, most laptops on hotel wifi), the upstream NAT mapping for the WireGuard UDP flow will eventually time out, and the next inbound packet from the server will get dropped. Setting PersistentKeepalive = 25 on the client tells it to send a heartbeat every 25 seconds, which keeps the NAT mapping alive.

Don't set keepalive on the server side. Servers with public IPs don't need it, and setting it just burns battery on idle connections.

Troubleshooting

Operational Habits

Going Further

Got a different topology — site-to-site, mesh, hub-and-spoke? Tell me about it. I'm always curious how other people lay out their VPN graphs.

$ subscribe --to newsletter

FreeBSD, pf, and SRE notes — straight to your inbox. No spam, just signal.

Powered by Buttondown. Unsubscribe anytime. Or grab the RSS feed.

Related Posts

← Back to Blog