A full-tunnel replacement for the official Cloudflare WARP client, built on
usque — with the one thing the official
client won't do: your own DNS (NextDNS) inside the tunnel. One root daemon, a thin
CLI, and an optional menubar app.
The story of why (and every wrong turn) is in
docs/warp-nextdns-macos-journey.md.
One launchd root daemon — cfwarpd — that does everything in-process:
| Piece | Does |
|---|---|
| tunnel | usque nativetun, embedded as a library — MASQUE (QUIC/HTTP3) to Cloudflare's edge |
| resolver | AdGuard dnsproxy, embedded — 127.0.0.1:53 → NextDNS / Cloudflare / custom / off |
| routing + netwatch | split-default via the utun, endpoint pinned direct, re-asserted on reconnect / network change |
| split-tunnel | one excludes list — IP/CIDR routed direct (browsers too), domains pinned on-demand via the resolver (apps) |
Tunnel and DNS are independent — start/stop/restart control only the tunnel;
dns controls only DNS. The CLI and menubar talk to the daemon over a user-owned unix socket.
Routing is split-default (0.0.0.0/1 + 128.0.0.0/1 via the utun) with the MASQUE
endpoint pinned to the physical gateway so usque's own QUIC can't recurse into its own
tunnel. The daemon re-asserts routing on every (re)connect (self-heals across sleep/wake
and network switches) and retries with backoff if the tunnel can't come up, surfacing
the last error in cfwarp status.
cmd/cfwarpd/ the daemon (Go): tunnel + dns + routing + netwatch + excludes
bin/cfwarp thin CLI (bash) bin/cfwarpd ← built by scripts/install.sh (gitignored)
launchd/ com.cfwarp.daemon.plist template (rendered to *.local.plist by scripts/install.sh)
scripts/ install · uninstall · selftest · gen-split-direct
menubar/ Swift app + login-agent template + install.sh
config/ config.yml.example (real config + identity live in ~/.config/cfwarp/)
docs/ the writeup + split-tunnel/perf reference
Prereqs: macOS (Apple Silicon), Go, and Xcode CLT for the menubar app.
git clone <this-repo> cfwarpd && cd cfwarpd
./scripts/install.sh # first run creates ~/.config/cfwarp/config.yml
$EDITOR ~/.config/cfwarp/config.yml # set your NextDNS profile
./scripts/install.sh # build + register + render the plist
# THE privileged step — load the daemon (re-run this same command after upgrades):
sudo sh -c 'launchctl bootout system/com.cfwarp.daemon 2>/dev/null;
mkdir -p /usr/local/libexec &&
install -o root -g wheel -m 0755 "$PWD/bin/cfwarpd" /usr/local/libexec/cfwarpd &&
cp launchd/com.cfwarp.daemon.local.plist /Library/LaunchDaemons/com.cfwarp.daemon.plist &&
launchctl bootstrap system /Library/LaunchDaemons/com.cfwarp.daemon.plist &&
ln -sf "$PWD/bin/cfwarp" /usr/local/bin/cfwarp'
cfwarp status # verify (expect warp=on)
./menubar/install.sh # optional: menubar shield + toggle
# optional: split-tunnel the well-peered CDNs (Google/CF/Fastly/… + NextDNS) direct
python3 scripts/gen-split-direct.py > ~/.config/cfwarp/split-direct.list
$EDITOR ~/.config/cfwarp/config.yml # add: exclude-file: split-direct.listThe daemon runs from a root-owned copy at /usr/local/libexec/cfwarpd (a root LaunchDaemon
must not execute a user-writable binary); the cutover installs it there.
scripts/install.sh builds against the
usque fork (macOS nativetun + the reconnect-deadlock
and stop-teardown fixes) straight from go.mod — nothing to clone — and registers a WARP
identity into ~/.config/cfwarp/config-native.json.
cfwarp start | stop | restart # TUNNEL only
cfwarp dns nextdns | cloudflare | custom <up> | default # DNS only — persists across reboots
cfwarp status | doctor | logs # read-only; doctor flags DNS/route drift
cfwarp exclude <ip|cidr|domain> | include <…> # split-tunnel a host direct- DNS upstream formats: plain IP (UDP),
https://…/dns-query(DoH),tls://…(DoT). - Excludes — one list (
config.yml, or anexclude-file:it points at): an IP/CIDR gets a direct route immediately (covers browsers too); a domain is suffix-matched by the resolver and its IPs pinned on-demand (apps only). Seedocs/split-tunnel-and-perf.md. - The daemon autostarts on boot; pause across reboots with
sudo launchctl disable system/com.cfwarp.daemon(enableto undo). - Another VPN (e.g. Pritunl):
cfwarp stop, and if it needs127.0.0.1:53,cfwarp dns default. Return withcfwarp start+cfwarp dns nextdns. - Uninstall:
sudo scripts/uninstall.sh(back to factory default; add--purgeto also delete the built binaries, identity, and all~/.config/cfwarpstate for a blank slate).
- All per-machine state is in
~/.config/cfwarp/:config.yml(profile, dns mode, interface, excludes + optionalexclude-file— interface/device auto-detect when unset),config-native.json(WARP private key, 0600), and anysplit-direct.list. - Logs:
logs/cfwarpd.log(setCFWARP_LOG=debugin the plist for more). - Menubar icon: filled shield = tunnel + DNS, half = tunnel only, plain = DNS only, slashed = off.
