Run a Pilot Node Through an HTTPS-Only Egress Proxy
Some environments block outbound UDP, return useless DNS answers for hostnames they have not approved, and allow exactly one way out: an HTTP proxy that accepts CONNECT to port 443. Hosted agent sandboxes such as Meta Muse's VM work this way, and so do some corporate desktops and CI runners. Compat mode already handles the UDP part. This guide handles the rest: getting a Pilot node registered when the daemon cannot resolve or dial the Pilot hostnames directly.
Diagnose first
Three checks tell you whether you need this guide or just compat mode.
# 1. Is UDP blocked? A daemon on the default transport never logs "daemon registered".
# 2. Is DNS poisoned? An address in 198.18.0.0/15 or 0.0.0.0 means yes.
getent hosts registry.pilotprotocol.network
# 3. Is the proxy the only way out? Direct TCP fails, CONNECT through the proxy succeeds.
curl -s --max-time 5 https://beacon.pilotprotocol.network/v1/compat -o /dev/null -w "%{http_code}\n"
curl -s --proxy "$HTTPS_PROXY" https://beacon.pilotprotocol.network/v1/compat -o /dev/null -w "%{http_code}\n"
If the second curl prints 426 (the beacon asking for a WebSocket upgrade) and the first prints 000, you are in a proxy-only environment. If the first also prints 426, direct TCP works and plain pilot-daemon -transport=compat is all you need.
Why the obvious fixes fail
Two constraints rule out most workarounds. The Pilot registry and beacon share one nginx listener that routes on the TLS server name, so the daemon's ClientHello must carry the real hostname. And TLS binds session keys to a transcript of the exact handshake bytes, so a forwarder that rewrites the server name produces a handshake the server rejects with DECRYPTION_FAILED_OR_BAD_RECORD_MAC. The daemon has to send the right ClientHello itself, which means it has to believe the hostname resolves somewhere reachable. /etc/hosts is usually read-only in these sandboxes, iptables usually lacks the NAT modules, and LD_PRELOAD cannot hook a Go binary's resolver because Go bypasses libc. The full list of attempts is in the skill's troubleshooting reference.
The design
- A transparent SNI router listens on
127.0.0.1:443. It reads one TLS record from each client, extracts the server name without modifying anything, opens aCONNECTtunnel through the proxy to that host, replays the original bytes, and pipes. - A hosts file maps
registry.pilotprotocol.networkandbeacon.pilotprotocol.networkto127.0.0.1. - A launcher bind-mounts that file over
/etc/hostsinside a private mount namespace (unshare -m, which needs root orCAP_SYS_ADMIN) and runspilot-daemonin compat mode there. Only the daemon sees the override.
All three pieces ship in the pilot-sandbox skill. Install it with clawhub install pilot-sandbox, or copy the folder from the repository.
Step by step
export PATH="$PATH:$HOME/.pilot/bin"
cd pilot-sandbox
# The router reads HTTPS_PROXY, e.g. http://user:[email protected]:3128
nohup python3 scripts/sni_router.py > sni_router.log 2>&1 &
grep listening sni_router.log
# The daemon, in its own mount namespace. setsid keeps it alive after your shell exits.
setsid unshare -m ./scripts/run-daemon.sh >> daemon.log 2>&1 < /dev/null &
sleep 30; grep -E "daemon registered|compat mode tunnel up" daemon.log
grep routed sni_router.log
The router log should show one routed SNI=registry... line and one routed SNI=beacon... line. The daemon log should show registration and the compat tunnel. If the router logs routes but the daemon logs a certificate error, read the trust section below.
The launcher passes these flags, which you can also run by hand for a one-off:
pilot-daemon -transport=compat \
-registry=registry.pilotprotocol.network:443 -registry-tls -registry-trust=system \
-compat-beacon=wss://beacon.pilotprotocol.network/v1/compat \
-socket=/tmp/pilot.sock -identity="$HOME/.pilot/identity.json"
Do not start it through pilotctl daemon start: that path forks with a scrubbed environment and does not forward every daemon flag.
Verify
pilotctl --json info
pilotctl --json trusted list
pilotctl --json ping 0:0000.0000.660F --count 2 --timeout 30s
Run pilotctl from your normal shell; it talks to the daemon over the Unix socket, which both namespaces share. A few pilotctl subcommands dial the registry themselves, lookup among them, and those fail outside the namespace. That is expected. The daemon's traffic is what matters, and trusted list and ping exercise both paths through it.
TLS trust: system or pinned
The registry presents a Let's Encrypt certificate. The launcher defaults to -registry-trust=system, which verifies it against the OS trust store and keeps working across renewals. Sandboxes that ship no CA bundle can pin instead:
export PILOT_REGISTRY_TRUST=pinned
export PILOT_REGISTRY_FINGERPRINT=<hex sha256 of the registry leaf>
Fetch the fingerprint through the proxy with the Python snippet in the troubleshooting reference. Let's Encrypt renews about every 60 days, so a pinned fingerprint is a maintenance item; when the daemon reports a mismatch, re-fetch it. In both modes end-to-end Ed25519 signatures still protect peer identity and payload integrity; TLS trust only covers the hop to the Pilot servers.
When something breaks
| Symptom | Cause | Fix |
|---|---|---|
HTTPS_PROXY not set | The router needs the proxy URL | Export it before starting the router |
proxy refused CONNECT ... 407 | Bad proxy credentials | Fix the user:pass@ part of the URL |
mount --bind failed | Not inside unshare -m, or not root | Launch exactly as shown |
| x509 unknown authority | No CA bundle | Switch to pinned trust |
| Fingerprint mismatch | Certificate renewed | Re-fetch, or switch to system trust |
| Routes logged, no registration | Proxy allows CONNECT but drops long-lived connections | Check the proxy's idle timeout; the beacon connection must persist |
