Documentation
¶
Overview ¶
internal/daemon/expose.go
internal/daemon/expose_cap.go
internal/daemon/expose_conn.go
internal/daemon/expose_route.go
Index ¶
- func RegisterForkDaemonServer(s *grpc.Server, srv *Server)
- func RequireControllerIdentity(ctx context.Context, req any, info *grpc.UnaryServerInfo, ...) (any, error)
- func RequireControllerIdentityStream(srv any, ss grpc.ServerStream, info *grpc.StreamServerInfo, ...) error
- func ServeCAS(casCfg *CASServing)
- func ServeHTTP(addr string, engine ForkEngine, sandboxAPI *SandboxAPI, casCfg *CASServing)
- type AuditEvent
- type Auditor
- type CASServing
- type EnginePauser
- type ForkEngine
- type JSONAuditor
- type NodeVitals
- type NodeVitalsEntry
- type NodeVitalsNumbers
- type NopAuditor
- type RequestKeyStasher
- type SandboxAPI
- func (api *SandboxAPI) ActiveStreams(sandboxID string) int
- func (api *SandboxAPI) AllowTokenless()
- func (api *SandboxAPI) CloseExpose(sandboxID string)
- func (api *SandboxAPI) CloseForwards(sandboxID string)
- func (api *SandboxAPI) Configure(sandboxID string, env, secrets map[string]string) error
- func (api *SandboxAPI) Deadline(sandboxID string) (time.Time, bool)
- func (api *SandboxAPI) EnableUnixFallback()
- func (api *SandboxAPI) ForwardPort(sandboxID string, guestPort int) (string, error)
- func (api *SandboxAPI) HandleExpose(w http.ResponseWriter, r *http.Request)
- func (api *SandboxAPI) Handler() http.Handler
- func (api *SandboxAPI) HoldStreamForTest(sandboxID string) (release func())
- func (api *SandboxAPI) IsPaused(sandboxID string) bool
- func (api *SandboxAPI) LastActivity(sandboxID string) (time.Time, bool)
- func (api *SandboxAPI) MarkPaused(sandboxID string, paused bool)
- func (api *SandboxAPI) NotifyForked(sandboxID string, generation uint64, entropy []byte, ...) (*vsock.NotifyForkedResponse, error)
- func (api *SandboxAPI) ProxyHTTP(sandboxID string, guestPort int, prefix string) (*httputil.ReverseProxy, error)
- func (api *SandboxAPI) RecordActivity(sandboxID string, t time.Time)
- func (api *SandboxAPI) RegisterSandbox(sandboxID, vsockPath string) error
- func (api *SandboxAPI) RegisterStreamPath(sandboxID, vsockPath string)
- func (api *SandboxAPI) RegisterToken(sandboxID, token string)
- func (api *SandboxAPI) SetAuditor(a Auditor)
- func (api *SandboxAPI) SetEnginePauser(p EnginePauser)
- func (api *SandboxAPI) SetMaxExecTimeoutSeconds(n int)
- func (api *SandboxAPI) SetMaxExposePerSandbox(n int)
- func (api *SandboxAPI) SetMaxForwardsPerSandbox(n int)
- func (api *SandboxAPI) SetMaxStreamsPerSandbox(n int)
- func (api *SandboxAPI) SetSingleSandbox(id string)
- func (api *SandboxAPI) SetTimeout(sandboxID string, d time.Duration) time.Time
- func (api *SandboxAPI) SetVitalsLabels(sandboxID string, labels VitalsLabels)
- func (api *SandboxAPI) UnregisterSandbox(sandboxID string)
- type Server
- func (s *Server) Fork(ctx context.Context, snapshotID, sandboxID string, ...) (*fork.ForkResult, error)
- func (s *Server) ForkRunning(ctx context.Context, sourceSandboxID, newSandboxID string, pauseSource bool, ...) (*fork.ForkResult, error)
- func (s *Server) ListSandboxes() []*forkdpb.SandboxInfo
- func (s *Server) ListVolumes() []*forkdpb.VolumeInfo
- func (s *Server) ReclaimVolume(sandboxID string) error
- func (s *Server) SampleMetrics(ctx context.Context, interval time.Duration)
- func (s *Server) SetKeyProvider(p RequestKeyStasher)
- func (s *Server) Terminate(ctx context.Context, sandboxID string) error
- func (s *Server) UpdateMetrics()
- type VitalsLabels
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
func RegisterForkDaemonServer ¶
RegisterForkDaemonServer registers the gRPC service.
func RequireControllerIdentity ¶
func RequireControllerIdentity(ctx context.Context, req any, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (any, error)
RequireControllerIdentity rejects RPCs whose mTLS peer is not the controller. Installed only when forkd serves TLS.
func RequireControllerIdentityStream ¶
func RequireControllerIdentityStream(srv any, ss grpc.ServerStream, info *grpc.StreamServerInfo, handler grpc.StreamHandler) error
RequireControllerIdentityStream is the streaming twin of RequireControllerIdentity; without it, streaming RPCs (ExecStream) would bypass the identity check entirely.
func ServeCAS ¶
func ServeCAS(casCfg *CASServing)
ServeCAS serves ONLY the token-gated CAS surface on the dedicated CAS listener (casCfg.Addr) over TLS. It is mounted on its own mux and listener, separate from the sandbox HTTP API, so peer template distribution never changes the scheme of the exec/files/metrics/healthz endpoints SDK clients use. The gate rejects an absent/wrong token with 403 before any store access; TLS keeps the token confidential. casCfg must be enabled (the caller checks). The token value is never logged.
func ServeHTTP ¶
func ServeHTTP(addr string, engine ForkEngine, sandboxAPI *SandboxAPI, casCfg *CASServing)
ServeHTTP starts the HTTP server for metrics, health, and the sandbox API. This server's scheme is UNCHANGED by CAS distribution: it is always the plaintext operational mux (sandbox routes carry their own bearer auth). When casCfg is enabled, the token-gated CAS surface is served separately by ServeCAS on its own TLS listener, so SDK clients connecting over http:// are never forced onto TLS. ServeHTTP starts that CAS listener in a goroutine when enabled, then serves the sandbox mux on addr.
Types ¶
type AuditEvent ¶
type AuditEvent struct {
SandboxID string `json:"sandbox_id"`
Op string `json:"op"`
// Detail is a safe human summary, never file content or secret values.
Detail string `json:"detail,omitempty"`
// Bytes is the size of the file content read or written, in bytes. It is the
// COUNT only; the content itself is never recorded.
Bytes int `json:"bytes,omitempty"`
// Unix is the event time in Unix seconds, stamped by the auditor.
Unix int64 `json:"unix"`
// OK reports whether the handler served the operation without error. For
// exec, a non-zero exit code is still OK=true (the call succeeded); the exit
// code is reported in Detail.
OK bool `json:"ok"`
}
AuditEvent is one structured record of an operation served by the SandboxAPI. It carries only SAFE summaries: operation names and (for the interactive PTY exec over WebSocket) a non-content marker. It NEVER carries file content, env values, secret values, or bearer tokens.
type Auditor ¶
type Auditor interface {
Record(ev AuditEvent)
}
Auditor records audit events emitted by the SandboxAPI handlers.
func AuditorFromFlag ¶
AuditorFromFlag builds an Auditor from a --audit-log flag value. An empty value disables auditing (NopAuditor). "-" or "stderr" logs to os.Stderr. Any other value is a file path opened append-only; the returned closer is the open file (nil for stderr/off) and the caller closes it on shutdown.
type CASServing ¶
CASServing carries the optional configuration that lets forkd serve its content-addressed store to peer nodes for template distribution. The store is the engine's CAS; Token is the shared peer credential a pull must present; TLS is the server TLS config the dedicated CAS listener is wrapped in; Addr is the listen address of that dedicated listener (e.g. ":9092").
CAS serving is enabled only when ALL of Store, Token, TLS, and Addr are set. The chunks are digest-addressed, so integrity does not depend on the channel, but the token gates enumeration and pull and the token itself must stay confidential, so it travels only over TLS. The CAS surface is served on its OWN listener (Addr), NOT on the sandbox HTTP port: the sandbox API (exec/files/metrics/healthz) keeps its existing scheme so SDK clients are unaffected. When any field is missing the CAS listener is NOT started and the sandbox HTTP server behaves exactly as before. The token value is never logged.
type EnginePauser ¶
EnginePauser is the engine-side pause/resume the SandboxAPI drives on a real forkd: pause snapshots full state (memory + filesystem) and pauses the VM, resume restores it. The standalone sandbox-server and unit tests leave it unset, so the API records the held state only (no VM behind it).
type ForkEngine ¶
type ForkEngine interface {
Fork(snapshotID, sandboxID string, opts fork.ForkOpts) (*fork.ForkResult, error)
ForkRunning(sourceSandboxID, newSandboxID string, pauseSource bool) (*fork.ForkResult, error)
Terminate(sandboxID string) error
// Pause snapshots a sandbox's full state (memory + filesystem) and pauses the
// VM so a later Resume restores it (issue #218). Resume restores a paused
// sandbox to RUNNING. Both are idempotent and back the sandbox HTTP API's
// pause/resume endpoints; on the mock engine they track the held state only.
Pause(sandboxID string) error
Resume(sandboxID string) error
GetCapacity() fork.Capacity
// Metering returns the full CoW-aware metering report (per-sandbox and
// per-template memory plus disk) for the operator/billing endpoint. Unlike
// GetCapacity it is NOT on the fork hot path and may stat backing files.
Metering() metering.Report
ListSandboxes() []fork.SandboxRecord
// ListVolumes enumerates per-sandbox volume backing dirs on disk, keyed by
// sandbox id, so a backing dir whose sandbox is gone is still reported. It
// is NOT on the fork hot path. The mock engine reports an in-memory set.
ListVolumes() []fork.VolumeRecord
// ReclaimVolume removes one per-sandbox volume backing dir (and its sandbox
// dir). It is the volume-orphan counterpart to Terminate; the controller GC
// calls it for a backing dir whose claim object is gone.
ReclaimVolume(sandboxID string) error
// CreateTemplate builds a template snapshot. volumes are the template's
// declared volumes; the engine bakes one placeholder drive per volume into
// the snapshot. Nil leaves the template drive-less (only the rootfs).
// forceRebuild, when true, skips the reuse-or-rebuild gate (#584) and always
// deletes and rebuilds; the controller sets it when the template CONTENT
// changed (issue #475). warmKernel, when true, runs one trivial run_code
// cell before the snapshot so forks wake with a warm code-interpreter
// kernel; warmup failures fail open (the build continues cold).
CreateTemplate(id string, image string, initCommands []string, volumes []volume.Spec, workload *firecracker.WorkloadSpec, vmRes *firecracker.VMResources, forceRebuild bool, warmKernel bool) error
// PullTemplate fetches a template's snapshot from a peer forkd's CAS over
// the peer's token-gated TLS surface, materializes it, verifies it, and
// records the digest. token is a credential and must never be logged.
PullTemplate(ctx context.Context, templateID, manifestDigest, sourceURL, token string) error
}
ForkEngine is the interface both the real Firecracker engine and the mock engine implement.
type JSONAuditor ¶
type JSONAuditor struct {
// contains filtered or unexported fields
}
JSONAuditor writes one JSON-encoded AuditEvent per line to w. It is safe for concurrent use by multiple handlers (the write is mutex-guarded).
func NewJSONAuditor ¶
func NewJSONAuditor(w io.Writer) *JSONAuditor
NewJSONAuditor returns a JSONAuditor writing to w. The clock defaults to time.Now; tests override now for determinism.
func (*JSONAuditor) Record ¶
func (a *JSONAuditor) Record(ev AuditEvent)
Record stamps the event time (when unset) and writes one JSON line. Encoding errors are dropped: audit logging must never break the request path.
type NodeVitals ¶ added in v1.2.0
type NodeVitals struct {
Sandboxes []NodeVitalsEntry `json:"sandboxes"`
Skipped int `json:"skipped"`
}
NodeVitals is the GET /v1/vitals/node response: one NodeVitalsEntry per sandbox this forkd currently serves whose guest answered, so the control plane can publish org/pool-labeled guest health metrics WITHOUT holding each sandbox's per-sandbox bearer token. It is node-scoped operational data (the same access class as /v1/metering, /metrics, and /healthz), NOT per-sandbox traffic, so it is served on the operational mux without per-sandbox bearer auth. The Skipped count is the number of sandboxes whose guest was unreachable this scrape (a degradation signal); no sandbox id or error text is carried for them.
SECRET HYGIENE: this node-scoped endpoint is UNAUTHENTICATED, so it carries ONLY the control-plane labels (claim, pool, workspace, namespace; all k8s object names), the numeric guest vitals (steal, balloon, used/total memory), and a numeric process COUNT. It deliberately does NOT carry the per-process table: no process command name, pid, state, rss, argv, env, secret, or token is on the wire here. The full per-process table stays behind the per-sandbox bearer authenticated /v1/vitals (used by kubectl mitos ps --processes), never on this node batch. The control-plane sampler that consumes this reads ONLY the numeric fields (including process_count) and the org/pool labels.
type NodeVitalsEntry ¶ added in v1.2.0
type NodeVitalsEntry struct {
SandboxID string `json:"sandbox_id"`
VitalsLabels
Vitals NodeVitalsNumbers `json:"vitals"`
}
NodeVitalsEntry is one sandbox's numeric vitals in the node report, plus its control-plane labels and forkd SandboxID. The SandboxID is the husk pod name; it is NOT a secret (it already flows through /v1/metering) and lets the control-plane sampler resolve the trusted mitos.run/org label off the husk pod, exactly as the usage scraper does. The sampler uses the SandboxID ONLY to resolve org; it never becomes a metric label, so it adds no cardinality.
This struct intentionally carries the NUMERIC vitals inline plus a numeric ProcessCount, and NEVER embeds the guest process table (vsock.ProcessEntry). A per-process command name, pid, state, or rss CANNOT appear on this unauthenticated node-scoped endpoint by construction: there is no field on the wire to hold one. ProcessCount is the LENGTH of the guest's process table, the only process signal the control-plane sampler needs.
type NodeVitalsNumbers ¶ added in v1.2.0
type NodeVitalsNumbers struct {
StealFraction float64 `json:"steal_fraction"`
MemTotalKB uint64 `json:"mem_total_kb"`
MemAvailableKB uint64 `json:"mem_available_kb"`
MemUsedKB uint64 `json:"mem_used_kb"`
BalloonReclaimedKB uint64 `json:"balloon_reclaimed_kb"`
// ProcessCount is len(guest process table), never a per-process field.
ProcessCount int `json:"process_count"`
}
NodeVitalsNumbers is the numeric-only guest vitals carried on the node-scoped batch endpoint: it mirrors the numeric fields of vsock.VitalsResponse but replaces the per-process table with a numeric ProcessCount. It exists so the unauthenticated node endpoint physically cannot serialize a process command, pid, state, or rss: the type has no field for one.
type NopAuditor ¶
type NopAuditor struct{}
NopAuditor discards every event. It is the default so auditing is off until a real auditor is wired in (via --audit-log).
type RequestKeyStasher ¶
type RequestKeyStasher interface {
SetWrappedKey(scopeID string, wrappedDEK []byte, kekID string)
ForgetKey(scopeID string)
}
RequestKeyStasher is the seam the gRPC handlers use to hand the controller- delivered WRAPPED DEK to the engine's key provider for the duration of a CreateTemplate/Fork call. The same *fork.RequestKeyProvider satisfies it. The wrapped DEK is opaque ciphertext and the (eventual) plaintext DEK is a secret value: SetWrappedKey/ForgetKey carry the wrapped form without ever logging it.
type SandboxAPI ¶
type SandboxAPI struct {
// contains filtered or unexported fields
}
SandboxAPI exposes HTTP endpoints for exec/files on sandboxes managed by this forkd. The SDK and sandbox-server talk to this API to interact with running sandboxes. All guest communication uses gRPC on vsock.AgentGRPCPort (53); the legacy JSON-lines port 52 is no longer opened here.
func NewSandboxAPI ¶
func NewSandboxAPI(vsockDir string) *SandboxAPI
func (*SandboxAPI) ActiveStreams ¶
func (api *SandboxAPI) ActiveStreams(sandboxID string) int
ActiveStreams reports the number of currently OPEN streams (streaming exec, run_code, PTY) for sandboxID. It is the work-aware idle signal (issue #218): a non-zero count means a background job is running, so the idle reaper must treat the sandbox as active and never reap it mid-run even with no inbound API interaction.
func (*SandboxAPI) AllowTokenless ¶
func (api *SandboxAPI) AllowTokenless()
AllowTokenless permits requests targeting sandboxes that have no registered bearer token. Used ONLY by the standalone sandbox-server (which has no token-minting control plane) and by unit tests of other layers. forkd never sets it: a forkd sandbox without a token fails closed with 401. Sandboxes WITH a registered token are always enforced, even under AllowTokenless.
Must be called before the API serves requests; the flag is not synchronized.
func (*SandboxAPI) CloseExpose ¶ added in v1.3.0
func (api *SandboxAPI) CloseExpose(sandboxID string)
CloseExpose closes every tracked expose conn for sandboxID and clears the tracking entry. Called by UnregisterSandbox so no vsock tunnel goroutine outlives a terminated sandbox. Safe to call for a sandbox with no tracked conns.
func (*SandboxAPI) CloseForwards ¶ added in v0.12.0
func (api *SandboxAPI) CloseForwards(sandboxID string)
CloseForwards closes every live host-side port forward for sandboxID: the host listeners and all in-flight tunnels. It is called by UnregisterSandbox so a terminate leaves no host listener or tunnel goroutine behind. Safe to call for a sandbox with no forwards.
func (*SandboxAPI) Configure ¶
func (api *SandboxAPI) Configure(sandboxID string, env, secrets map[string]string) error
Configure delivers claim-time env and secrets to a sandbox's guest agent over gRPC. Values are never logged.
func (*SandboxAPI) Deadline ¶
func (api *SandboxAPI) Deadline(sandboxID string) (time.Time, bool)
Deadline returns the live TTL deadline recorded by set_timeout for sandboxID. The bool is false when no live timeout has been set (the sandbox runs under its creation-time idle/maxLifetime only).
func (*SandboxAPI) EnableUnixFallback ¶
func (api *SandboxAPI) EnableUnixFallback()
EnableUnixFallback lets dialGuestGRPC fall back to the guest agent's fixed local unix socket (/tmp/sandbox-agent-<port>.sock) when the vsock UDS path does not exist. This supports the standalone sandbox-server's local-testing workflow (agent running on the host, no Firecracker).
forkd deliberately does NOT enable this: its vsock paths come from the fork engine, and a fallback to a global socket could deliver claim-time secrets to an unrelated local process.
Must be called before the API serves requests; the flag is not synchronized.
func (*SandboxAPI) ForwardPort ¶ added in v0.12.0
func (api *SandboxAPI) ForwardPort(sandboxID string, guestPort int) (string, error)
ForwardPort opens a host TCP listener on 127.0.0.1:0 and bridges every accepted connection over a fresh gRPC PortForward tunnel to the guest's 127.0.0.1:guestPort (issue #228). It returns the host address (host:port) the caller dials. The listener and all its tunnels are tracked under sandboxID and torn down by CloseForwards (which UnregisterSandbox calls on terminate), so no host listener or tunnel goroutine outlives the sandbox.
The host listener binds to loopback ONLY: the standalone server has no token on this path (the same tokenless trust model as the rest of the standalone server), so a loopback bind keeps the forward reachable only from the host running the server, never from the network. The guest dial is forced to loopback by the guest agent. A guest port that is not listening surfaces as a per-connection tunnel error (the host connection is closed), not a hang.
It fails fast (before opening a listener) when the sandbox has no registered stream path or agent, when guestPort is out of range, or when the sandbox is already at the per-sandbox forward cap.
func (*SandboxAPI) HandleExpose ¶ added in v1.2.0
func (api *SandboxAPI) HandleExpose(w http.ResponseWriter, r *http.Request)
HandleExpose is the exported entry point so the standalone sandbox-server (a separate package) can mount the guest HTTP proxy route. It is identical to the route forkd mounts internally via handleExpose.
func (*SandboxAPI) Handler ¶
func (api *SandboxAPI) Handler() http.Handler
Handler returns an http.Handler for the sandbox exec/files API. The handler combines distinct auth surfaces on a single mux:
- Lifecycle JSON /v1/* routes (set_timeout, pause, resume): wrapped in requireBearer (body-peeking HTTP middleware that reads the "sandbox" field from the JSON body).
- Connect Sandbox service (issue #24, Task 3.2): mounted on the outer mux WITHOUT the body-peeking wrapper, because Connect auth is handled at the interceptor level via the "Authorization: Bearer <token>" and "X-Sandbox-Id" HTTP headers. BearerInterceptor enforces the same per-sandbox token security as requireBearer. The full runtime surface (exec, files, run_code, vitals, interactive PTY) is served here; the legacy JSON /v1 runtime routes were removed once every SDK and kubectl-mitos moved to Connect (#358).
- Connect-over-WebSocket Exec: outside requireBearer (bodyless GET); auth is handled by ptyAuth (?sandbox= + Authorization: Bearer query/header).
func (*SandboxAPI) HoldStreamForTest ¶
func (api *SandboxAPI) HoldStreamForTest(sandboxID string) (release func())
HoldStreamForTest reserves one open-stream slot for sandboxID and returns a release func, simulating a live background job (streaming exec, run_code, or PTY) so the work-aware idle signal (ActiveStreams > 0) is exercisable without driving a real vsock stream. It is the injection seam controller envtests use to assert a sandbox with a background job is not idle-reaped (issue #218).
func (*SandboxAPI) IsPaused ¶
func (api *SandboxAPI) IsPaused(sandboxID string) bool
IsPaused reports whether sandboxID is currently paused (clock stopped).
func (*SandboxAPI) LastActivity ¶
func (api *SandboxAPI) LastActivity(sandboxID string) (time.Time, bool)
LastActivity returns the time of the most recent exec or file call on the sandbox. The bool is false when the sandbox has never been accessed.
func (*SandboxAPI) MarkPaused ¶
func (api *SandboxAPI) MarkPaused(sandboxID string, paused bool)
MarkPaused sets or clears the paused flag for sandboxID. A paused sandbox has a stopped idle clock and is never idle-reaped; clearing it returns the sandbox to the normal idle/TTL clock. It is the seam the engine pause/resume drives and the unit-testable signal the reaper reads.
func (*SandboxAPI) NotifyForked ¶
func (api *SandboxAPI) NotifyForked(sandboxID string, generation uint64, entropy []byte, guestNet *vsock.NotifyForkedNetwork, volumes []vsock.VolumeMountEntry) (*vsock.NotifyForkedResponse, error)
NotifyForked tells a sandbox's guest agent a restore just happened so it can reseed the kernel CRNG, step the wall clock, and signal userspace, over gRPC. When guestNet is non-nil it also carries this fork's distinct eth0 address + gateway so the guest re-addresses its NIC. When volumes is non-empty it carries the per-fork volume mount table the guest mounts after the host rebound the drives. Entropy is sensitive seed material and is never logged; the network addresses, device nodes, and paths are safe to log.
It RETURNS the guest's NotifyForkedResponse so the caller can enforce the fork-correctness gate (ReseededRNG): a transport success alone does not mean the guest reseeded its CRNG. The response carries booleans and counts only, never entropy bytes.
func (*SandboxAPI) ProxyHTTP ¶ added in v1.2.0
func (api *SandboxAPI) ProxyHTTP(sandboxID string, guestPort int, prefix string) (*httputil.ReverseProxy, error)
ProxyHTTP returns a reverse proxy that forwards an HTTP request to the guest's 127.0.0.1:guestPort over a fresh PortForward tunnel, stripping prefix from the request path so the guest daemon sees the sub-path. FlushInterval is -1 so responses (including Server-Sent-Events) stream immediately with no buffering; keep-alives are disabled so each request uses its own tunnel and guest TCP connection, matching the per-connection tunnel model of ForwardPort. It fails fast for an unregistered sandbox or an out-of-range port. Bytes are never logged (secret hygiene).
func (*SandboxAPI) RecordActivity ¶
func (api *SandboxAPI) RecordActivity(sandboxID string, t time.Time)
RecordActivity stamps t as the sandbox's last-activity time, overriding the clock-based touch. It exists so callers (and tests of the GC reconciler) can set a known last-activity for a sandbox id; forkd itself relies on the implicit touch from exec and file handlers.
func (*SandboxAPI) RegisterSandbox ¶
func (api *SandboxAPI) RegisterSandbox(sandboxID, vsockPath string) error
RegisterSandbox records the vsock UDS path for sandboxID and registers the sandbox as active. All subsequent guest communication uses gRPC on vsock.AgentGRPCPort (53) dialed on demand; there is no persistent shared connection. callers should call RegisterStreamPath separately only when the path differs from vsockPath; RegisterSandbox already records vsockPath as the stream path. For forkd both calls use the same path, so calling RegisterSandbox is sufficient.
func (*SandboxAPI) RegisterStreamPath ¶
func (api *SandboxAPI) RegisterStreamPath(sandboxID, vsockPath string)
RegisterStreamPath records the vsock UDS path for opening per-call gRPC connections to a sandbox's guest agent. RegisterSandbox already records this path; call RegisterStreamPath only when the path must be updated after initial registration.
func (*SandboxAPI) RegisterToken ¶
func (api *SandboxAPI) RegisterToken(sandboxID, token string)
RegisterToken registers the bearer token required on every HTTP request targeting sandboxID. An empty token is a no-op: the sandbox stays tokenless and fails closed (unless AllowTokenless). Token values are never logged.
func (*SandboxAPI) SetAuditor ¶
func (api *SandboxAPI) SetAuditor(a Auditor)
SetAuditor installs the auditor that records a structured event after each exec and file operation. Passing nil installs the NopAuditor (auditing off). Must be called before the API serves requests; the field is not synchronized.
func (*SandboxAPI) SetEnginePauser ¶
func (api *SandboxAPI) SetEnginePauser(p EnginePauser)
SetEnginePauser installs the engine pause/resume hook (issue #218). forkd sets it so the pause/resume HTTP endpoints drive the real Firecracker snapshot/restore; the standalone server leaves it nil and the endpoints record the held state only. Must be called before the API serves requests; the field is not synchronized.
func (*SandboxAPI) SetMaxExecTimeoutSeconds ¶
func (api *SandboxAPI) SetMaxExecTimeoutSeconds(n int)
SetMaxExecTimeoutSeconds sets the ceiling on a requested exec or run_code timeout (issue #216). A request over the ceiling is rejected with the typed timeout_too_large code, never silently reduced. n<=0 disables the ceiling. Must be called before the API serves requests; the field is not synchronized.
func (*SandboxAPI) SetMaxExposePerSandbox ¶ added in v1.3.0
func (api *SandboxAPI) SetMaxExposePerSandbox(n int)
SetMaxExposePerSandbox sets the per-sandbox ceiling on concurrent OPEN expose tunnels (authenticated guest HTTP proxy). A NEW tunnel opened while a sandbox is already at the cap is rejected with 429; existing tunnels are never killed. n<=0 disables the cap (unbounded). Must be called before the API serves requests; the field is not synchronized.
func (*SandboxAPI) SetMaxForwardsPerSandbox ¶ added in v0.12.0
func (api *SandboxAPI) SetMaxForwardsPerSandbox(n int)
SetMaxForwardsPerSandbox sets the per-sandbox ceiling on concurrent OPEN port forwards (issue #228). A NEW forward over the cap is rejected. n<=0 disables the cap (unbounded). Must be called before the API serves requests; the field is not synchronized.
func (*SandboxAPI) SetMaxStreamsPerSandbox ¶
func (api *SandboxAPI) SetMaxStreamsPerSandbox(n int)
SetMaxStreamsPerSandbox sets the per-sandbox ceiling on concurrent OPEN streams (streaming exec, run_code, PTY). A NEW stream opened while a sandbox is already at the cap is rejected with 429; existing streams are never killed. n<=0 disables the cap (unbounded). Must be called before the API serves requests; the field is not synchronized.
func (*SandboxAPI) SetSingleSandbox ¶
func (api *SandboxAPI) SetSingleSandbox(id string)
SetSingleSandbox switches the API into single-sandbox mode for the husk-stub, which serves exactly ONE VM per pod. In this mode the auth gate (requireBearer and ptyAuth) validates the presented bearer against the single sandbox's registered token regardless of the request's "sandbox" id, then routes the request to id. This is required because the husk-stub registers its one VM under a fixed local id while the SDK addresses the in-pod API with the claim's status.sandboxID (the husk pod name), which never equals that local id; a strict per-id lookup would 401 every SDK request.
The token gate is NOT weakened: a wrong or absent bearer is still rejected (401), the comparison stays constant-time, and a sandbox with no registered token still fails closed (unless AllowTokenless). forkd never calls this, so its multi-sandbox per-id token lookup is unchanged: a token for sandbox A cannot authorize sandbox B.
Must be called before the API serves requests; the fields are not synchronized.
func (*SandboxAPI) SetTimeout ¶
SetTimeout records a live TTL deadline of now + d for sandboxID and returns the absolute deadline. It is the running-sandbox TTL the lifetime reaper reads via ListSandboxes; calling it again replaces the deadline (extend or shorten). The clock is the API's now (overridable in tests).
func (*SandboxAPI) SetVitalsLabels ¶
func (api *SandboxAPI) SetVitalsLabels(sandboxID string, labels VitalsLabels)
SetVitalsLabels records the claim/pool/workspace identity for sandboxID so its vitals snapshot is labeled. forkd calls it on the Fork path with the same identity the OTel spans carry. Calling it again replaces the labels. The labels are object names, never secrets.
func (*SandboxAPI) UnregisterSandbox ¶
func (api *SandboxAPI) UnregisterSandbox(sandboxID string)
UnregisterSandbox clears the sandbox's path and bearer token.
type Server ¶
type Server struct {
// contains filtered or unexported fields
}
func NewServer ¶
func NewServer(engine ForkEngine, sandboxAPI *SandboxAPI) *Server
func (*Server) Fork ¶
func (s *Server) Fork(ctx context.Context, snapshotID, sandboxID string, env, secrets map[string]string, netConf *forkdpb.NetworkConfig, volumes []*forkdpb.VolumeMount, apiToken string, labels VitalsLabels) (*fork.ForkResult, error)
Fork handles a fork request from the controller. apiToken is the bearer token the HTTP sandbox API will require for this sandbox; an empty token registers NOTHING, so HTTP calls to the sandbox fail closed with 401 (forkd never runs the API in tokenless mode). The token value is never logged.
netConf carries the template's NetworkPolicy (egress policy + allowlist). It is parsed into fork.NetworkOpts and threaded into the engine, which uses it to build the per-fork egress ruleset when networking is enabled. When netConf is nil the fork gets no network identity (networking disabled or no policy on the template). The egress policy and allowlist entries are safe to log. labels carries the control-plane identity (claim/pool/workspace/namespace) the controller attached to this fork; forkd records it per-sandbox so the sandbox's Layer 3 guest telemetry (/v1/vitals, kubectl mitos ps --processes) is LABELED (issue #164). The fields are object names, never secrets; an empty VitalsLabels leaves the sandbox unlabeled.
func (*Server) ForkRunning ¶
func (s *Server) ForkRunning(ctx context.Context, sourceSandboxID, newSandboxID string, pauseSource bool, apiToken string) (*fork.ForkResult, error)
ForkRunning checkpoints a running sandbox and forks it.
ForkRunning deliberately does NOT deliver new config: forks inherit the source VM's memory, including any previously delivered env+secrets. Fresh-credential reissue for live forks is issue #7's end state.
It MUST still send NotifyForked: a live-fork child boots from the parent's exact memory image, so it shares the parent's CRNG and userspace PRNG state. That is precisely the fork-correctness hazard, so the same fail-closed policy as restore-from-snapshot applies on a real engine.
apiToken is the new sandbox's own bearer token (the source's token does NOT open the fork). Empty means no token is registered and HTTP calls to the fork fail closed with 401.
func (*Server) ListSandboxes ¶
func (s *Server) ListSandboxes() []*forkdpb.SandboxInfo
ListSandboxes returns one SandboxInfo per sandbox the engine currently holds, merging the engine's created-at with the SandboxAPI's last-activity time. last_activity_unix is zero for sandboxes that have never been accessed; uptime_seconds is computed from created-at against the current time.
func (*Server) ListVolumes ¶
func (s *Server) ListVolumes() []*forkdpb.VolumeInfo
ListVolumes returns one VolumeInfo per per-sandbox volume backing dir the engine reports, keyed by sandbox id with an age in seconds. The controller GC uses it to find volume backings whose claim object is gone.
func (*Server) ReclaimVolume ¶
ReclaimVolume removes one per-sandbox volume backing dir. It is the volume-orphan counterpart to Terminate; unlike Terminate it does not touch the SandboxAPI registration (an orphan volume has no live sandbox).
func (*Server) SampleMetrics ¶
SampleMetrics periodically refreshes the metering gauges (UpdateMetrics) until ctx is cancelled. The engine re-samples each live sandbox's memory on every metering pass, so the gauges report LIFETIME unique memory, not the T=0 fork-time footprint (fork-correctness Row 5, issue #3). Without this loop the gauges UpdateMetrics sets are never populated and /metrics serves a stale zero. It samples once immediately so the first scrape after startup is already populated; interval <= 0 selects defaultMetricsSampleInterval.
func (*Server) SetKeyProvider ¶
func (s *Server) SetKeyProvider(p RequestKeyStasher)
SetKeyProvider wires the request-scoped key provider the handlers use to hand the controller-delivered encryption key to the engine. It must be the same instance the engine reads from (EngineOpts.KeyProvider). Called only when at-rest encryption is enabled.
func (*Server) UpdateMetrics ¶
func (s *Server) UpdateMetrics()
UpdateMetrics refreshes capacity and metering gauges. Memory gauges are CoW-aware: shared is each template's shared set counted once, unique is the per-fork dirty total. The disk gauge reflects CoW-aware metered backing storage. ActiveSandboxes comes from the cheap capacity path; the rest from the full metering report (which also stats backing files).
type VitalsLabels ¶
type VitalsLabels struct {
Claim string `json:"claim,omitempty"`
Pool string `json:"pool,omitempty"`
Workspace string `json:"workspace,omitempty"`
Namespace string `json:"namespace,omitempty"`
}
VitalsLabels is the control-plane identity the host attaches to a sandbox's guest telemetry: the claim, pool, and workspace names. They are k8s object names, never secrets. Any field may be empty when the host does not know it (e.g. a poolless direct fork); an empty field is reported as empty, never guessed.