Whisper · Docs
Concepts

Connect & egress

Giving an agent a name is only half the job — until its packets actually leave from that name, "identity" is a claim anyone could fake with a spoofed header.

Whisper's connect tiers make the source address itself the proof, so the network layer and the identity layer become the same layer.

Most agent frameworks bolt identity onto the application layer: a signed JWT in a header, an API key in a config, a claim your code makes about itself. None of it survives contact with the wire — a proxy can strip it, a header can be forged, and the destination has no way to check it against anything independent.

The fix isn't a better token. It's making the agent's network address the credential: route it out through its own routable /128, and every peer — from a curl on the far end to a DANE-validating TLS stack — can check the claim against DNS, DNSSEC, and RDAP without trusting Whisper at all.

That's what connect does: it wires an agent's outbound (and, at Tier 1, inbound) traffic through the identity minted for it, at three depths of integration — pick the one that matches how much of your stack you want to touch.

The three tiers

Tier Mechanism Reachability Setup
1 — Routed /128 WireGuard: kernel, userspace (boringtun), or wireproxy (no-root) → local SOCKS5 Full two-way — inbound and outbound from the /128 One peer config or whisper connect --tier wireguard
1.5 — Egress proxy SOCKS5 / HTTP-CONNECT, source-bound to the /128 via AnyIP + IP_FREEBIND over 2a04:2a01::/32 Outbound only, but zero-install ALL_PROXY / HTTPS_PROXY env var
2 — Resolver only DoH on :443 (keyed) or a dedicated per-tenant /128 on standard :53 No data-path change Point your resolver config at it

Pick the tier by how much control you need over the actual bytes on the wire versus how little you want to touch your deployment. All three are live simultaneously for every registered agent — nothing about minting an identity commits you to one.

Tier 1 — routed /128 via WireGuard

This is the deep-integration path: the kernel (or a userspace WireGuard implementation) holds a real network interface whose source address is the agent's /128, so every packet — TCP, UDP, ICMP, anything — leaves already wearing the identity, and inbound connections to the agent's reverse-DNS name land on it too. WireGuard itself (Donenfeld, in-tree since Linux 5.6) is a minimal, modern point-to-point tunnel: a wg-quick config with a PrivateKey, the peer's PublicKey, an Endpoint, and AllowedIPs scoped to the agent's /128 is the entire client-side footprint — no daemon beyond wg-quick up, no root needed at all if you run it userspace via boringtun or tunnel it further through wireproxy down to a local SOCKS5 (the default whisper-cli picks for spawned agents, since most CI/container environments won't grant CAP_NET_ADMIN).

With stock tools — bring up the tunnel yourself once you have a peer config (from whisper connect --tier wireguard or the control-plane call below):

# /etc/wireguard/wg-agent.conf
[Interface]
PrivateKey = <agent's WireGuard private key>
Address = 2a04:2a01:eb5a:ca74:cef2:2a:323d:40d4/128

[Peer]
PublicKey = <Whisper edge public key>
Endpoint = ns1.whisper.online:51820
AllowedIPs = ::/0
PersistentKeepalive = 25
sudo wg-quick up wg-agent
curl -6 -s https://api64.ipify.org
# -> 2a04:2a01:eb5a:ca74:cef2:2a:323d:40d4

With Whisper:

whisper connect my-agent --tier wireguard   # writes the peer config, brings up wireproxy->SOCKS5, no root
CALL whisper.agents({op:'connect', args:{agent:'my-agent', tier:'wireguard'}})
  -> a routed WireGuard peer config (kernel or wireproxy/userspace -> local SOCKS5)

Tier 1.5 — the egress proxy (AnyIP + IP_FREEBIND)

Tier 1 needs an interface; Tier 1.5 needs nothing but an environment variable. The gateway binds outbound sockets for your agent to its /128 using Linux's IP_FREEBIND socket option together with an AnyIP route for 2a04:2a01::/32 — the kernel lets the proxy source a TCP connection from an address it doesn't have to hold on a real interface, as long as the routing table sends that prefix back to the box. The proxy then does a standard SOCKS5 (RFC 1928) or HTTP CONNECT (RFC 9110 §9.3.6) handshake with your client, and every byte after that leaves with the agent's /128 as its source. This is the zero-install path: curl, git, Node's fetch, Python's requests/httpx, and Claude Code all already speak HTTPS_PROXY/ALL_PROXY — nothing in your code changes.

With stock tools — once you have a connection_string, any SOCKS5-aware client works, no Whisper software involved:

export ALL_PROXY=socks5h://my-agent:et_...@ns1.whisper.online:1080   # et_ bearer from the connection_string
curl -s https://api64.ipify.org
# -> 2a04:2a01:eb5a:ca74:cef2:2a:323d:40d4
git clone https://github.com/whisper-sec/whisper-cli    # also routes through it, unmodified

With Whisper:

whisper connect my-agent            # default tier: socks5
whisper run -- python agent.py      # sets ALL_PROXY/HTTPS_PROXY, verifies egress, then execs your code
CALL whisper.agents({op:'connect', args:{agent:'my-agent', tier:'socks5'}})
  -> http_proxy (HTTPS_PROXY form), connection_string (socks5h://), dns, doh_url

Authentication on this tier defaults to a bearer bound to the /128 (a leaked token is useless off-address); mTLS and DPoP are available where you want the credential to be cryptographic rather than a shared secret — see Egress authentication: mTLS & DPoP.

Tier 2 — resolver only

No data-path change at all: keep the agent's existing address and point its DNS at Whisper instead. DoH (RFC 8484) on :443 takes your API key as a header; the per-tenant :53 resolver is a routable IPv6 /128 you dig like 1.1.1.1, no key needed on the wire because the source address of the query is the tenant. Both apply the same graph-first policy before answering — see Graph-first resolution.

# stock dig, no Whisper code
dig @2a04:2a01:0:53::53 example.com AAAA +dnssec

# DoH with curl, wire-format query
curl -H "X-API-Key: whisper_live_..." -H 'content-type: application/dns-message' \
     --data-binary @query.bin https://doh.whisper.online/dns-query

Confirming egress — never trust the client's own claim

The whole point of source-binding is that a third party, not your own process, reports what address it saw. Fetch the echo endpoint through the proxy or tunnel, not around it:

With stock tools:

curl -x socks5h://my-agent:et_...@ns1.whisper.online:1080 -s https://whisper.online/egress-ip
# -> {"ip":"2a04:2a01:eb5a:ca74:cef2:2a:323d:40d4"}

Any external echo service works the same way — curl -6 -x <proxy> https://api64.ipify.org — Whisper's own /egress-ip just saves the round trip of parsing plain text.

With Whisper:

whisper ip my-agent      # runs the same echo-through-the-proxy check as one command
whisper connect my-agent # and whisper run do this automatically before handing control to your process

A broken egress path fails loudly — a mismatched or unreachable echo is an error, never a silent fallback to your own address. That's the Postel-style contract in both directions: liberal about how you connect (three tiers, any stock client), strict about what leaves the wire — it is either provably the agent's /128, or the tool refuses to proceed.

Whichever tier you pick, the identity underneath doesn't change: the same /128, the same DANE TLSA pin, the same reverse-DNS name are all still independently checkable with dig/kdig/openssl — see Verify an agent. Connect chooses how traffic reaches that identity; it never changes what the identity is.

Next

Egress authentication: mTLS & DPoP — the cryptographic alternatives to the default bound bearer for Tier 1/1.5 traffic · Graph-first resolution — what happens to a query once it reaches Tier 2's resolver.