Skip to content

Network & tunnels

A Modal Sandbox reaches the internet unless you limit it. Modal enforces the limit outside the container, so code inside can’t lift it. Traffic into a sandbox goes through tunnels: each port you open gets a URL of its own.

git clonepip installsession-samSandboxeverything elseagentyour codegithub.comallowedpypi.orgnot listed
With allowedDomains: ['github.com'], the sandbox reaches GitHub and nothing else. Modal stops the rest outside the container.
const sandbox = yield* sandboxes.create({ blockNetwork: true });

Nothing leaves the sandbox. Commands, files and tunnels still work, because they go through Modal.

const sandbox = yield* sandboxes.create({
allowedDomains: ["github.com", "registry.npmjs.org"],
cidrAllowlist: ["10.0.0.0/8"],
});

The sandbox reaches these domains and ranges, and nothing else.

Let an agent install its dependencies, then close the network before it runs the code it wrote:

yield* sandbox.exec(["pnpm", "install"], { cwd: "/work" });
yield* sandbox.setNetworkAccess({ blockNetwork: true });
yield* sandbox.exec(["node", "agent-output.mjs"], { cwd: "/work" });

The new policy replaces the old one, and Modal closes the connections it no longer allows. Pass an allow-list to narrow the network instead, or {} to open it again.

List the ports when you create the sandbox. Each gets a tunnel, and getTcpPort returns its public URL and a client for it:

const sandbox = yield* sandboxes.create({ encryptedPorts: [3000] });
const port = yield* sandbox.getTcpPort(3000);
const response = yield* port.fetch(HttpClientRequest.get("/health"));

port.url is the tunnel’s URL. A Worker in front of the sandbox can forward the request it received:

return yield* port.fetch(yield* HttpServerRequest.HttpServerRequest);

encryptedPorts terminate TLS at Modal. unencryptedPorts pass raw TCP, for a database or SSH; port.connect() opens a socket to either.

A tunnel URL is public. To hand one port to a browser that shouldn’t hold any Modal credentials, create a connect token for it:

const { url, token } = yield* sandbox.connectToken({ port: 8080, userMetadata: user });

The token opens that port and nothing else.

Some APIs only accept calls from known addresses. A Modal.Proxy gives sandboxes fixed outbound IPs:

src/network.ts
import * as Modal from "@alchemy.run/modal";
export const Egress = Modal.Proxy("Egress", { ips: 1 });

Read its id while the Worker is constructed, then pass it to create:

// construction phase
const egressId = yield* (yield* Egress).proxyId;
// in a handler
const sandbox = yield* sandboxes.create({ proxy: { proxyId: yield* egressId } });

The Proxy’s ips attribute lists the addresses to allow on the other side. Proxies need Modal’s Team plan.

A route’s redirect sends traffic to a Modal.Proxy or to a sidecar container, never to a URL of yours, so a gateway such as a Cloudflare Worker can’t sit on a sandbox’s egress path. To call one, have the code in the sandbox use the gateway’s URL as its base URL.

To add an API key to a sandbox’s requests without letting it see the key, see Secrets.