# 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):

```ini
# /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
```

```sh
sudo wg-quick up wg-agent
curl -6 -s https://api64.ipify.org
# -> 2a04:2a01:eb5a:ca74:cef2:2a:323d:40d4
```

**With Whisper:**

```sh
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:

```sh
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:**

```sh
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](/docs/egress-auth).

### 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](/docs/resolver).

```sh
# 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:**

```sh
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:**

```sh
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](/docs/verify). Connect chooses how traffic *reaches* that identity; it never changes what the identity *is*.

## Next

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