Reaching my home VPN by hostname: UniFi + Cloudflare DDNS
/ 8 min read
Table of Contents
If you run a WireGuard server at home behind a consumer ISP, there’s a small landmine waiting for you: your public WAN IP is almost certainly dynamic. It works fine for weeks, then one night your ISP rotates the address, and suddenly none of your phones can dial home. The configs still look correct — they just point at an IP that no longer belongs to you.
This post is how I fixed that on a UniFi UDM-Pro-Max: stop pinning an IP, connect by a hostname instead, and keep that hostname pointed at the current WAN IP automatically with Cloudflare’s Dynamic DNS. I’m using my own domain (huislab.app) throughout — swap in yours.
This is part 1, covering the DDNS + WireGuard setup. There’s a sneaky DNS gotcha that bit me right after this worked — I’ll cover that in part 2.
The problem, precisely
A WireGuard client config has an Endpoint line, e.g.:
[Peer]Endpoint = 203.0.113.10:51820That IP gets baked in when the config is generated. The tunnel is happy as long as 203.0.113.10 is still your WAN IP. The moment your ISP changes it, the client keeps firing handshakes at an address that’s now someone else’s, and the connection silently dies. You only find out when you’re away from home and need it.
flowchart TB subgraph isp["🌐 ISP rotates your WAN IP overnight"] direction TB OLD["was 203.0.113.10"] NEW["now 198.51.100.7"] OLD -.->|"DHCP lease renewed"| NEW end subgraph before["❌ Endpoint pinned to an IP"] direction TB C1["📱 WireGuard client<br/><span style='font-size:0.8em'>Endpoint = 203.0.113.10:51820</span>"] DEAD["💀 someone else's address now<br/><span style='font-size:0.8em'>silent handshake timeout</span>"] C1 -->|"handshake to the old IP"| DEAD end subgraph after["✅ Endpoint is a hostname"] direction TB C2["📱 WireGuard client<br/><span style='font-size:0.8em'>Endpoint = vpn.huislab.app:51820</span>"] GOOD["🏠 your gateway<br/><span style='font-size:0.8em'>DNS already points at 198.51.100.7</span>"] C2 -->|"resolve, then handshake"| GOOD end NEW -.->|"breaks the pinned config"| before NEW ==>|"hostname follows automatically"| after classDef bad stroke:#e06c75,stroke-width:2px,stroke-dasharray:6 4,fill:transparent; classDef good stroke:#98c379,stroke-width:2px,fill:transparent; class before bad; class after good;
The fix is conceptually simple: put a hostname in the Endpoint instead of an IP, and make something keep that hostname’s DNS record in sync with your real WAN IP. Two pieces:
- Dynamic DNS (DDNS) — a small updater that watches your WAN IP and pushes it into a DNS
Arecord whenever it changes. - UniFi’s “Alternate Address” — the setting that tells UniFi to write the hostname (not the IP) into every client config it generates.
Why Cloudflare
I already run huislab.app on Cloudflare, so using it as the DDNS provider keeps everything in one place. The bonus: recent UniFi Network releases (9.1.92 and newer) ship native Cloudflare DDNS support, so you no longer need the old Cloudflare Worker shim that homelabbers passed around for years. If your gateway’s Dynamic DNS dialog lists Cloudflare as a provider and asks only for Hostname / Zone / API Token, you’re on the native integration.
One caveat worth knowing up front: the native updater handles a single-hostname IPv4 record and won’t preserve a record’s proxy status. That matters, and it’s the reason for the very first step below.
Here’s the whole thing, end to end — the DDNS updater keeps the record fresh, and the client uses that record to find home:
flowchart TB subgraph cf["☁️ Cloudflare DNS · zone huislab.app"] REC["A record · vpn<br/><span style='font-size:0.8em'>grey cloud / DNS only</span><br/><span style='font-size:0.8em'>→ current WAN IP</span>"] end subgraph home["🏠 Home · behind a dynamic WAN IP"] direction TB subgraph udm["UniFi UDM-Pro-Max"] direction TB IND["🔄 inadyn · Dynamic DNS<br/><span style='font-size:0.8em'>Zone:DNS:Edit token</span>"] WG["🔒 WireGuard server<br/><span style='font-size:0.8em'>UDP :51820</span>"] end end subgraph out["📱 WireGuard client · on the road"] CL["Endpoint = vpn.huislab.app:51820"] end IND ==>|"1 · pushes WAN IP whenever it changes"| REC CL -->|"2 · resolve vpn.huislab.app"| REC REC -->|"3 · returns the real WAN IP"| CL CL ==>|"4 · WireGuard UDP to that IP"| WG classDef dnsonly stroke:#98c379,stroke-width:2px,fill:transparent; classDef vm fill:transparent,stroke:#5c6370,stroke-width:1px; class REC dnsonly; class udm,home,out vm;
Two paths to trace there: flow 1 is the write side — inadyn on the gateway updates the A record. Flows 2–4 are the read side — the client looks up the hostname, gets your current WAN IP back, and sends WireGuard’s UDP straight at the gateway. Steps 1–6 below build exactly this.
Step 1 — Create the DNS record (DNS only!)
In the Cloudflare dashboard, open your zone → DNS → Records → Add record:
- Type:
A - Name:
vpn(this becomesvpn.huislab.app) - IPv4 address: any placeholder, e.g.
203.0.113.10— the updater overwrites it - Proxy status: DNS only (grey cloud) — not Proxied (orange cloud)
- TTL: Auto
The grey cloud is the single most important detail in this whole setup. WireGuard runs over UDP, and Cloudflare’s orange-cloud proxy only handles HTTP/HTTPS. If you leave the record proxied, vpn.huislab.app resolves to a Cloudflare anycast address (a 104.x or 172.67.x IP), your WireGuard packets hit a proxy that has no idea what to do with them, and the client hangs forever on “connecting” with no handshake. Grey cloud = your real IP is published = UDP reaches your gateway.
flowchart TB Q["📱 client resolves vpn.huislab.app"] subgraph orange["🟠 Proxied (orange cloud) — broken for VPN"] direction TB O1["resolves to Cloudflare anycast<br/><span style='font-size:0.8em'>104.x / 172.67.x</span>"] O2["Cloudflare proxy · HTTP/HTTPS only"] O3["💀 UDP WireGuard packets dropped<br/><span style='font-size:0.8em'>stuck on 'connecting', no handshake</span>"] O1 --> O2 --> O3 end subgraph grey["⚪ DNS only (grey cloud) — correct"] direction TB G1["resolves to your real WAN IP"] G2["🏠 packets reach your gateway"] G3["✅ handshake completes"] G1 --> G2 --> G3 end Q -.->|"orange cloud"| orange Q ==>|"grey cloud"| grey classDef bad stroke:#e06c75,stroke-width:2px,stroke-dasharray:6 4,fill:transparent; classDef good stroke:#98c379,stroke-width:2px,fill:transparent; class orange bad; class grey good;
Step 2 — Create a scoped API token
Go to dash.cloudflare.com/profile/api-tokens → Create Token and use the Edit zone DNS template:
- Permissions: Zone → DNS → Edit
- Zone Resources: Include → Specific zone →
huislab.app
Scope it to the one zone — don’t leave it on “All zones”. Create the token and copy it immediately; Cloudflare only shows it once.
Step 3 — Configure DDNS in UniFi
In the UniFi Network app: Settings → Internet → WAN → Dynamic DNS → Create New:
- Service: Cloudflare
- Hostname:
vpn.huislab.app - Zone Name:
huislab.app - API Token: the token from step 2
Save it. Under the hood UniFi uses inadyn to push updates.
Step 4 — Verify the record actually updates
Give it a minute, then check that the placeholder IP has been replaced with your real WAN IP. The clean way to test is to query a public resolver so you see what the outside world gets:
dig @1.1.1.1 vpn.huislab.app +shortYou want this to return your actual WAN IP. If it still shows the placeholder, the usual suspects are a mistyped Zone Name or a token that wasn’t scoped to the right zone. You can also SSH into the gateway and check the updater logs in /var/log/messages (grep for inadyn).
CGNAT check: DDNS only helps if your gateway holds a genuinely public IP. If your WAN IP sits in the
100.64.0.0/10range, you’re behind carrier-grade NAT and the record will update but the hostname won’t be reachable from outside — that’s a separate problem (IPv6, a relay, or a chat with your ISP). Most fixed-line connections are fine.
Step 5 — Point WireGuard at the hostname (the Alternate Address)
This is the piece that makes clients use the hostname. Open your WireGuard server: Settings → VPN → [your WireGuard server]. In the Server Address section you’ll likely see a warning that your WAN IP is dynamic and you should enable DDNS and configure the alternate address — that’s exactly what we’re doing.
Tick Use Alternate Address for Clients and enter:
vpn.huislab.appSave. From now on, every client config UniFi generates will carry Endpoint = vpn.huislab.app:51820 instead of a literal IP.
That dynamic-IP warning banner does not disappear after you’ve done this. It’s an informational notice tied to having a dynamic WAN, not a checklist item — it stays put even when everything’s configured correctly. Ignore it; the working setup is what matters, not the banner.
Step 6 — Re-export your existing clients
Here’s the gotcha that catches people: already-generated client configs keep the old endpoint. Setting the Alternate Address only affects configs created after the change. Existing peers still have the old IP (or old hostname) baked in until you regenerate and re-import them.
So for each device — re-export the config from UniFi and re-import it on the client (delete the old tunnel first). The good news is there’s no rush: as long as your WAN IP hasn’t changed yet, the old configs keep working. The hard deadline is “before the next IP rotation.” Do it at your leisure, but do it.
Where this leaves you
At this point:
vpn.huislab.appalways resolves to your current WAN IP, updated automatically by Cloudflare DDNS.- Every new WireGuard client connects by that hostname, so an ISP IP change no longer breaks remote access.
- The whole thing needs zero maintenance from the public internet’s point of view.
I tested it by tethering a travel router to a phone hotspot (genuinely “outside” the network) and watching the handshake land.
…and then I tried it from a device inside my house and it didn’t resolve, which sent me down a rabbit hole involving split-horizon DNS, a wildcard rewrite quietly hijacking the hostname, and NAT hairpin. That’s part 2.
TL;DR
- Don’t pin IPs in VPN configs behind a dynamic WAN — use a hostname.
- Cloudflare
Arecord forvpn.<domain>, grey cloud / DNS only (WireGuard is UDP). - Scoped Zone:DNS:Edit token → UniFi → Settings → Internet → WAN → Dynamic DNS.
- WireGuard server → Use Alternate Address for Clients → the hostname.
- Re-export existing client configs; they don’t update themselves.
- Verify with
dig @1.1.1.1 vpn.<domain> +short, and rule out CGNAT.