Leaving Tailscale for plain WireGuard: one hub, one DNS name, and Bob's Mac
In plain English. Tailscale has been doing three jobs for us: building encrypted tunnels, finding each machine wherever it is, and keeping a list of names. Plain WireGuard only does the first job. So moving off Tailscale means picking a replacement for the other two.
- Finding machines: make one box the meeting point (our UniFi gateway) and have every other machine call it. Only that one box needs a stable name. Its home internet address changes from time to time, so dynamic DNS keeps the name pointed at it.
- Bob: his Mac Mini calls our gateway, not the other way round. That means his changing address does not matter. WireGuard learns it from his packets. He does not need to advertise anything.
- The catch on Macs: the macOS WireGuard app does not look the name up again when our address changes. Bob's Mini needs a small watchdog for that.
- What we give up: direct machine-to-machine paths, a relay for networks that block UDP, automatic names, and access rules managed in one place.
Bottom line: it is doable with gear we already own. Run it side by side with Tailscale for two weeks before switching anything off.
Figure 1. Target shape. The dashed box is the only name that has to follow a changing IP address.
What we have today
Checked from the M4 Max on October 31, 2026. The tailnet has 18 nodes and 8 were online: workstations, the GB300 Station, a NetBox/agent box, family Macs, phones and tablets. Most lab machines share one LAN with the gateway. They only use Tailscale when James is away from home. Bob is not on this tailnet.
Both of our sites run UniFi gateways on UniFi OS 5.1.132. The main gateway holds a real public IPv4 address. We checked that the address on its WAN port is the same one the internet sees, so there is no carrier-grade NAT in front of it. That matters, because a WireGuard server behind CGNAT cannot take incoming connections at all. The address is assigned by DHCP, so it can change.
The dynamic DNS problem
A WireGuard peer can point at a hostname, but WireGuard looks that name up once and then never again (ArchWiki). DDNS only helps if two things happen. The name has to stay current. And clients have to look it up again when the address changes.
Keeping the name current
- Our blog domain's DNS can't be updated by a script. It is hosted at GoDaddy. Since May 2024 GoDaddy only gives DNS API access to accounts with 10 or more domains or a paid Discount Domain Club plan (summary with the vendor notice). We don't qualify.
- UniFi has a DDNS client built in. It is inadyn. Officially supported providers are afraid, cloudflare, dnspark, dslreports, dyndns, easydns, namecheap, noip, sitelutions and zoneedit. It only works when the gateway has a public IP (Ubiquiti).
- Pick: buy a cheap domain just for infrastructure and host it on Cloudflare's free plan. Make a Cloudflare API token that can edit DNS in that one zone. Point the gateway's DDNS at it, using the WAN that actually carries traffic. This leaves the blog and its mail records alone. A zero-cost alternative is a free afraid.org name plus a
CNAMEfrom the existing domain. - Belt and braces: also run favonia/cloudflare-ddns in a container on a LAN server. Community posts say inadyn on UniFi can stop updating after a gateway restart. We have not reproduced that. A second updater is cheap insurance.
- Watchdog: a Hermes no-agent cron job every 10 minutes compares the name to the public IP seen from the LAN. It sends an email if they disagree for more than 15 minutes. Keep the record TTL at 60–120 seconds.
Making clients look the name up again
- Linux and Homebrew
wg-quick: runreresolve-dns.shfrom wireguard-tools every 30–60 seconds. - iPhone: the iOS app sets its peer endpoints again on every network change (per the description in the PR below), so a phone moving between Wi-Fi and cellular picks up a new hub address at the next switch.
- macOS app: on a network change it only rebinds its sockets and keeps the old endpoint. The fix is in wireguard-apple PR #63, which had not been merged as of this writing. A Mac that never changes networks (like Bob's Mini on Ethernet) also never re-resolves. It needs help.
Bob's Mac Mini
The request was that Bob "advertise his dynamic IP somehow." The better design gets rid of that need. WireGuard has built-in roaming: after each authenticated packet, the receiver remembers the address that packet came from. If Bob's Mini dials our hub and sends a keepalive every 25 seconds, the hub always knows where Bob is. James can reach Bob through the tunnel. Bob's router needs no open port, and his house needs no DDNS.
- Bob's side routes only the tunnel subnet (for example
10.66.0.0/24). His internet and home LAN are untouched. Many ISP gateways use the same192.168.1.xrange we do, and this avoids the clash. - Firewall it one way. In UniFi's zone firewall, James → Bob is allowed and Bob's tunnel address → our LAN is blocked. If Bob's machine is ever compromised, it should not open a door into the lab.
- Hub address changes. Two options, both to be tested on one of our own Macs first. (a) Keep the App Store app, which Bob can see and toggle. Add a launchd watchdog: if the last handshake is older than 3 minutes and the hub's DNS no longer matches, it restarts the tunnel with
scutil --nc stop/start. This is untested. (b) Use Homebrewwireguard-toolsunder launchd withreresolve-dns.sh, which is the upstream-documented path. Our lean: (b) for an unattended machine, if Bob is fine with no menu-bar icon. - Keep the current remote-desktop tool as break-glass until the tunnel has survived at least one forced hub IP change and two weeks of normal use.
- Only if Bob truly needs inbound (something must reach him while his tunnel is down): run a DDNS updater on the Mini and forward a UDP port on his ISP gateway. Before that, confirm the gateway's WAN address is a real public one. We don't recommend it, because it opens a port at Bob's house to fix a problem the outbound design doesn't have.
What we give up
| Capability | Tailscale | Plain WireGuard hub |
|---|---|---|
| Path between two remote machines | Direct, via NAT traversal | Through the hub, both ways |
| Networks that block UDP (some hotels) | DERP relay over HTTPS | Tunnel fails; keep a fallback |
| Names | MagicDNS | Gateway local DNS, pushed as the tunnel's DNS server |
| Who can reach what | Central ACL policy + identity | Gateway firewall rules per tunnel IP |
| Keys | Rotated and expired by the service | Ours: generate on device, vault any config that holds a private key |
| Extras (Taildrop, Tailscale SSH) | Built in | Gone; plain SSH and AirDrop instead |
| Third-party coordination service | Required | None |
Middle path. If the main goal is getting off Tailscale's cloud rather than off any coordinator, Headscale self-hosts the control plane. It keeps the Tailscale clients, NAT traversal and MagicDNS. It needs a public HTTPS host. The trade is one more server to run against everything in the table above.
Hermes: docs and plugins
Our Hermes Desktop clients already connect to the dashboard by LAN address, behind password sign-in. Over a WireGuard tunnel that routes the home LAN, the URL works unchanged. The Desktop docs describe a remote gateway as something to reach over "Tailscale or another trusted VPN". They say to keep a password-protected backend off the open internet and never forward its port from the router. A WireGuard tunnel meets that bar. Hermes' SSRF guard already treats RFC 1918 and 100.64.0.0/10 as private, so a 10.x tunnel subnet gets the same protection as the tailnet did.
| Catalog plugin | What it does | Fit here |
|---|---|---|
| wireguard | Read-only wg show status and peer list | Windows only. Not usable on our Macs or UniFi gateway, but it's a good pattern to copy. |
| netplane | Installs and runs a Headscale control plane, with join keys and a node list | Only for the Headscale middle path. Needs a fresh Debian/Ubuntu host. |
| hermes-tailscale | Desktop roster of tailnet machines | Retires with Tailscale. |
| hermes-vpn | Picks a VPN exit country for the agent's browser | Different problem. Not needed. |
No catalog plugin manages a UniFi WireGuard server. We plan to build two small, read-only pieces of our own. One is the DDNS-drift watchdog above. The other is a skill that reads handshake age per peer, so "is Bob's tunnel up?" gets a real answer. Adding or removing peers stays a human decision in the UniFi UI, the same line the wireguard plugin draws.
Rollout
Tailscale stays up through every phase. Rollback at any step means using the tailnet as we do today.
| # | Step | Gate before moving on |
|---|---|---|
| 0 | Infra domain on Cloudflare, scoped token, gateway DDNS, LAN updater, watchdog cron | Name matches the public IP; watchdog fires on a forced mismatch |
| 1 | WireGuard server on the live WAN, tunnel subnet outside 192.168.x and 100.64/10 | Handshake from a phone on cellular |
| 2 | James's phone and laptop, on-demand away from home Wi-Fi, DNS = gateway | Hermes Desktop sign-in and SSH to the lab over cellular |
| 3 | Second site, site-to-site | Traffic both directions |
| 4 | Bob's Mini, one-way firewall rule, re-resolve watchdog | James reaches Bob; Bob cannot reach our LAN; survives a forced hub IP change |
| 5 | Remote family Macs, same pattern as Bob | SSH over the tunnel per node |
| 6 | Two weeks with tailscale down, then log out and uninstall | No path left that only Tailscale provides |
Still open
- Why we're leaving decides plain WireGuard versus Headscale. That's James's call, and this post doesn't assume one.
- Whether anything has to reach Bob while his tunnel is down. If nothing does, Bob's house needs no DDNS.
- Whether UniFi's site-to-site option fits our two consoles. We'll check that in the UI, not from a spec sheet.