Sandbox-to-Sandbox Access
One sandbox can reach a service inside another sandbox over two data-plane paths: CubeProxy Host routing (the same ingress as the public URL) and the node host-port mapping (CubeVS eBPF DNAT). Option 1 names the target sandbox ID and port in the HTTP Host header (<port>-<sandboxID>.cube.app) and CubeProxy routes and proxies the request to the target sandbox; option 2 accesses the target sandbox's node IP and mapped port directly — nodeIP:hostPort.
Option 1 data flow (CubeProxy on a different machine from the sandboxes):
source sandbox A CubeProxy node target node
(source node) CubeProxy :80 primary NIC ingress
│ │ │
│ ① Host: 8080-B.cube.app │
│ (--resolve pins the CubeProxy node IP) │
│──────────────────────►│ │
│ │ ② hostIP:hostPort │
│ │───────────────────────────►│ from_world DNAT
│ │ │ to sandboxIP:8080
│ │ │──► target sandbox B :8080Option 2 data flow — same node (source node = target node):
source sandbox A from_cube primary NIC ingress
│ │ │
│ ① nodeIP:hostPort │ │
│────────────────────►│ ② SNAT, then software │
│ │ loopback: bpf_redirect │
│ │ (BPF_F_INGRESS) │
│ │───────────────────────────►│ from_world DNAT
│ │ │ to sandboxIP:8080
│ │ │──► target sandbox B :8080
Entire path stays on one node; no NIC/switch hairpinning required.Option 2 data flow — cross-node:
source sandbox A from_cube target node
(source node) (source node) primary NIC ingress
│ │ │
│ ① nodeIP:hostPort │ │
│────────────────────►│ ② SNAT, out the physical │
│ │ NIC ════════════════════►│ from_world DNAT
│ │ (physical link) │ to sandboxIP:8080
│ │ │──► target sandbox B :8080Runnable end-to-end examples live in examples/code-sandbox-quickstart: sandbox-curl-sandbox.py (option 1) and sandbox-port-mapping.py (option 2).
Choosing Between Them
| Dimension | Option 1: CubeProxy Host routing | Option 2: Host-port mapping |
|---|---|---|
| Protocols | HTTP / HTTPS / gRPC only (L7, Host-header routing) | Any TCP protocol (L4 DNAT) |
| Data plane | Via CubeProxy, one proxy hop | eBPF DNAT straight into the sandbox, no proxy hop |
| Ingress auth | Supports the per-sandbox token of allow_public_traffic=False | No token support; exposure is governed by network policy + expose-port |
| Service port source | The <port> prefix in the URL names B's container port | Query the info API for the hostIP:hostPort mapped to B's container port |
| Endpoint stability | Stable URL format, unchanged across pause/resume | hostIP:hostPort may change across pause/resume (see below) |
| DNS in sandbox | Not needed (--resolve pins the proxy IP) | Not needed (plain node IP) |
Typical scenarios:
- Option 1: an agent in one sandbox calls an HTTP API / gRPC service hosted in another; you want to reuse CubeProxy's token auth and a stable endpoint URL without querying the host port.
- Option 2: arbitrary TCP between sandboxes (databases, message queues, custom binary protocols); latency-sensitive paths that should bypass the proxy; ports can be declared in the template upfront.
Template build: declare the service port
Both options treat the template's --expose-port declarations as the service-port contract — the caller only needs to know B's container port and never has to know whether it is co-located with B. Append the service port to the standard template (8080 as an example; see the Quickstart for registry addresses, and use the cube-sandbox-cn registry in mainland China):
# 49999/49983 are the sandbox-code image's own code interpreter / envd ports;
# replace 8080 with your service port.
cubemastercli tpl create-from-image \
--image cube-sandbox-int.tencentcloudcr.com/cube-sandbox/sandbox-code:latest \
--writable-layer-size 1G \
--expose-port 49999 \
--expose-port 49983 \
--expose-port 8080 \
--probe 49999Notes:
--expose-portmay be repeated; only declared ports get a hostPort mapping (option 2 and option 1's cross-node path both rely on it).--probeis the template readiness probe port — 49999 forsandbox-code.- The command prints a
template_idand waits for the build to reachREADY;cubemastercli tpl listshows the status too. - Adding ports to an existing template means rebuilding it — expose-port is a template property and cannot be changed at runtime.
Create sandboxes with the new template ID; Cubelet programs the port mapping automatically at sandbox start, with no extra create-request parameters.
Option 1: CubeProxy Host Routing
CubeProxy routes by the <port>-<sandboxID>.<domain> Host header. Inside sandbox A, pin the name to the CubeProxy node IP with curl --resolve — no DNS needed:
curl -sS --resolve 8080-<sandboxID-B>.cube.app:80:<proxy-node-ip> \
http://8080-<sandboxID-B>.cube.app/Create B from a template that declares the service port as shown above. The caller names B's container port in the Host header: same-node, CubeProxy dials sandboxIP:containerPort directly; cross-node, it enters B through that port's hostIP:hostPort mapping (an undeclared port gets a 404 cross-node).
Implementation detail: same-node, CubeProxy's direct dial does not consult the port mapping, so undeclared ports happen to work — but that is lenient behavior that turns into a 404 as soon as A and B land on different nodes. Do not rely on it.
Properties:
- Same ingress as the public URL, so auth semantics are identical: when B was created with
network={"allow_public_traffic": False}, requests must carry thee2b-traffic-access-tokenheader (see Restrict Public Access). - Requires CubeProxy.
Limitations:
- L7 protocols only: HTTP / HTTPS / gRPC (gRPC rides HTTP/2, which the Host routing handles). Arbitrary TCP/UDP services (databases, Redis, custom protocols) cannot use this path — use option 2 instead.
- The service port should be declared via the template's
--expose-port(see "Template build: declare the service port" above).
Option 2: Node Host-Port Mapping (hostIP:hostPort)
When a template declares --expose-port, Cubelet programs a CubeVS port mapping at sandbox create: TCP traffic to <nodeIP>:<hostPort> is DNAT'd by the from_world eBPF program on the primary NIC ingress directly into the sandbox — no CubeProxy involved.
Prerequisites
- cubemaster
cubelet_conf.enable_exposed_port: true(default); - the template declares the service port (see "Template build: declare the service port" above).
Discovering the allocated host port
Host ports are allocated per sandbox. Query with the SDK (recommended):
endpoint = sandbox_b.get_exposed_port_endpoint(8080)
# "9.135.152.214:20006" — always the hostIP:hostPort formThe endpoint works for off-node and in-sandbox callers: same-node sandbox traffic is looped back through the primary NIC ingress by CubeVS hairpin. For processes running on the target node itself, use CubeProxy Host routing instead — host-local traffic does not traverse the from_world/hairpin DNAT path. The Go / Node SDKs expose the same method as GetExposedPortEndpoint / getExposedPortEndpoint.
Node version requirement: same-node sandbox-to-sandbox access requires a hairpin-capable CubeVS (shipped in the same release as this API). Older nodes have no same-node access at all (neither hostPort nor tap-direct); upgrade the nodes to gain it — no caller-side changes needed.
HTTP fallback (CubeAPI forwards to CubeMaster):
curl -s 'http://<cubeapi>:3000/sandboxes/<B>/exposed-ports/8080'
# → {"sandboxID":"<B>","containerPort":8080,"mode":"host_port",
# "endpoint":"9.135.152.214:20006","hostIP":"9.135.152.214","hostPort":20006}Accessing from sandbox A
curl -sS http://<nodeIP>:<hostPort>/When A and B are on the same node, A's packets are SNATed in from_cube and looped back into the node NIC ingress in software, which triggers the DNAT — this does not depend on NIC/switch hairpinning. Cross-node traffic (A and B on different nodes) traverses the wire to the same ingress path.
Properties:
- Direct data plane, no proxy hop; any TCP protocol works (HTTP, databases, message queues, custom binary protocols).
- Bypasses CubeProxy, so the
allow_public_traffictoken does not apply — exposure is governed by Network Policy plus the template's expose-port list.
Limitations:
- TCP only, no UDP.
- The service port must be declared via the template's
--expose-port. - The endpoint may change across pause/resume: host ports are allocated at sandbox create (including the internal re-create on resume), so after a pause → resume cycle
hostIP:hostPortcan differ (the hostIP changes too when the sandbox is rescheduled to another node). Callers must re-query the info API after B resumes — do not cache the endpoint long-term.
Alternative: control plane (SDK connect)
When the goal is to operate sandbox B from inside A (run commands, read and write files) rather than reach a service port, install the cubesandbox SDK in A and Sandbox.connect() to B:
from cubesandbox import Sandbox
sandbox_b = Sandbox.connect("<sandboxID-B>")
sandbox_b.commands.run("echo hello")Inside A, set CUBE_API_URL to a node-reachable CubeAPI address (a loopback 127.0.0.1 from your .env would point at the sandbox itself) and CUBE_PROXY_NODE_IP. Full example: sandbox-in-sandbox.py.
Testing
Both options ship runnable end-to-end demo scripts (in examples/code-sandbox-quickstart). Each script: creates sandboxes A and B → starts an HTTP service in B → curls it from A → asserts the response body:
sandbox-curl-sandbox.py— option 1 (service port fixed at 8080);sandbox-port-mapping.py— option 2 (override the port withCUBE_SERVICE_PORT, default 8080).
Both should use a template that declares the corresponding port (see "Template build" above), passed as CUBE_TEMPLATE_ID.
Setup, run commands, and expected outputs are documented in that directory's README. If the curl from A gets ECONNREFUSED (exit code 2), sandbox-port-mapping.py prints a troubleshooting checklist (port not exposed, enable_exposed_port off, egress policy blocking).
Common pitfalls
- Egress policy: if A uses
allow_out/deny_out(see Network Policy), the allowlist must cover the CubeAPI / CubeProxy / host node IP. - API address: for control-plane calls from inside a sandbox, loopback (
127.0.0.1) refers to the sandbox itself — replace it with a node-reachable address.
Further reading
- Network Policy — sandbox egress allow/deny lists and L7 rules.
- Restrict Public Access — per-sandbox tokens on the CubeProxy ingress.
- Security Proxy — domain filtering and credential injection for outbound HTTP/HTTPS.