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

# Networking

> Expose HTTP services, raw TCP ports, and SSH from a Sailbox

Sailboxes are closed to inbound traffic by default for security.

## Inbound access control

To expose a service at creation time, pass the guest port in the create
request, start a process that listens on that port, and wait for the listener
endpoint to become ready:

<CodeGroup>
  ```python Python theme={null}
  import sail

  app = sail.App.find(name="web-demo", mint_if_missing=True)
  sb = sail.Sailbox.create(
      app=app,
      name="web-demo",
      ingress_ports=[3000],
  )

  sb.exec("python3 -m http.server 3000", background=True).wait()
  print(sb.wait_for_listener(3000, timeout=60).endpoint.url)
  ```

  ```typescript TypeScript theme={null}
  import { App, Sailbox } from "@sailresearch/sdk";

  const app = await App.find("web-demo", { mintIfMissing: true });
  const sb = await Sailbox.create({
    app,
    name: "web-demo",
    ingressPorts: [{ guestPort: 3000, protocol: "http" }],
  });

  await (
    await sb.exec("python3 -m http.server 3000", { background: true })
  ).wait();
  const listener = await sb.waitForListener(3000, { timeoutSeconds: 60 });
  if (listener.endpoint?.kind === "http") {
    console.log(listener.endpoint.url);
  }
  ```

  ```rust Rust theme={null}
  use sail::{
      BaseImage,
      CreateSailboxRequest,
      ExecOptions,
      ImageArchitecture,
      ImageSpec,
      IngressPort,
      IngressProtocol,
  };

  let app = client.find_app("web-demo", /* mint_if_missing */ true).await?;
  let sb = client
      .create_sailbox(
          &CreateSailboxRequest {
              app_id: app.id,
              name: "web-demo".into(),
              image: ImageSpec {
                  base: Some(BaseImage::Debian),
                  architecture: ImageArchitecture::Arm64,
                  ..Default::default()
              },
              ingress_ports: vec![IngressPort {
                  guest_port: 3000,
                  protocol: IngressProtocol::Http,
                  allowlist: Vec::new(),
              }],
              ..Default::default()
          },
          /* timeout */ None,
      )
      .await?;

  let serve = sb
      .exec_shell(
          "python3 -m http.server 3000",
          ExecOptions {
              background: true,
              ..Default::default()
          },
      )
      .await?;
  serve.wait().await?;

  use sail::{ListenerEndpoint, WaitForListenerOptions};

  let listener = sb
      .wait_for_listener(3000, WaitForListenerOptions::default())
      .await?;
  if let Some(ListenerEndpoint::Http { url }) = listener.endpoint() {
      println!("{url}");
  }
  ```
</CodeGroup>

You can also add and remove ports on a running Sailbox with `expose`/`unexpose`
(see [Add or remove ports at runtime](#add-or-remove-ports-at-runtime)).

Sail supports two inbound protocols:

* HTTP, which returns a public HTTPS URL and supports HTTP and WebSocket traffic.
* Raw TCP, which returns a public `host` and `port` for protocols such as SSH,
  Postgres, or custom TCP servers.

Terminating a Sailbox also removes all of its listeners.

## HTTP and WebSocket services

Expose a guest port over HTTP (in Python, a bare port number in
`ingress_ports` means HTTP). The listener resolves to a routable HTTPS URL.

<CodeGroup>
  ```python Python theme={null}
  import sail

  app = sail.App.find(name="web-demo", mint_if_missing=True)

  sb = sail.Sailbox.create(
      app=app,
      name="web-demo",
      ingress_ports=[3000],
  )

  sb.exec("mkdir -p /srv/app && echo hello > /srv/app/index.html").wait()
  sb.exec("python3 -m http.server 3000", background=True, cwd="/srv/app").wait()

  listener = sb.wait_for_listener(3000, timeout=60)
  print(listener.endpoint.url)
  ```

  ```typescript TypeScript theme={null}
  import { App, Sailbox } from "@sailresearch/sdk";

  const app = await App.find("web-demo", { mintIfMissing: true });

  const sb = await Sailbox.create({
    app,
    name: "web-demo",
    ingressPorts: [{ guestPort: 3000, protocol: "http" }],
  });

  await (
    await sb.exec("mkdir -p /srv/app && echo hello > /srv/app/index.html")
  ).wait();
  await (
    await sb.exec("python3 -m http.server 3000", {
      background: true,
      cwd: "/srv/app",
    })
  ).wait();

  const listener = await sb.waitForListener(3000, { timeoutSeconds: 60 });
  if (listener.endpoint?.kind === "http") {
    console.log(listener.endpoint.url);
  }
  ```

  ```rust Rust theme={null}
  use sail::{
      BaseImage,
      CreateSailboxRequest,
      ExecOptions,
      ImageArchitecture,
      ImageSpec,
      IngressPort,
      IngressProtocol,
  };

  let app = client.find_app("web-demo", /* mint_if_missing */ true).await?;

  let sb = client
      .create_sailbox(
          &CreateSailboxRequest {
              app_id: app.id,
              name: "web-demo".into(),
              image: ImageSpec {
                  base: Some(BaseImage::Debian),
                  architecture: ImageArchitecture::Arm64,
                  ..Default::default()
              },
              ingress_ports: vec![IngressPort {
                  guest_port: 3000,
                  protocol: IngressProtocol::Http,
                  allowlist: Vec::new(),
              }],
              ..Default::default()
          },
          /* timeout */ None,
      )
      .await?;

  let setup = sb
      .exec_shell(
          "mkdir -p /srv/app && echo hello > /srv/app/index.html",
          ExecOptions::default(),
      )
      .await?;
  setup.wait().await?;
  let serve = sb
      .exec_shell(
          "python3 -m http.server 3000",
          ExecOptions {
              cwd: Some("/srv/app".to_string()),
              background: true,
              ..Default::default()
          },
      )
      .await?;
  serve.wait().await?;

  use sail::{ListenerEndpoint, WaitForListenerOptions};

  let listener = sb
      .wait_for_listener(3000, WaitForListenerOptions::default())
      .await?;
  if let Some(ListenerEndpoint::Http { url }) = listener.endpoint() {
      println!("{url}");
  }
  ```
</CodeGroup>

Use the same URL for WebSocket clients by replacing `https://` with `wss://`
when the process inside the Sailbox speaks WebSocket on that port.

<CodeGroup>
  ```python Python theme={null}
  ws_url = listener.endpoint.url.replace("https://", "wss://")
  ```

  ```typescript TypeScript theme={null}
  const wsUrl =
    listener.endpoint?.kind === "http"
      ? listener.endpoint.url.replace("https://", "wss://")
      : undefined;
  ```

  ```rust Rust theme={null}
  use sail::ListenerEndpoint;

  let ws_url = match listener.endpoint() {
      Some(ListenerEndpoint::Http { url }) => url.replace("https://", "wss://"),
      _ => String::new(),
  };
  ```
</CodeGroup>

## Raw TCP ports

Expose a port as raw TCP instead of HTTP when the protocol is not HTTP-aware or
the service already implements its own authentication. The listener resolves to
a public `host` and `port` that any TCP client can dial directly.

<CodeGroup>
  ```python Python theme={null}
  sb = sail.Sailbox.create(
      app=app,
      name="tcp-demo",
      ingress_ports=[sail.IngressPort(5432, "tcp", allowlist=["203.0.113.0/24"])],
  )
  ```

  ```typescript TypeScript theme={null}
  const sb = await Sailbox.create({
    app,
    name: "tcp-demo",
    ingressPorts: [
      { guestPort: 5432, protocol: "tcp", allowlist: ["203.0.113.0/24"] },
    ],
  });
  ```

  ```rust Rust theme={null}
  use sail::{
      BaseImage,
      CreateSailboxRequest,
      ImageArchitecture,
      ImageSpec,
      IngressPort,
      IngressProtocol,
  };

  let sb = client
      .create_sailbox(
          &CreateSailboxRequest {
              app_id: app.id,
              name: "tcp-demo".into(),
              image: ImageSpec {
                  base: Some(BaseImage::Debian),
                  architecture: ImageArchitecture::Arm64,
                  ..Default::default()
              },
              ingress_ports: vec![IngressPort {
                  guest_port: 5432,
                  protocol: IngressProtocol::Tcp,
                  allowlist: vec!["203.0.113.0/24".to_string()],
              }],
              ..Default::default()
          },
          /* timeout */ None,
      )
      .await?;
  ```
</CodeGroup>

After the service starts, wait for the endpoint and connect with the matching
client:

<CodeGroup>
  ```python Python theme={null}
  listener = sb.wait_for_listener(5432, timeout=60)
  host, port = listener.endpoint.host, listener.endpoint.port
  print(f"postgres://user:password@{host}:{port}/postgres")
  ```

  ```typescript TypeScript theme={null}
  const listener = await sb.waitForListener(5432, { timeoutSeconds: 60 });
  if (listener.endpoint?.kind === "tcp") {
    const { host, port } = listener.endpoint;
    console.log(`postgres://user:password@${host}:${port}/postgres`);
  }
  ```

  ```rust Rust theme={null}
  use sail::{ListenerEndpoint, WaitForListenerOptions};

  let listener = sb
      .wait_for_listener(5432, WaitForListenerOptions::default())
      .await?;
  if let Some(ListenerEndpoint::Tcp { host, port }) = listener.endpoint() {
      println!("postgres://user:password@{host}:{port}/postgres");
  }
  ```
</CodeGroup>

## SSH access

For a quick interactive shell, use [`Sailbox.shell()`](/sailbox-sdk#shell) or
`sail box shell`: it streams a terminal over the same channel as `exec`, uses
no ingress port, and while open it forwards the box's localhost servers and
browser opens to your machine. Set up SSH when you want a standard SSH endpoint: a devbox,
your own client, `scp`, or port forwarding. It exposes guest port 22 as raw
TCP, which counts against your org's raw-TCP endpoint limit.

SSH access is organization-scoped: a box trusts your org's SSH certificate
authority, so anyone in the org can connect with a short-lived certificate
signed for their key. There are no per-box keys to manage. A private box is
the exception: its sshd accepts only its creator's certificates.

Enable SSH at create time, or with
[`enable_ssh()`](/sailbox-sdk#sailbox-enable-ssh) on a Sailbox that already
exists. Both install the org CA as trusted, start `sshd`, and expose port 22.
`enable_ssh()` is idempotent; re-run it to bring `sshd` back up if it stops.

<CodeGroup>
  ```python Python theme={null}
  import sail

  sb = sail.Sailbox.create(
      app=app,
      name="ssh-demo",
      ssh=True,
  )
  ```

  ```typescript TypeScript theme={null}
  const sb = await Sailbox.create({
    app,
    name: "ssh-demo",
    ssh: true,
  });
  ```

  ```rust Rust theme={null}
  use sail::{BaseImage, CreateSailboxRequest, ImageArchitecture, ImageSpec};

  let sb = client
      .create_sailbox(
          &CreateSailboxRequest {
              app_id: app.id,
              name: "ssh-demo".into(),
              image: ImageSpec {
                  base: Some(BaseImage::Debian),
                  architecture: ImageArchitecture::Arm64,
                  ..Default::default()
              },
              ssh: true,
              ..Default::default()
          },
          /* timeout */ None,
      )
      .await?;
  ```
</CodeGroup>

To connect, wire up your machine with the `sail box ssh` CLI and use the box's
`<name>.sail` shortcut:

```bash theme={null}
sail box ssh alias <sailbox-id>
ssh ssh-demo.sail
```

`alias` fetches your certificate and writes the shortcut into your SSH config.
The shortcut is what presents the certificate, so alias a box before you
connect. Run it on each machine you connect from, whether you or a teammate
enabled the box; it only touches local config and never wakes or changes the
box. `sail box ssh enable <id>` enables SSH on an existing box and runs
`alias` for you in one step, as does creating a box with
`sail box create --enable-ssh`.

To restrict which sources may connect, pass an allowlist of CIDR prefixes:
`sb.enable_ssh(allowlist=["203.0.113.0/24"])` in Python,
`box.enableSsh({ allowlist: ["203.0.113.0/24"] })` in TypeScript, the
`allowlist` argument of `enable_ssh` in Rust, or `sail box ssh enable <id> --allowlist 203.0.113.0/24` from the CLI. A new allowlist replaces the current
one, so re-running can tighten or relax access. Disabling SSH
(`sail box ssh disable`) removes the port-22 listener along with its
restriction.

The SSH server persists across sleep and resume. An open SSH session drops when
the Sailbox sleeps, but reconnecting with `ssh <name>.sail` wakes it and uses
the same host key.

## Inspect endpoints

Look up one listener when you know the guest port, or list all published ports
for a Sailbox:

<CodeGroup>
  ```python Python theme={null}
  for listener in sb.listeners():
      listener = sb.wait_for_listener(listener.guest_port, timeout=60)
      if listener.protocol == "http":
          print(listener.guest_port, listener.endpoint.url)
      else:
          endpoint = listener.endpoint
          print(listener.guest_port, f"{endpoint.host}:{endpoint.port}")
  ```

  ```typescript TypeScript theme={null}
  for (const { guestPort } of await sb.listeners()) {
    const listener = await sb.waitForListener(guestPort, { timeoutSeconds: 60 });
    switch (listener.endpoint?.kind) {
      case "http":
        console.log(guestPort, listener.endpoint.url);
        break;
      case "tcp":
        console.log(
          guestPort,
          `${listener.endpoint.host}:${listener.endpoint.port}`,
        );
        break;
    }
  }
  ```

  ```rust Rust theme={null}
  use sail::ListenerEndpoint;

  for listener in sb.listeners().await? {
      match listener.endpoint() {
          Some(ListenerEndpoint::Http { url }) => println!("{} {url}", listener.guest_port),
          Some(ListenerEndpoint::Tcp { host, port }) => {
              println!("{} {host}:{port}", listener.guest_port)
          }
          None => println!("{} (not routable yet)", listener.guest_port),
      }
  }
  ```
</CodeGroup>

HTTP listeners expose a public URL. TCP listeners expose a public `host` and
`port`.

## Add or remove ports at runtime

You don't have to declare every port at create time. `expose` publishes a new
port on a running Sailbox and `unexpose` removes one, with no guest restart.
The listener returned by `expose` carries the resolved endpoint but an
`"unknown"` route status: the response confirms configuration, not
reachability. `wait_for_listener` confirms the route is live.

<CodeGroup>
  ```python Python theme={null}
  # Add an HTTP port and read its URL.
  sb.expose(8080)
  print(sb.wait_for_listener(8080, timeout=60).endpoint.url)

  # Add a CIDR-restricted raw-TCP port, then remove it when you're done.
  sb.expose(5432, protocol="tcp", allowlist=["203.0.113.0/24"])
  sb.unexpose(5432)
  ```

  ```typescript TypeScript theme={null}
  // Add an HTTP port and read its URL.
  await sb.expose(8080);
  const listener = await sb.waitForListener(8080, { timeoutSeconds: 60 });
  if (listener.endpoint?.kind === "http") {
    console.log(listener.endpoint.url);
  }

  // Add a CIDR-restricted raw-TCP port, then remove it when you're done.
  await sb.expose(5432, { protocol: "tcp", allowlist: ["203.0.113.0/24"] });
  await sb.unexpose(5432);
  ```

  ```rust Rust theme={null}
  use sail::{IngressProtocol, ListenerEndpoint, WaitForListenerOptions};

  // Add an HTTP port and read its URL once routable.
  sb.expose(8080, IngressProtocol::Http, &[]).await?;
  let listener = sb
      .wait_for_listener(8080, WaitForListenerOptions::default())
      .await?;
  if let Some(ListenerEndpoint::Http { url }) = listener.endpoint() {
      println!("{url}");
  }

  // Add a CIDR-restricted raw-TCP port, then remove it when you're done.
  sb.expose(5432, IngressProtocol::Tcp, &["203.0.113.0/24".to_string()])
      .await?;
  sb.unexpose(5432).await?;
  ```
</CodeGroup>

Re-exposing a port under the same protocol updates its `allowlist` to the value
you pass. Use it to tighten or relax a live port. Unexposing a raw-TCP port
stops serving it, so it no longer counts against your org's raw-TCP endpoint
limit. Its public `host:port`
stays owned by your org. Sail may reassign an idle address to another of your
Sailboxes; it is never given to another org. If the address has not been
reassigned, re-exposing the same guest port reclaims it exactly. Otherwise the
re-expose allocates a new address, so read the endpoint from the response
instead of assuming the old one. A stale client that dials an old raw-TCP
address may therefore reach a different Sailbox in your org, and never another
org's. A raw-TCP guest port can't be repurposed to HTTP; use a different guest
port. HTTP ports carry no such reservation and are released on `unexpose`.

`expose` and `unexpose` work on a paused or sleeping Sailbox without waking it; a
later resume serves the new listener.

From the CLI:

```bash theme={null}
sail box expose <id> 8080
sail box expose <id> 5432 --tcp --allowlist 203.0.113.0/24
sail box unexpose <id> 5432
```

## Access controls

<Warning>
  A raw-TCP port is reachable from the public internet with no platform-side
  authentication. The in-guest daemon, such as `sshd`, is the only access
  control, so make sure it requires credentials.
</Warning>

To restrict which sources may connect, pass `allowlist`. Entries that parse as
CIDR prefixes match source IPs on HTTP and TCP listeners. Other entries are
treated as Sail app names for authenticated Sailbox-origin traffic, supported
on HTTP listeners:

<CodeGroup>
  ```python Python theme={null}
  ingress_ports=[
      sail.IngressPort(5432, "tcp", allowlist=["203.0.113.0/24"]),
      sail.IngressPort(8080, allowlist=["203.0.113.0/24", "admin-tools"]),
  ]
  ```

  ```typescript TypeScript theme={null}
  import type { IngressPortInput } from "@sailresearch/sdk";

  const ingressPorts: IngressPortInput[] = [
    { guestPort: 5432, protocol: "tcp", allowlist: ["203.0.113.0/24"] },
    {
      guestPort: 8080,
      protocol: "http",
      allowlist: ["203.0.113.0/24", "admin-tools"],
    },
  ];
  ```

  ```rust Rust theme={null}
  use sail::{IngressPort, IngressProtocol};

  let ingress_ports = vec![
      IngressPort {
          guest_port: 5432,
          protocol: IngressProtocol::Tcp,
          allowlist: vec!["203.0.113.0/24".to_string()],
      },
      IngressPort {
          guest_port: 8080,
          protocol: IngressProtocol::Http,
          allowlist: vec!["203.0.113.0/24".to_string(), "admin-tools".to_string()],
      },
  ];
  ```
</CodeGroup>

Connections from outside the listed CIDR prefixes, or from authenticated
Sailbox-origin requests whose app name is not listed, fail before reaching the
guest. App names do not need to exist when you configure the listener.
Cross-organization app-name matches are denied. An empty or omitted `allowlist`
means any source may connect.

For HTTP requests from one Sailbox to an app-name allowlisted listener, include
the SDK-provided source headers. Inside the calling Sailbox, the Python SDK
reads its own identity:

```python theme={null}
import requests
import sail

requests.get(listener.endpoint.url, headers=sail.ingress_auth_headers())
```

From outside a Sailbox, such as an orchestrator or test driving Sailboxes
from the host, fetch the same headers for a specific live Sailbox you own
(requires an organization-scoped API key):

<CodeGroup>
  ```python Python theme={null}
  headers = source_sb.ingress_auth_headers()
  requests.get(listener.endpoint.url, headers=headers)
  ```

  ```typescript TypeScript theme={null}
  const headers = await sourceSb.ingressAuthHeaders();
  if (listener.endpoint?.kind === "http") {
    await fetch(listener.endpoint.url, { headers });
  }
  ```

  ```rust Rust theme={null}
  // Attach these header pairs to the HTTP client you use to call the listener.
  let headers = source_sb.ingress_auth_headers().await?;
  for (name, value) in &headers {
      println!("{name}: {value}");
  }
  ```
</CodeGroup>

Raw-TCP connections do not carry source app identity, so app-name entries are
rejected on `"tcp"` listeners: a raw-TCP `allowlist` must contain only CIDR prefixes.

Exposing a well-known unauthenticated service port, such as Postgres, MySQL, or
Redis, as raw TCP without an explicit `allowlist` is rejected. Set source
restrictions, or use the all-sources CIDR allowlist to confirm you want it
publicly reachable:

<CodeGroup>
  ```python Python theme={null}
  ingress_ports=[
      sail.IngressPort(5432, "tcp", allowlist=["0.0.0.0/0", "::/0"]),
  ]
  ```

  ```typescript TypeScript theme={null}
  import type { IngressPortInput } from "@sailresearch/sdk";

  const ingressPorts: IngressPortInput[] = [
    { guestPort: 5432, protocol: "tcp", allowlist: ["0.0.0.0/0", "::/0"] },
  ];
  ```

  ```rust Rust theme={null}
  use sail::{IngressPort, IngressProtocol};

  let ingress_ports = vec![IngressPort {
      guest_port: 5432,
      protocol: IngressProtocol::Tcp,
      allowlist: vec!["0.0.0.0/0".to_string(), "::/0".to_string()],
  }];
  ```
</CodeGroup>

## Sleeping services

Sleeping Sailboxes wake on network ingress. If you call `sleep()` on a Sailbox
with exposed listeners, the next inbound HTTP, WebSocket, or TCP connection
wakes the VM before forwarding traffic to the guest process.

<CodeGroup>
  ```python Python theme={null}
  sb.sleep()
  # A later request to listener.endpoint.url wakes the Sailbox.
  ```

  ```typescript TypeScript theme={null}
  await sb.sleep();
  // A later request to the listener's URL wakes the Sailbox.
  ```

  ```rust Rust theme={null}
  sb.sleep(/* wake_at */ None).await?;
  // A later request to the listener's URL wakes the Sailbox.
  ```
</CodeGroup>

Use `pause()` instead when you want to preserve VM state without waking on
network traffic.

## Ports and cleanup

Ports must be unique within a Sailbox and between `1` and `65535`. Ports `10000`,
`10001`, `15001`, and `15002` are reserved by Sail. Port `22` is the SSH port
and cannot be exposed as HTTP; expose it as raw TCP instead.

Each org can hold a limited number (32) of concurrent raw-TCP endpoints. The
limit counts actively-exposed endpoints, so `unexpose` frees a slot. An idle
`host:port` stays owned by your org, even after the Sailbox that used it
terminates. Sail may reassign it to another of your Sailboxes; another org
never receives it. Contact us to raise your limit if you need more concurrent
endpoints.
