> ## Documentation Index
> Fetch the complete documentation index at: https://docs.tensorlake.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Local Tunnels

> Forward a local TCP port to a port inside a sandbox over an authenticated WebSocket.

Tunnels give your machine a `localhost:<port>` that maps directly to a port inside a running sandbox. The relay travels over a WebSocket through the sandbox proxy, so your TensorLake credentials authenticate every connection. You do **not** need to add the port to `exposed_ports` or make it public.

**Reach for a tunnel when you need a raw TCP connection into a sandbox.** The sandbox proxy at `*.sandbox.tensorlake.ai` only speaks HTTP, WebSocket, gRPC, and SSH. Anything else (VNC's RFB protocol, the Postgres wire protocol, MySQL, Redis's RESP, MongoDB, custom binary protocols) needs a tunnel because the proxy cannot frame those bytes for you.

You can also use a tunnel for HTTP/WS/gRPC traffic when you would rather keep the port private to your laptop than expose it through the public sandbox URL. Driving Chrome's DevTools Protocol from your laptop is a typical case: CDP is WebSocket, so the proxy could carry it, but a tunnel keeps the debugger reachable only at `127.0.0.1` and skips the per-port `exposed_ports` configuration.

Tunnels and exposed ports are independent. A tunnel works even when the port is not in `exposed_ports`.

## Open a Tunnel

The simplest way is the CLI. Pick any local port (defaults to the same number as the remote port) and leave the command running.

<Tabs>
  <Tab title="CLI">
    ```bash theme={null}
    tl sbx tunnel <sandbox-id-or-name> 5901 --listen-port 15901
    ```

    The command keeps running and prints connection events. Press `Ctrl+C` to stop the tunnel; the sandbox keeps running.

    Without `--listen-port`, the local port matches the remote port:

    ```bash theme={null}
    tl sbx tunnel <sandbox-id-or-name> 9222
    ```
  </Tab>

  <Tab title="JavaScript">
    ```javascript theme={null}
    import { Sandbox } from "tensorlake";

    const sandbox = await Sandbox.connect({ sandboxId: "<sandbox-id>" });
    const tunnel = await sandbox.createTunnel(5901, { localPort: 15901 });

    const { host, port } = tunnel.address();
    console.log(`tunnel listening on ${host}:${port}`);

    // ... use it ...

    await tunnel.close();
    ```

    `createTunnel(remotePort, options)` returns a `TcpTunnel`. Useful options:

    * `localHost`: bind interface (defaults to `127.0.0.1`).
    * `localPort`: local port number; pass `0` for an ephemeral port and read it back from `tunnel.address()`.
    * `connectTimeout`: seconds to wait for each WebSocket connection (defaults to `10`).
  </Tab>

  <Tab title="Python">
    The Python SDK does not yet ship a native tunnel helper. Drive the CLI from a subprocess:

    ```python theme={null}
    import subprocess

    tunnel = subprocess.Popen(
        ["tl", "sbx", "tunnel", "<sandbox-id>", "9222", "-l", "9222"],
    )
    try:
        # Use http://127.0.0.1:9222 from your code.
        ...
    finally:
        tunnel.terminate()
        tunnel.wait()
    ```
  </Tab>
</Tabs>

<Note>
  The local listener is per-process. If you want two clients to share one tunnel, run the CLI once and connect both clients to the same `localhost:<port>`.
</Note>

## How It Works

The CLI and the TypeScript SDK both speak the same protocol:

1. A WebSocket is opened to the sandbox proxy. The SDK authenticates with an
   API key; the CLI uses either an API key or its stored PAT.
2. The proxy authorizes the request, finds the dataplane that owns the sandbox, and pipes bytes to `127.0.0.1:<remote-port>` inside the sandbox.
3. The local TCP listener accepts a connection from your client and relays bytes both ways across the WebSocket.

Because every byte rides on an authenticated WebSocket, the remote port stays private to your account: there is no public hostname for it.

## Common Patterns

| Inside the sandbox                | Local port | Client                                                              |
| --------------------------------- | ---------- | ------------------------------------------------------------------- |
| `5901` (TigerVNC)                 | `15901`    | macOS Screen Sharing, RealVNC, TigerVNC, Remmina                    |
| `9222` (Chrome DevTools Protocol) | `9222`     | Playwright `connect_over_cdp`, Puppeteer, `chrome-remote-interface` |
| `5432` (Postgres)                 | `5432`     | `psql`, DBeaver, TablePlus                                          |
| `3000` (dev server)               | `3000`     | Browser at `http://localhost:3000`                                  |

Tunneling is also the easiest way to reach the sandbox's authenticated [Computer Use](/sandboxes/computer-use) VNC port from a desktop client without polling screenshots, or to point Playwright at sandboxed Chrome. See [Drive Chrome over CDP](/sandboxes/chrome-cdp) for the full walkthrough.

## Troubleshooting

* **`Connection refused` from the local end.** The remote service inside the sandbox is not listening on the port yet. Tail its logs (`tl sbx exec <id> -- bash -lc 'ss -ltnp'`) and retry.
* **`502 Bad Gateway` during handshake.** The sandbox has not finished booting the workload. Wait a few seconds and reconnect; the proxy returns 502 when nothing is listening on the remote port.
* **WebSocket auth failures.** Confirm `tl whoami` shows the right organization and project, or that `TENSORLAKE_API_KEY` is set in the shell that runs the CLI.
