Documentation
¶
Overview ¶
Package sandbox defines the openblox contract for running untrusted, machine-generated code in disposable Linux sandboxes.
This package holds only interfaces, value types, options, and errors. Implementations live in sibling packages — see github.com/blox-eng/openblox/pkg/docker for the Docker + gVisor backend.
Secure by default ¶
The zero value of every option is the safe one. A sandbox created with no options has no network, dropped capabilities, a read-only root filesystem, a non-root user, and bounded CPU, memory, and process count:
sb, err := backend.Create(ctx, "session-1") // locked down sb, err := backend.Create(ctx, "session-1", WithEgress(policy)) // deliberately not
This inverts the common design where a permissive default is hardened by remembering the right flags. Forgetting an option here can only make a sandbox more restrictive, never less.
Naming and identity ¶
Sandboxes are addressed by a caller-supplied name, and two calls with the same name reach the same sandbox until it is reaped. openblox does not interpret the name: multi-tenancy, per-user scoping, and access control belong to the caller. Callers separating untrusted principals should hash their identity into the name rather than passing it through, since the name reaches the container runtime.
Index ¶
- Constants
- Variables
- type Backend
- type Command
- type CreateOption
- func WithCommandTimeouts(def, ceiling time.Duration) CreateOption
- func WithEgress(p EgressPolicy) CreateOption
- func WithEnv(entries ...string) CreateOption
- func WithImage(ref string) CreateOption
- func WithLabel(key, value string) CreateOption
- func WithLifetime(l Lifetime) CreateOption
- func WithResources(r Resources) CreateOption
- func WithRuntime(name string) CreateOption
- func WithUser(user string) CreateOption
- type EgressPolicy
- type Info
- type Lifetime
- type Preview
- type Resources
- type Result
- type Sandbox
- type Spec
- type State
Constants ¶
const ( DefaultRuntime = "runsc" DefaultUser = "1000:1000" DefaultCPUs = 2 DefaultMemoryBytes = 2 << 30 // 2 GiB // DefaultDiskBytes is scratch space, drawn from the memory budget because it // is tmpfs. Half of memory leaves room for the processes doing the writing. DefaultDiskBytes = 1 << 30 // 1 GiB DefaultMaxProcesses = 256 DefaultIdleTimeout = 15 * time.Minute DefaultMaxAge = 2 * time.Hour DefaultCommandTimeout = 60 * time.Second // MaxCommandTimeout bounds what a caller may request. Long enough for heavy // domain compute (large-model geometry, big spreadsheet parses) without // letting a wedged call hold a slot indefinitely. MaxCommandTimeout = 10 * time.Minute )
Defaults for a sandbox created with no options. Conservative on purpose: raising a limit is a visible edit at the call site, and a caller who forgets an option gets the restrictive behaviour rather than the permissive one.
Variables ¶
var ( // ErrNotFound means no sandbox exists under the requested name. ErrNotFound = errors.New("sandbox not found") // ErrInvalid means the request was malformed and retrying will not help. ErrInvalid = errors.New("invalid request") // ErrTimeout means a command exceeded its deadline and was killed. The // sandbox itself remains usable. ErrTimeout = errors.New("timed out") // isolation — typically that the gVisor runtime is not installed. // // Backends must fail with this rather than silently falling back to weaker // isolation. A sandbox that is quietly less isolated than requested is worse // than no sandbox, because the caller keeps trusting it. ErrRuntimeUnavailable = errors.New("required runtime unavailable") // absent locally and could not be pulled. Distinct from ErrInvalid because // the request was well-formed and retrying may well succeed — a registry // being unreachable is a different problem from a misspelled reference. ErrImageUnavailable = errors.New("sandbox image unavailable") )
Sentinel errors. Match them with errors.Is; backends wrap them with context rather than returning them bare.
Functions ¶
This section is empty.
Types ¶
type Backend ¶
type Backend interface {
// Create returns a running sandbox for name, creating it if absent and
// returning the existing one if present. It is the caller's session-affinity
// primitive: the same name reaches the same sandbox until it is reaped.
//
// Options are applied only when the sandbox is created. Calling Create again
// with different options on a live sandbox does not reconfigure it.
Create(ctx context.Context, name string, opts ...CreateOption) (Sandbox, error)
// Open returns an existing sandbox. It reports ErrNotFound if name has no
// sandbox, and never creates one.
Open(ctx context.Context, name string) (Sandbox, error)
// List returns every sandbox this backend manages, running or stopped.
List(ctx context.Context) ([]Info, error)
// Destroy removes a sandbox and its writable layer. It is idempotent:
// destroying an absent sandbox is not an error.
Destroy(ctx context.Context, name string) error
// Close releases the backend's own resources. It does not stop or destroy
// running sandboxes, which outlive the process that created them.
Close() error
}
Backend provisions sandboxes on a host runtime. Implementations are safe for concurrent use.
type Command ¶
type Command struct {
// Argv is the program and its arguments. It is executed directly, without a
// shell, so no quoting or escaping is applied. To use shell syntax, invoke a
// shell explicitly: []string{"sh", "-c", script}.
Argv []string
// Env entries are "KEY=value", merged over the sandbox's environment.
Env []string
// Dir is the working directory. Empty means the image's default.
Dir string
Stdin io.Reader
// Timeout bounds this command. Zero means the backend's default. The backend
// clamps it to a configured maximum.
Timeout time.Duration
}
Command is a program to run inside a sandbox.
There is no "language" field. A sandbox runs commands; mapping a language to an interpreter invocation belongs to the caller, who knows what its image contains.
type CreateOption ¶
type CreateOption func(*Spec)
CreateOption customises a sandbox at creation.
func WithCommandTimeouts ¶
func WithCommandTimeouts(def, ceiling time.Duration) CreateOption
WithCommandTimeouts overrides the default and maximum command durations.
func WithEgress ¶
func WithEgress(p EgressPolicy) CreateOption
WithEgress relaxes the default of no network access.
func WithEnv ¶
func WithEnv(entries ...string) CreateOption
WithEnv appends environment entries, each "KEY=value".
func WithImage ¶
func WithImage(ref string) CreateOption
WithImage sets the container image. Pin a digest rather than a tag: a tag can be repointed under you, and the image is the sandbox's entire userland.
func WithLabel ¶
func WithLabel(key, value string) CreateOption
WithLabel attaches a label for the caller's own bookkeeping. Labels are visible to host tooling and must not carry secrets.
func WithLifetime ¶
func WithLifetime(l Lifetime) CreateOption
WithLifetime overrides the idle and max-age bounds. A zero field keeps its default; use a negative value to disable a bound entirely.
func WithResources ¶
func WithResources(r Resources) CreateOption
WithResources overrides the resource bounds. Zero-valued fields keep their defaults, so a caller can raise memory alone without unbounding the rest.
func WithRuntime ¶
func WithRuntime(name string) CreateOption
WithRuntime overrides the isolation runtime. The default, runsc, is gVisor. Setting this to the host's default runtime trades away the isolation openblox exists to provide.
func WithUser ¶
func WithUser(user string) CreateOption
WithUser sets the uid:gid the sandbox runs as. It must not be root.
type EgressPolicy ¶
type EgressPolicy int
EgressPolicy controls a sandbox's outbound network access.
const ( // EgressNone gives the sandbox no network interface at all. It is the zero // value, and therefore the default. // // This is stronger than a firewall that drops outbound packets: with no // interface there is no DNS resolver to abuse as a covert channel, and no // route to host-local metadata services. Files still move in and out over // the runtime's control channel, so default-deny costs nothing functionally. EgressNone EgressPolicy = iota // EgressUnrestricted gives the sandbox ordinary host networking. // // Only appropriate when the code being run is trusted. If it is trusted, ask // why it needs a sandbox. EgressUnrestricted )
type Info ¶
type Info struct {
// Name is the caller-supplied identity passed to Create.
Name string
// ID is the runtime's identifier, for correlating with host-level tooling.
ID string
Image string
State State
CreatedAt time.Time
}
Info describes a sandbox.
type Lifetime ¶
type Lifetime struct {
// IdleTimeout stops a sandbox with no activity for this long.
IdleTimeout time.Duration
// MaxAge destroys a sandbox this long after creation regardless of activity.
// It reaps what the idle sweep misses — a sandbox kept warm by a wedged
// background process is never idle.
MaxAge time.Duration
}
Lifetime bounds how long a sandbox may live.
type Preview ¶
type Preview struct {
URL string
Token string
// Port is echoed back so a caller holding several previews can match them
// up without tracking the mapping itself.
Port int
ExpiresAt time.Time
}
Preview is a signed, expiring route to a port inside a sandbox.
type Resources ¶
type Resources struct {
CPUs float64
MemoryBytes int64
// DiskBytes caps the sandbox's writable scratch space.
//
// The root filesystem is read-only, so all writes land on sized tmpfs mounts
// and this is their combined ceiling. Container-layer quotas
// (--storage-opt size) are deliberately not used: they require overlay2 on
// xfs with pquota and hard-fail on the far more common ext4, so relying on
// them would make the disk bound silently unavailable on most hosts.
//
// Because tmpfs is RAM-backed, this budget is drawn from MemoryBytes rather
// than being independent of it: a sandbox that fills its scratch space has
// that much less memory for processes. DiskBytes must not exceed
// MemoryBytes.
DiskBytes int64
// MaxProcesses caps the process count. Without it a fork bomb inside the
// sandbox exhausts host PIDs regardless of CPU and memory limits.
MaxProcesses int
}
Resources bounds what a single sandbox may consume.
type Result ¶
Result is the outcome of a completed command.
Stdout and Stderr are bytes, not strings: a sandbox runs arbitrary programs and its output is not guaranteed to be valid UTF-8.
type Sandbox ¶
type Sandbox interface {
// Info returns a snapshot of the sandbox's identity and state. It does not
// query the runtime and so cannot fail; use [Backend.Open] for fresh state.
Info() Info
// Exec runs a command to completion and returns its output. A non-zero exit
// status is reported in Result.ExitCode, not as an error — err is non-nil
// only when the command could not be run or did not finish.
Exec(ctx context.Context, cmd Command) (Result, error)
// WriteFile writes src to path inside the sandbox, creating parent
// directories as needed. It streams, so it is safe for large payloads.
WriteFile(ctx context.Context, path string, mode fs.FileMode, src io.Reader) error
// ReadFile opens path inside the sandbox for reading. The caller must close
// the returned reader.
ReadFile(ctx context.Context, path string) (io.ReadCloser, error)
// StartProcess starts cmd as a detached background process under name. It is
// idempotent: if a process is already running under name, it is left alone
// and no error is returned.
StartProcess(ctx context.Context, name string, cmd Command) error
// Expose returns a signed, expiring URL for a port inside the sandbox.
//
// The returned token is a bearer credential and must be sent as a request
// header, never a query parameter — query strings leak into access logs,
// browser history, and Referer headers.
Expose(ctx context.Context, port int, ttl time.Duration) (Preview, error)
// Revoke invalidates a token returned by Expose before it expires.
Revoke(ctx context.Context, port int, token string) error
// Stop halts the sandbox without discarding it. A stopped sandbox can be
// reached again through [Backend.Create] with the same name.
Stop(ctx context.Context) error
}
Sandbox is a live instance. Methods are safe for concurrent use, but the guest they talk to is not: concurrent Exec calls run concurrently inside it.
type Spec ¶
type Spec struct {
Image string
Runtime string
User string
Resources Resources
Lifetime Lifetime
Egress EgressPolicy
Env []string
Labels map[string]string
// DefaultTimeout applies to commands that set none.
DefaultTimeout time.Duration
// MaxTimeout is the ceiling a command may request.
MaxTimeout time.Duration
}
Spec is the resolved configuration for a new sandbox. Callers build one through CreateOption values rather than constructing it directly.
func NewSpec ¶
func NewSpec(opts ...CreateOption) Spec
NewSpec resolves options over the secure defaults.
type State ¶
type State string
State is the lifecycle position of a sandbox.
openblox deliberately models three states rather than tracking every transition: the container runtime is the authority, and a parallel state machine over it can only drift.
const ( // StateRunning means the sandbox is up and accepting commands. StateRunning State = "running" // StateStopped means the sandbox exists but is halted. It can be restarted // through [Backend.Create] with the same name. StateStopped State = "stopped" // StateError means the runtime reported a state openblox cannot act on. The // sandbox should be destroyed rather than reused. StateError State = "error" )
The states a sandbox can be in.