Networking
Three ways to reach a port inside a sandbox: a TCP forward through the gateway (client.Tcp), an SSH relay (client.Ssh), and a service the gateway publishes (client.Services).
TCP forwarding
ForwardAsync opens a duplex ForwardStream to a loopback port inside the sandbox. One reader and one writer may be active at the same time; CompleteWriteAsync half-closes the stream so the remote side sees end-of-file while the response keeps flowing.
// A duplex stream to port 8080 inside the sandbox.
await using var stream = await client.Tcp.ForwardAsync("agent-1", 8080);
await stream.WriteAsync(Encoding.ASCII.GetBytes("GET /health HTTP/1.0\r\nHost: localhost\r\n\r\n"));
await stream.CompleteWriteAsync(); // half-close: the server sees EOF, the response keeps flowing
using var reader = new StreamReader(stream, Encoding.UTF8);
var response = await reader.ReadToEndAsync();
ListenAsync binds a local port (0 picks a free one) and forwards each accepted connection to the sandbox port, so ordinary clients such as HttpClient or a browser can connect. Dispose the listener to stop accepting and tear down the active connections.
// Binds a local port (0 picks a free one) and forwards each accepted connection to port 8080 in the sandbox.
await using var listener = await client.Tcp.ListenAsync("agent-1", remotePort: 8080, localPort: 0);
Console.WriteLine($"open http://{listener.LocalEndPoint}/");
using var http = new HttpClient();
var body = await http.GetStringAsync(new Uri($"http://{listener.LocalEndPoint}/health"));
ListenOptions sets the bind address (loopback by default), a service id for audit and correlation, and UseSshTunnel to carry the connections over SSH relays instead.
SSH
TunnelAsync opens the raw SSH byte stream to the sandbox, for an SSH client library to drive. It creates a session for the tunnel and revokes it when the stream is disposed.
// The raw SSH byte stream to the sandbox, for an SSH client library to drive. The tunnel creates its
// own session and revokes it when the stream is disposed.
await using (var tunnel = await client.Ssh.TunnelAsync("agent-1"))
{
// ... hand `tunnel` to your SSH library ...
}
For an external SSH client, create a session yourself. SshSession carries the gateway host, port and scheme, the host key fingerprint to pin, the expiry, and the token; the token is a credential and ToString redacts it. The response is validated before it is returned, because its fields end up in a ProxyCommand that a local shell runs.
// A short-lived session for an external SSH client. The token is a secret; ToString() redacts it.
var session = await client.Ssh.CreateSessionAsync("agent-1");
Console.WriteLine($"{session.GatewayScheme}://{session.GatewayHost}:{session.GatewayPort}, expires {session.ExpiresAt}");
Console.WriteLine($"pin host key {session.HostKeyFingerprint}");
// Revoke it as soon as it is no longer needed.
await client.Ssh.RevokeSessionAsync(session.Token);
Services
A service publishes a sandbox port through the gateway under a URL. Declare them when you create the sandbox (SandboxSpec.ServiceExposures), or expose them later:
// Expose port 8080 of the sandbox through the gateway.
var web = await client.Services.ExposeAsync("agent-1", "web", 8080, new ExposeServiceOptions
{
Domain = true,
AuthorizationMode = ServiceAuthorizationMode.Strip, // remove Authorization before forwarding
});
Console.WriteLine($"{web.Name}: {web.Url}");
await foreach (var service in client.Services.List("agent-1"))
{
Console.WriteLine($"{service.Name} -> {service.TargetPort}: {service.Url}");
}
await client.Services.DeleteAsync("agent-1", "web");
ExposeServiceOptions.Domain publishes the service on its own domain. AuthorizationMode decides what happens to the caller's Authorization header: Strip (the default) removes it before forwarding, and BearerPassthrough forwards one valid bearer header unchanged. Sandbox.ServiceUrls lists the URLs of a sandbox's services.
Multi-replica gateways
Forwards and SSH tunnels are routed to the replica that holds the sandbox's supervisor session. See Multi-replica gateways.