Mesh Plane and Capabilities
Every other page on this site — Your first tunnel,
CT_AGENT_ORIGIN_PROTO, the whole Rot/Gelb/Grün story — describes Browser Plane mode
(CT_AGENT_MODE=browser): an ordinary browser reaching an ordinary HTTPS site. It’s not the default.
Leave CT_AGENT_MODE unset and ct-agent runs Mesh Plane instead — the mode
ADR-0010
prioritized first, specifically because Browser Plane “structurally leaks the hostname to the operator”
via TLS SNI. Mesh Plane doesn’t.
Browser Plane and an Agent-Fabric channel
are not mutually exclusive — see
Serve a tunnel and a channel together for running
both on the same service, as two independent ct-agent processes.
Since these two modes share most of their environment-variable namespace, a config for one is often
only a line or two away from the other — and running the wrong one doesn’t fail loudly, it just quietly
doesn’t do what you expected. Example configurations by use case
lines up complete, working .env files for each, plus how to tell which one you’re actually running
from a live process (the log line alone can be misleading).
What’s actually different
Mesh Plane routes by an opaque routing token, not a hostname the operator’s edge has to read to route — the SNI/hostname-leakage Browser Plane accepts as a tradeoff simply doesn’t apply. It’s also not HTTP-shaped at all: any protocol, including UDP, over a Noise-encrypted session your Client authenticates end-to-end, with no TLS certificate anywhere in the path (no Rot/Gelb/Grün story here — that’s entirely a Browser Plane concept).
The UDP path specifically is real, not aspirational — CT_AGENT_ORIGIN_PROTO=udp (see
Environment variables) bridges datagrams
instead of a stream, and both ends are hermetically tested against a real UDP origin, re-confirmed
passing for this page: ct-agent’s serve_noise_udp_bridges_datagrams_to_origin on the Agent side, and
ct-client’s udp_selftest/run_bench_udp on the Client side. Same caveat as everywhere else on this
page: the Agent side is yours to configure with one env var, but consuming it still needs a Client that
speaks Mesh Plane’s own framing over UDP — there’s no browser or curl equivalent for this leg any more
than there is for TCP.
The Capability: how a Client gets in, without the operator vouching for anything
A Browser Plane client just needs a public hostname to type into an address bar; a Mesh Plane Client
needs the routing token and a way to authenticate your Origin before the Noise handshake even runs.
Per ADR-0014,
ct-agent bundles exactly that into one self-contained artifact — a Capability — that you distribute
to your own authorized Clients through your own out-of-band channel (Signal, a password manager, however
you’d share any other credential). The operator only ever stores an opaque token-to-tunnel mapping; it
never holds your Origin’s key and can’t forge or be compelled to hand over what it doesn’t have.
CT_AGENT_CAPABILITY_OUT (see
Environment variables (core tunnel)) is
where your agent writes this file — not fetched from the control plane, minted locally
(mint_capability, source-confirmed) from material the agent already has: a fresh random routing token
by default, its own Origin identity, and the edge address. The token it picks is simply what gets
registered in the platform’s Tunnel Registry afterward — the agent originates the trust material, the
operator just records it.
The exact wire format, confirmed directly against ct_common::Capability::encode/decode in source:
routing_token (32 bytes) | origin_identity (32 bytes) | addr_len (u32 LE) | edge_addr (addr_len bytes)
routing_token is the same routing token your tunnel already has; origin_identity is your Origin’s
static Noise public key, which a Client pins to authenticate the Origin end-to-end — this is the piece
that makes possession of the Capability alone sufficient to reach and trust your Origin, with no
operator involvement in that trust decision at all.
How a Client actually reaches your Origin
Per ADR-0015
— the Tailscale/DERP model — holding a Capability doesn’t mean traffic routes through the platform at
all. If your Agent has advertised a reachable direct address, the Client asks the edge for it (a plain
lookup, 'P' query — no proof-of-work gate on this specific step) and tries dialing straight there. If
that succeeds, traffic flows Client↔Agent directly — the operator is genuinely out of the data path, not
just claiming to be. Otherwise the connection relays through the edge.
CT_AGENT_DIRECT_ADVERTISE is actually consumed
(ct-agent's serve.rs::run_agent): the real shipped mechanism for Mesh
Plane tunnels is simpler and opt-in, not automatic STUN-style traversal — your Agent only
advertises a direct address at all if you set
CT_AGENT_DIRECT_ADVERTISE
to an IP it's genuinely reachable at (a public IP, or one you've port-forwarded yourself); the guided
setup script doesn't set it, so a default onboarded tunnel is relay-only until you configure this
yourself. Real NAT-traversal engineering (libp2p's DCUtR hole-punch) does exist in this codebase, source-
confirmed in ct-agent's p2p.rs — but it's for the separate
Agent-Fabric channel system
(#121), not Mesh Plane tunnels, and even there it's only validated against a real 2-NAT lab setup, not
proven for every real-world NAT.
Confirmed real, not just described in the ADR — but the specific mechanism has been rebuilt since this
page was first written. The very first implementation of this (crates/client::rendezvous, a PoW-gated
design from the earliest development cycles) turned out to have zero production callers on either side
and was deleted as dead code (issue #580); the tunnel level had already moved to a different, simpler
protocol before that removal. What’s live today, source-confirmed in crates/client/src/transport.rs:
query_direct_endpoint (the 'P' lookup above), client_tunnel_direct (the direct-dial attempt), and
client_tunnel_auto/client_tunnel_p2p_or_relay (M11.4b-iv, #374) — which don’t just try direct then
fall back serially, but race the direct attempt against the Edge relay concurrently, giving direct a
75ms head start (DIRECT_HEAD_START) so a live direct path almost always wins without a slow-but-real
one being starved by a faster relay. cargo test -p ct-client --lib transport::, re-run hermetically for
this page, is 14/14 passing, including client_tunnel_auto_falls_through_to_relay_when_the_direct_endpoint_query_stalls
and p2p_or_relay_fallback_times_out_against_a_stalled_edge — both exercise this exact fallback path.
crates/client in this repo (ct-client) reads like an internal smoke-test/bench
tool ("verifying the round-trip", printing labeled CSV rows for a latency sweep) rather than a
customer-facing application — consistent with the design itself: a Capability is meant to be consumed
by your own Client implementing the Noise handshake
(ADR-0013),
not necessarily any specific binary this repo ships.
Revocation
Per ADR-0014: revoking access is rotating the routing token and/or the Origin key — there’s no separate “Capability revocation” mechanism to reason about, because a Capability’s own validity is entirely derived from those two things still being live. Concretely:
- Retire the routing token entirely (every Capability minted against it stops working, permanently) — Revoke on your tunnels page.
- Rotate just the Origin key (invalidates Capabilities minted with the old key, without touching the
routing token or requiring re-onboarding) —
ct-agent rotate, in the CLI reference.