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

# Forward a box port to a local port

> Publish a port inside a Cloud box on an address on your own machine, so any TCP client can reach it without knowing about BoxLite.

Some clients cannot be taught to speak through an SDK — `curl`, a browser, a database driver, an existing library that only knows how to dial a socket. `forward()` publishes the tunnel on a local address so all of them just work.

## Prerequisites

* An API key from the console, exported as `BOXLITE_API_KEY`. See [API keys](/cloud/api-keys).
* `pip install boxlite`, and the REST URL exported as `BOXLITE_REST_URL`. See [Quickstart](/cloud/quickstart).
* A box you can start, and a service inside it that listens on a TCP port.

Every example reads both values from the environment, so nothing hard-codes a credential.

`forward()` publishes the tunnel on an address on your machine, so any TCP client — `curl`, a browser, a database driver, an existing library that only knows how to dial a socket — can reach the service without knowing about BoxLite.

```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
# cloud_tunnel_forward.py — publish a box port on a local port
# Run: python cloud_tunnel_forward.py
import asyncio
import os
import time
import urllib.request

from boxlite import (
    ApiKeyCredential,
    Boxlite,
    BoxliteRestOptions,
    BoxOptions,
    SocketAddress,
)

GUEST_PORT = 18080
IMAGE = "ghcr.io/boxlite-ai/boxlite-agent-base:v0.1.0"


async def main() -> None:
    # export BOXLITE_API_KEY=<YOUR_API_KEY> before running
    rt = Boxlite.rest(BoxliteRestOptions(
        url=os.environ.get("BOXLITE_REST_URL", "https://api.boxlite.ai"),
        credential=ApiKeyCredential(os.environ["BOXLITE_API_KEY"]),
    ))

    box_id = None
    forwarder = None
    try:
        box = await rt.create(
            BoxOptions(image=IMAGE),
            name=f"tunnel-forward-{int(time.time())}",
        )
        box_id = box.id
        await box.start()

        launch = await box.exec("sh", args=[
            "-c",
            f"nohup python3 -m http.server {GUEST_PORT} --bind 0.0.0.0 "
            "> /tmp/server.log 2>&1 & echo launched",
        ])
        await launch.wait()
        await asyncio.sleep(2)  # give the server time to bind

        # host must be a numeric IP; port=0 asks the OS for a free port.
        listener = SocketAddress.tcp(host="127.0.0.1", port=0)

        tunnel = await box.network.tunnel(GUEST_PORT)
        forwarder = await tunnel.forward(listener)  # consumes the tunnel

        local = forwarder.local_addr()  # synchronous
        print(f"forwarding 127.0.0.1:{local.port} -> box port {GUEST_PORT}")

        # Any TCP client can now dial the local address. urllib is blocking,
        # so run it off the event loop.
        def fetch() -> int:
            url = f"http://127.0.0.1:{local.port}/"
            with urllib.request.urlopen(url, timeout=10) as resp:
                return resp.status

        print(f"local request status: {await asyncio.to_thread(fetch)}")
    except Exception as exc:
        print(f"forward demo failed: {type(exc).__name__}: {exc}")
    finally:
        if forwarder is not None:
            try:
                await forwarder.close()
            except Exception as exc:
                print(f"forwarder close failed: {exc}")
        if box_id:
            try:
                await rt.remove(box_id, force=True)
            except Exception as exc:
                print(f"remove failed: {exc}")


if __name__ == "__main__":
    asyncio.run(main())
```

Three constraints on the listening address:

* The host must be a **numeric IP**. `SocketAddress.tcp(host="localhost")` raises `ValueError: tunnel listener host must be a numeric IP` — pass `"127.0.0.1"`.
* `port=0` is the default and asks the operating system for a free port. Read the port you actually got from `forwarder.local_addr().port`.
* A Unix socket path must be **absolute**. `SocketAddress.unix("relative.sock")` raises `ValueError: tunnel Unix socket path must be absolute`.

`await forwarder.wait()` blocks until the forwarder finishes, which is what you await in a long-running process instead of exiting. `await forwarder.close()` shuts it down. The forwarder is built from a tunnel, and `forward()` spends that tunnel, so build each forwarder from its own fresh `await box.network.tunnel(port)`.

## Next steps

<CardGroup cols={2}>
  <Card title="Network" icon="network-wired" href="/cloud/network">
    How tunnels work, the full parameter reference, and the outbound boundary.
  </Card>

  <Card title="Network policy" icon="shield-halved" href="/cloud/network-policy">
    Control what the box itself is allowed to reach.
  </Card>
</CardGroup>
