Prerequisites
- An API key from the console, exported as
BOXLITE_API_KEY. See API keys. pip install boxlite, and the REST URL exported asBOXLITE_REST_URL. See Quickstart.- A box you can start, and a service inside it that listens on a TCP port.
Inbound: reach a service inside the box
How a tunnel reaches your service
You do not open a port on the platform. You ask one specific box for a tunnel to one specific port inside it, andawait box.network.tunnel(port) prepares it.
Behind that single call: the SDK asks the Cloud API to prepare the tunnel (POST /v1/boxes/{id}/network/tunnel if you are driving the REST API yourself), opens a TLS connection to the public proxy, and issues an HTTP CONNECT. The proxy connects on to the runner hosting your box, and the runner connects to the guest port. From there the path is your service’s socket: the tunnel moves bytes and nothing along it interprets your protocol.
Two consequences shape everything below.
- Your service must bind
0.0.0.0. The tunnel terminates on the box’s network interface, so a server bound to127.0.0.1inside the box is unreachable — the same reason port forwarding needs0.0.0.0on a box you run yourself, explained in Network access. - A prepared tunnel carries exactly one connection. See A tunnel is one-shot.
Serve HTTP from a box and get its URL
This script creates a box, starts an HTTP server inside it, prints the tunnel’s public URL, makes a request over the tunnel, and removes the box infinally.
uri() returns the public URL of a tunnel served remotely, which is what a Cloud tunnel is. On a local runtime the same call returns None, because a local tunnel is already a live connection with no address to publish. Reading uri() leaves the tunnel usable — only connect() and forward() spend it.
The snippet starts the server with python3. If the server log says python3: not found, install an interpreter into the box first, or start a service the image already ships.
A tunnel is one-shot
Preparing a tunnel is cheap and boxes accept several at once, so this is a shape to lean into rather than work around: many concurrent clients on the same guest port each get their own tunnel, and different guest ports on one box can be tunnelled at the same time.Forward a box port to a local port
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.
- The host must be a numeric IP.
SocketAddress.tcp(host="localhost")raisesValueError: tunnel listener host must be a numeric IP— pass"127.0.0.1". port=0is the default and asks the operating system for a free port. Read the port you actually got fromforwarder.local_addr().port.- A Unix socket path must be absolute.
SocketAddress.unix("relative.sock")raisesValueError: 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).
Read and write raw bytes
connect() hands you the byte stream directly. This is the level you work at for a protocol that is not HTTP — a line protocol, a binary framing, a database wire protocol — or when you want full control over what goes on the wire.
read(max_bytes) returns up to max_bytes bytes and an empty bytes object once the far side has closed. max_bytes of 0 raises ValueError: max_bytes must be non-zero, and reading after close() raises a BoxLite error carrying connection is closed.
A synchronous surface exists as well: SyncBox.network and SyncBox.tunnel(port) return the SyncNetworkHandle and SyncTunnelForwarder equivalents of the classes documented here.
Parameters and returns
box.network.tunnel(port)
Async. Prepares one tunnel to one port inside one box.
Returns a
BoxTunnel. await box.tunnel(port) is an equivalent shorthand on a SimpleBox.
BoxTunnel
SocketAddress
Two class methods build the address forward() listens on.
Read-only attributes:
TunnelForwarder
BoxConnection
What a tunnel carries
These behaviours are verified end to end against BoxLite Cloud, so you can build on them:- HTTP requests and responses,
GETandPOST. - WebSocket — the upgrade handshake and frames in both directions.
- Several guest ports on one box, tunnelled at the same time.
- Concurrent clients against the same guest port, each on its own tunnel.
- Responses larger than 2 MiB through a single connection, byte-for-byte intact.
- Slow readers — a client that drains the stream slowly does not lose data.
- Client cancellation — abandoning your side ends that stream without disturbing the box or its other streams.
- Service restart — restart the server inside the box, open a fresh tunnel, and traffic flows again.
- Arbitrary TCP, including a real SSH session to an
sshdlistening on port 2222. The path is not HTTP-specific.
Limits
Three properties of this path to design around. ACONNECT to a stopped box can be accepted before the stream fails. A successful connect is not proof that the box is running — the accept can land and the stream fail immediately afterwards. Treat your first successful read or write as the readiness signal. Tunnel traffic also does not wake a stopped box: the console is explicit that preview traffic keeps a running box alive but does not wake a stopped one, so start the box yourself and check Stop when idle if it stops under you.
A response can be lost after shutdown_write(). TCP half-close is not carried end to end, so a guest that waits for end-of-input before it replies may never reply, and a reply already in flight can be dropped. Use a protocol that frames its own messages — a newline, a length prefix, Content-Length — instead of one that signals “done” with EOF.
Direct browser use of a tunnel URL is not covered. The URL from uri() is the address the SDK dials when you call connect() or forward(). Navigating to it in a browser, and browser authentication for a private box, sit outside the verified path. Drive tunnels from the SDK, and when you want a browser on the service, forward the port to your machine as shown above.
Outbound: what the box can reach
Inbound and outbound are separate controls. The outbound side of a box’s network boundary is expressed withNetworkSpec, passed as the network field of BoxOptions: mode decides whether the box has a network interface at all, and allow_net narrows egress to a list of hosts.
One detail about that allowlist saves a debugging session. A blocked host is a DNS sinkhole, not a connection error: a host that is not on the list resolves to 0.0.0.0. Your code sees a connection to 0.0.0.0 fail, and a nslookup of the blocked host can still exit 0. So when you check whether a host was blocked, inspect the resolved address rather than the exit code.
The NetworkSpec parameter table, the three useful mode and allow_net combinations, and the verification recipe live on Network access. That page owns them, and this page does not restate them.
Troubleshooting
Next steps
Boxes on Cloud
Images, sizes, and the lifecycle controls that decide whether your service is still listening.
Quickstart
The five-call Cloud lifecycle, from API key to teardown.
Network access
The
NetworkSpec and ports parameter tables, and the egress allowlist in full.
