# SDK & CLI

> The tunnel CLI surface: keys, domains, http, tcp, udp, connect, p2p, plus config, env vars, and the data-plane engine flags.

The **`tunnel` CLI** and its QUIC data-plane engine drive the tunnel
modality. There is no typed `Tunnel` SDK class today.

> **NOTE:**
> A programmatic client, which embeds a tunnel in your own process, is
> **Preview**. See [Programmatic API (Preview)](#programmatic-api-preview) at the
> bottom. For shipping work, use the CLI documented here.

## Install

**One-liner:**
```sh
curl -fsSL https://artifacts.clutchcall.dev/rnp/install.sh | sh
```

**Homebrew / manual:**
```sh
# Single static binary, no runtime deps — drop it on $PATH
tunnel version
```

The CLI is a pure-static binary for Windows, macOS, and Linux (amd64 / arm64).
It starts a separate QUIC engine for the data path. The engine ships next to
the CLI, or you point at it with an env var (see [Config](#config)).

## Save a key

A tunnel uses an **API key** (`rtk_…`). Create the key in the Tunnel console
on the **API keys** page, then save it on the machine:

```sh
tunnel config add-authtoken rtk_…
```

The key is the only credential that the CLI keeps on the machine. A tunnel
does not need a sign-in. For CI or a service, set `TUNNEL_AUTHTOKEN` instead.
The CLI also reads the old `RNP_AUTHTOKEN` variable.

`tunnel login` opens the browser to sign in. Use it only for key and domain
commands. The CLI does not save the sign-in session.

> **NOTE:**
> The CLI was named `rnp` before. The CLI still runs when you start it as `rnp`,
> and it shows one warning line.

## Command surface

| Command | What it does |
| --- | --- |
| `tunnel config add-authtoken <rtk_…>` | Save a key on this machine. No sign-in. |
| `tunnel login` | Sign in. If the machine has no key, select or create one. |
| `tunnel keys` | List the keys of the organization |
| `tunnel keys create [--name N]` | Create a key and save it on this machine |
| `tunnel keys use` | Select a key for this machine |
| `tunnel keys revoke <id>` | Revoke a key. Its tunnels stop. |
| `tunnel domains` | List the domains and their status |
| `tunnel domains add <name>` | `acme` gives `acme.rnp.sh`. It is active immediately. |
| `tunnel domains add <hostname>` | A custom domain. The CLI shows the DNS record to create. |
| `tunnel domains check <hostname>` | Check the DNS now |
| `tunnel domains rm <hostname>` | Remove a domain |
| `tunnel http <port> [--url HOST]` | Expose `localhost:<port>` at `https://HOST` |
| `tunnel tcp <port> [--url NAME]`, `tunnel udp <port> [--url NAME]` | Share a raw TCP or UDP service privately under `NAME` |
| `tunnel connect <name> --listen [HOST:]PORT` | Reach a shared TCP or UDP service on a local port |
| `tunnel start <tunnel>…`, `tunnel start --all` | Start the named tunnels in the config file |
| `tunnel status` | Organization, key, hosts, and egress for this month. No sign-in. |
| `tunnel p2p share <pair-id> --port <p>` | Share a local service directly with a peer (no relay) |
| `tunnel p2p connect <pair-id> --listen <p>` | Reach a peer's shared service directly |
| `tunnel service install\|uninstall` | Run `tunnel start --all` at login (Linux and macOS) |
| `tunnel update`, `tunnel version` | Update the CLI, or show the version |

The commands `claim`, `devices`, `topup` and `logout` are removed. Use
`tunnel domains add` and `tunnel keys`.

---

## `http` — expose a local service

```sh
tunnel http <local-port> [--url <host>]
```

The command dials the nearest edge PoP over QUIC. It forwards inbound public
traffic to `localhost:<local-port>`. It prints the public URL and runs until
you interrupt it.

- **`<local-port>`** (`int`, required) — The local TCP port to expose (e.g. `3000`).
- **`--url`** (`string`) — The public hostname, for example `acme.rnp.sh` or your own domain   (`tunnel.example.com`). Add the domain on the **Domains** page of the Tunnel   console, or with `tunnel domains add`, before you use it.

```sh
tunnel domains add acme                       # reserve https://acme.rnp.sh
tunnel http 3000 --url acme.rnp.sh            # → https://acme.rnp.sh
tunnel http 3000 --url tunnel.example.com     # → https://tunnel.example.com
```

## `tcp`, `udp` and `connect` — private raw services

`tunnel tcp` and `tunnel udp` share a raw service privately under a name.
`tunnel connect` reaches it from another machine. The other machine uses a key
of the same organization, or a key that is bound to the name.

```sh
db$     tunnel tcp 5432 --url pg
laptop$ tunnel connect pg --listen 127.0.0.1:5432
laptop$ psql -h localhost -p 5432
```

> **TIP:**
> WebSocket upgrades, server-sent events, and chunked/streaming responses pass
> through unchanged. Each public connection is its own QUIC stream.

---

## `p2p` — direct peer-to-peer

Two peers run with the **same pair-id**. They discover each other through the
control-plane rendezvous and connect **directly**. The edge brokers candidates
but never carries bytes. Across NATs, set the `TUNNEL_STUN` env var to a
reflector, so each side can gather its public candidate.

**Share (the box with the service):**
```sh
tunnel p2p share <pair-id> --port <local-port> [--proto tcp|udp|http]
```

**Connect (the box reaching in):**
```sh
tunnel p2p connect <pair-id> --listen <local-port>
```

- **`<pair-id>`** (`string`, required) — A shared secret that both peers know. It gates the direct tunnel and keys the   rendezvous.
- **`--port`** (`int`) — (share) The local service port to expose to the peer.
- **`--listen`** (`int`) — (connect) A local port to listen on. The CLI pipes connections to it through   to the peer's shared service.
- **`--proto`** (`string`, default `tcp`) — (share) `tcp`, `udp`, or `http`.
- **`--stun`** (`string`) — The STUN reflector `host:port` for reflexive candidate gathering. You can set   `TUNNEL_STUN` instead.

```sh
# Box A shares its SSH; Box B reaches it on its own port 8022
A$ tunnel p2p share mylink --port 22
B$ tunnel p2p connect mylink --listen 8022
B$ ssh -p 8022 localhost       # rides the direct QUIC link
```

### When the punch fails

Two subscribers of one carrier NAT often cannot reach each other directly.
The connect side can then route through the relay:

```sh
B$ tunnel p2p connect mylink --listen 8022 --relay <pop-host>:<port>
```

The direct dial has a 5 second bound. After it, the CLI proxies each local
connection to `--relay` over plain TCP. That port is the sharer's ordinary raw
TCP tunnel on the PoP.

> **WARNING:**
> The `--relay` fallback is a plain TCP proxy to a public port on the PoP. That
> port has no pair-token gate. Only the shared service's own authentication
> protects it. Use it for a service that authenticates by itself, such as SSH.
> Do not use it for a policy server or a control loop. For those, use the SDK
> path, which keeps the QUIC connection, its certificate pin and its pair token
> on the relay path. See [P2P on QUIC](/sdks/python/p2p).

> **NOTE:**
> Endpoint-independent (cone) NATs punch through directly. Symmetric NATs fall
> back to the relayed path.

---

## `status`

```sh
tunnel status     # organization, key, hosts, egress this month
```

`status` uses the saved key. It does not need a sign-in.

---

## Config

The CLI keeps its config file in your user config directory (mode `0600`).
The file holds the API key (`authtoken`), the control-plane base URL, and
optional named tunnels for `tunnel start`. A flag wins over an environment
variable, and an environment variable wins over the file.

### Environment variables

Each variable has the `TUNNEL_` prefix. The CLI also reads the old `RNP_`
name of each variable when the `TUNNEL_` name is not set.

| Var | Purpose | Default |
| --- | --- | --- |
| `TUNNEL_AUTHTOKEN` | API key (`rtk_…`) | `authtoken` in the config file |
| `TUNNEL_API` | Control-plane base URL | `https://api.firstbyte.fast` |
| `TUNNEL_POP` | QUIC edge `host:port` (data plane) | `<base>:4443` |
| `TUNNEL_STUN` | STUN reflector `host:port` for P2P | — |
| `TUNNEL_ENGINE_BIN` | Path to the QUIC data-plane engine (`tunnel-engine`). The old name is `RNP_TUNNEL_BIN`. | next-to-binary / `$PATH` |
| `TUNNEL_BASE_DOMAIN` | Public base domain for printed URLs | `firstbyte.fast` |

### Data-plane engine flags

`tunnel http` execs the QUIC engine (`tunnel-engine`)
with the resolved arguments. You rarely call the engine directly, but the
surface is:

```sh
tunnel-engine http <port> --server <pop> --token <jwt> [--slug <slug>]
tunnel-engine p2p share <proto> <port> --quic-port <p> --peer <ip:port> --token <pair-id> [--stun <addr>]
tunnel-engine p2p connect --quic-port <p> --peer <ip:port> --listen <port> --token <pair-id> [--stun <addr>]
```

---

## Remote desktop on the same transport

A remote-desktop client (a fork of an open-source remote-desktop project) runs
its network transport over this **same raw-QUIC relay** instead of its own
rendezvous/relay. To enable it, point the client's `quic-relay-server` option
at a `host:port` that runs the tunnel module:

- The **controlled box** registers its device id as a slug (`"proto":"quick"`)
  and parks bidi work streams.
- The **controller** dials the box by slug. The edge splices the two QUIC
  streams.
- The remote-desktop client's own message framing and end-to-end encryption
  ride unchanged on top. The edge only secures the QUIC hop to the PoP. It
  never sees cleartext.

This is the **remote-desktop tunnel type** in the
[details page](/modalities/tunnel/details#tunnel-types). You need no extra CLI
beyond the relay endpoint.

---

## Programmatic API (Preview)

> **WARNING:**
> **Preview / forward-looking.** The shipping SDK does not contain a typed
> `Tunnel` client that embeds a tunnel inside your own process. Ship with the CLI
> today. The shape below is an illustration, and it can change.

  
```ts
// Preview — not yet shipped
import { Tunnel } from "@clutchcall/agents/tunnel";

const tunnel = new Tunnel({
  pop:   "edge.firstbyte.fast:4443",
  token: process.env.CLUTCHCALL_CREDENTIALS!,
});

const { url } = await tunnel.http({ port: 3000, slug: "alice" });
console.log("public URL:", url);   // https://alice.firstbyte.fast
await tunnel.close();
```
  
  
```python
# Preview — not yet shipped
from clutchcall-agents.tunnel import Tunnel

tunnel = Tunnel(pop="edge.firstbyte.fast:4443", token=TOKEN)
handle = tunnel.http(port=3000, slug="alice")
print("public URL:", handle.url)
tunnel.close()
```
  

## Related

- [Tunnel overview](/modalities/tunnel/details) — wire model & architecture
- [Cookbook](/modalities/tunnel/cookbook) — task snippets
- [Recipes](/modalities/tunnel/recipes) — worked examples
