microsandbox_protocol/lib.rs
1//! `microsandbox-protocol` defines the shared protocol types used for communication
2//! between the host and the guest agent over CBOR-over-virtio-serial.
3//!
4//! For how the protocol is versioned and evolved while staying backward compatible
5//! across independently-upgraded hosts and live sandboxes, see `VERSIONING.md` in
6//! this crate.
7
8#![warn(missing_docs)]
9
10mod error;
11
12//--------------------------------------------------------------------------------------------------
13// Constants: Host↔Guest Shutdown Timings
14//--------------------------------------------------------------------------------------------------
15
16/// Maximum time agentd spends in its handoff-mode poweroff sequence.
17///
18/// In init-handoff sandboxes (systemd, openrc, …) agentd's shutdown
19/// handler signals the new PID 1 with `SIGRTMIN+4`, sleeps for this
20/// duration to give the init a chance to act, then falls back to
21/// `SIGTERM`. The host's [`SHUTDOWN_FLUSH_TIMEOUT`] must exceed this
22/// so the host's fallback exit doesn't cut the sequence short.
23pub const HANDOFF_POWEROFF_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(5);
24
25/// How long the host waits after forwarding `core.shutdown` to agentd
26/// before triggering its own VMM exit fallback.
27///
28/// agentd uses this window to `sync()` block-backed root filesystems
29/// and power off the kernel cleanly (or run its handoff sequence —
30/// see [`HANDOFF_POWEROFF_TIMEOUT`]). On a healthy guest the VMM
31/// exits well inside the window and the host fallback is a no-op;
32/// the fallback only fires when the guest is wedged.
33///
34/// Must exceed [`HANDOFF_POWEROFF_TIMEOUT`] plus margin for the
35/// init's own signal handling — enforced at compile time below.
36pub const SHUTDOWN_FLUSH_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(8);
37
38// Compile-time invariant: the host must wait at least as long as
39// agentd's longest internal grace, otherwise the host fallback will
40// cut agentd's handoff sequence short and we'll silently strand
41// init-handoff sandboxes.
42const _: () = assert!(
43 SHUTDOWN_FLUSH_TIMEOUT.as_secs() > HANDOFF_POWEROFF_TIMEOUT.as_secs(),
44 "SHUTDOWN_FLUSH_TIMEOUT must exceed HANDOFF_POWEROFF_TIMEOUT",
45);
46
47//--------------------------------------------------------------------------------------------------
48// Constants: Host↔Guest Protocol
49//--------------------------------------------------------------------------------------------------
50
51/// Virtio-console port name for the agent channel.
52pub const AGENT_PORT_NAME: &str = "agent";
53
54/// Virtiofs tag for the runtime filesystem (scripts, heartbeat).
55pub const RUNTIME_FS_TAG: &str = "msb_runtime";
56
57/// Guest-write byte budget for the runtime (`/.msb`) virtiofs mount.
58///
59/// `/.msb` is a host↔guest control channel, not bulk storage: the only
60/// guest-written payload is a ~1 KiB heartbeat (host-written scripts and TLS
61/// certs form the mount's baseline and are not charged). This 16 MiB ceiling is
62/// therefore almost entirely abuse headroom — it exists so the channel cannot be
63/// used to fill the host disk. It is intentionally a fixed constant rather than
64/// a user-facing knob.
65pub const RUNTIME_FS_QUOTA_BYTES: u64 = 16 * 1024 * 1024;
66
67/// Guest mount point for the runtime filesystem.
68pub const RUNTIME_MOUNT_POINT: &str = "/.msb";
69
70/// Guest directory for file mount virtiofs shares.
71pub const FILE_MOUNTS_DIR: &str = "/.msb/file-mounts";
72
73/// Guest path for named scripts (added to PATH by agentd).
74pub const SCRIPTS_PATH: &str = "/.msb/scripts";
75
76/// Maximum number of simultaneous SDK clients the host relay admits.
77pub const AGENT_RELAY_MAX_CLIENTS: u32 = 128;
78
79/// Size of the correlation ID range allocated to each relay client.
80pub const AGENT_RELAY_ID_RANGE_STEP: u32 = u32::MAX / AGENT_RELAY_MAX_CLIENTS;
81
82//--------------------------------------------------------------------------------------------------
83// Constants: Guest Init Environment Variables
84//--------------------------------------------------------------------------------------------------
85
86/// Environment variable carrying the sandbox in-guest security profile.
87///
88/// Values:
89/// - `default` — preserve normal guest-root semantics. Exec sessions do not
90/// set `no_new_privs` and keep `CAP_SYS_ADMIN`.
91/// - `restricted` — set `no_new_privs` and drop `CAP_SYS_ADMIN` before user
92/// exec sessions. Agentd also forces `nosuid,nodev` on user mounts.
93///
94/// Example:
95/// - `MSB_SECURITY_PROFILE=restricted`
96pub const ENV_SECURITY_PROFILE: &str = "MSB_SECURITY_PROFILE";
97
98/// Environment variable carrying tmpfs mount specs for guest init.
99///
100/// - `path` — guest mount path (required, always the first element)
101/// - `size=N` — size limit in MiB (optional)
102/// - `noexec` — mount with noexec flag (optional)
103/// - `nosuid` — mount with nosuid flag (optional)
104/// - `nodev` — mount with nodev flag (optional)
105/// - `ro` — mount read-only (optional)
106/// - `rw` — explicit writable default (optional)
107/// - `mode=N` — permission mode as octal integer (optional, e.g. `mode=1777`)
108///
109/// Format: `path[:opts][;path[:opts];...]`.
110///
111/// Entries are separated by `;`. Within an entry, the path comes first,
112/// followed by an optional colon and comma-separated options. Options compose
113/// order-independently (e.g. `:ro,noexec` and `:noexec,ro` are equivalent).
114///
115/// Examples:
116/// - `MSB_TMPFS=/tmp:size=256` — 256 MiB tmpfs at `/tmp`
117/// - `MSB_TMPFS=/tmp:size=256;/var/tmp:size=128` — two tmpfs mounts
118/// - `MSB_TMPFS=/tmp` — tmpfs at `/tmp` with defaults
119/// - `MSB_TMPFS=/tmp:size=256,noexec` — with noexec flag
120/// - `MSB_TMPFS=/seed:size=64,ro` — read-only tmpfs
121pub const ENV_TMPFS: &str = "MSB_TMPFS";
122
123/// Environment variable specifying how agentd assembles the root filesystem.
124///
125/// Format: comma-separated `key=value` pairs, semicolons for multi-value fields.
126///
127/// Variants:
128/// - `kind=disk-image,device=/dev/vda[,fstype=ext4]`
129/// - `kind=oci-layered,lowers=/dev/vdb;/dev/vdc;/dev/vdd,lower_fstype=erofs,upper=/dev/vde,upper_fstype=ext4`
130/// - `kind=oci-flat,lower=/dev/vdb,lower_fstype=erofs,upper=/dev/vdc,upper_fstype=ext4`
131///
132/// Legacy format (`/dev/vda[,fstype=ext4]`) is accepted and treated as `kind=disk-image`.
133pub const ENV_BLOCK_ROOT: &str = "MSB_BLOCK_ROOT";
134
135/// Environment variable carrying the guest network interface configuration.
136///
137/// Format: `key=value,...`
138///
139/// - `iface=NAME` — interface name (required)
140/// - `mac=AA:BB:CC:DD:EE:FF` — MAC address (required)
141/// - `mtu=N` — MTU (optional)
142///
143/// Example:
144/// - `MSB_NET=iface=eth0,mac=02:5a:7b:13:01:02,mtu=1500`
145pub const ENV_NET: &str = "MSB_NET";
146
147/// Environment variable carrying the guest IPv4 network configuration.
148///
149/// Format: `key=value,...`
150///
151/// - `addr=A.B.C.D/N` — address with prefix length (required)
152/// - `gw=A.B.C.D` — default gateway (required)
153/// - `dns=A.B.C.D` — DNS server (optional)
154///
155/// Example:
156/// - `MSB_NET_IPV4=addr=172.16.1.2/30,gw=172.16.1.1,dns=172.16.1.1`
157pub const ENV_NET_IPV4: &str = "MSB_NET_IPV4";
158
159/// Environment variable carrying the guest IPv6 network configuration.
160///
161/// Format: `key=value,...`
162///
163/// - `addr=ADDR/N` — address with prefix length (required)
164/// - `gw=ADDR` — default gateway (required)
165/// - `dns=ADDR` — DNS server (optional)
166///
167/// Example:
168/// - `MSB_NET_IPV6=addr=fd42:6d73:62:2a::2/64,gw=fd42:6d73:62:2a::1,dns=fd42:6d73:62:2a::1`
169pub const ENV_NET_IPV6: &str = "MSB_NET_IPV6";
170
171/// Environment variable carrying virtiofs directory volume mount specs for guest init.
172///
173/// Format: `tag:guest_path[:opts][;tag:guest_path[:opts];...]`
174///
175/// - `tag` — virtiofs tag name (required, matches the tag used in `--mount`)
176/// - `guest_path` — mount point inside the guest (required)
177/// - `ro` / `rw` — access mode option (optional)
178/// - `noexec` — disable direct execution from the mount (optional)
179/// - `nosuid` — mount with nosuid flag (optional)
180/// - `nodev` — mount with nodev flag (optional)
181///
182/// Entries are separated by `;`.
183///
184/// Examples:
185/// - `MSB_DIR_MOUNTS=data:/data` — mount virtiofs tag `data` at `/data`
186/// - `MSB_DIR_MOUNTS=data:/data:ro,noexec` — mount read-only and noexec
187/// - `MSB_DIR_MOUNTS=data:/data;cache:/cache:ro` — two mounts
188pub const ENV_DIR_MOUNTS: &str = "MSB_DIR_MOUNTS";
189
190/// Environment variable carrying virtiofs **file** volume mount specs for guest init.
191///
192/// Used when the host path is a single file rather than a directory. The SDK
193/// wraps each file in an isolated staging directory (hard-linked to preserve
194/// the same inode) and shares that directory via virtiofs. Agentd mounts the
195/// share at [`FILE_MOUNTS_DIR`]`/<tag>/` and bind-mounts the file to the
196/// guest path.
197///
198/// Format: `tag:filename:guest_path[:opts][;tag:filename:guest_path[:opts];...]`
199///
200/// - `tag` — virtiofs tag name (required, matches the tag used in `--mount`)
201/// - `filename` — name of the file inside the virtiofs share (required)
202/// - `guest_path` — final file path inside the guest (required)
203/// - `ro` / `rw` — access mode option (optional)
204/// - `noexec` — disable direct execution from the mount (optional)
205/// - `nosuid` — mount with nosuid flag (optional)
206/// - `nodev` — mount with nodev flag (optional)
207///
208/// Entries are separated by `;`.
209///
210/// Examples:
211/// - `MSB_FILE_MOUNTS=fm_config:app.conf:/etc/app.conf`
212/// - `MSB_FILE_MOUNTS=fm_config:app.conf:/etc/app.conf:ro,noexec`
213/// - `MSB_FILE_MOUNTS=fm_a:a.sh:/usr/bin/a.sh;fm_b:b.sh:/usr/bin/b.sh`
214pub const ENV_FILE_MOUNTS: &str = "MSB_FILE_MOUNTS";
215
216/// Environment variable carrying disk-image volume mount specs for guest init.
217///
218/// Each spec describes one virtio-blk device attached for the sole purpose
219/// of being mounted at a guest path by agentd (distinct from the rootfs
220/// block device, which is described by [`ENV_BLOCK_ROOT`]).
221///
222/// Format: `id:guest_path[:opts][;id:guest_path[:opts];...]`
223///
224/// - `id` — the `virtio_blk_config.serial` value set by the VMM. Agentd
225/// resolves it to a device node via `/dev/disk/by-id/virtio-<id>`, or
226/// by scanning `/sys/block/*/serial` as a fallback.
227/// - `guest_path` — absolute mount path in the guest (required).
228/// - `fstype=...` — inner filesystem type (optional). When absent,
229/// agentd probes `/proc/filesystems` to find a type that mounts cleanly.
230/// - `ro` / `rw` — access mode option (optional).
231/// - `noexec` — disable direct execution from the mount (optional).
232/// - `nosuid` — mount with nosuid flag (optional).
233/// - `nodev` — mount with nodev flag (optional).
234///
235/// Entries are separated by `;`. Options are comma-separated flags or
236/// key-value pairs in the final option block.
237///
238/// Examples:
239/// - `MSB_DISK_MOUNTS=data_12ab:/data:fstype=ext4` — ext4 disk at `/data`
240/// - `MSB_DISK_MOUNTS=seed_7f:/seed:ro` — autodetect fstype, read-only
241/// - `MSB_DISK_MOUNTS=a_1:/a:fstype=ext4;b_2:/b:ro,noexec` — two disks
242pub const ENV_DISK_MOUNTS: &str = "MSB_DISK_MOUNTS";
243
244/// Environment variable carrying the default guest user for agentd execs.
245///
246/// Format: `USER[:GROUP]` or `UID[:GID]`
247///
248/// - `USER`
249/// - `UID`
250/// - `USER:GROUP`
251/// - `UID:GID`
252///
253/// Example:
254/// - `MSB_USER=alice` — default to user `alice`
255/// - `MSB_USER=1000` — default to UID 1000
256/// - `MSB_USER=alice:developers` — default to user `alice` and group `developers`
257/// - `MSB_USER=1000:100` — default to UID 1000 and GID 100
258pub const ENV_USER: &str = "MSB_USER";
259
260/// Environment variable carrying the guest hostname for agentd.
261///
262/// Format: bare string
263///
264/// Example:
265/// - `MSB_HOSTNAME=worker-01`
266///
267/// agentd calls `sethostname()` and adds the name to `/etc/hosts`.
268/// Defaults to a sandbox-name-derived hostname when not explicitly set.
269pub const ENV_HOSTNAME: &str = "MSB_HOSTNAME";
270
271/// Environment variable carrying the DNS name the guest uses to reach
272/// the sandbox host (Docker's `host.docker.internal` equivalent).
273///
274/// The host-side network stack emits this value via its
275/// `guest_env_vars()` method; agentd reads it into
276/// [`crate::exec`]-adjacent boot params and writes the mapping into
277/// `/etc/hosts`. The value the network stack emits is a fixed
278/// protocol constant — today always `host.microsandbox.internal`.
279pub const ENV_HOST_ALIAS: &str = "MSB_HOST_ALIAS";
280
281/// Environment variable carrying sandbox-wide resource limits.
282///
283/// Format: `resource=limit[:hard][;resource=limit[:hard];...]`
284///
285/// - `resource` — lowercase rlimit name such as `nofile` or `nproc`
286/// - `limit` — soft limit
287/// - `hard` — hard limit (optional; if omitted, uses the soft limit)
288///
289/// Examples:
290/// - `MSB_RLIMITS=nofile=65535`
291/// - `MSB_RLIMITS=nofile=65535:65535;nproc=4096:4096`
292///
293/// agentd applies these during PID 1 startup so every later guest process
294/// inherits the raised baseline instead of having to opt into per-exec rlimits.
295pub const ENV_RLIMITS: &str = "MSB_RLIMITS";
296
297/// Environment variable selecting a guest init binary for PID 1 handoff.
298///
299/// When set, agentd performs initial setup (mounts, runtime dirs), then
300/// forks. The parent execs the binary at this path, becoming the new
301/// PID 1. The child stays alive as a normal grandchild process serving
302/// host requests over virtio-serial.
303///
304/// Format: bare absolute path inside the guest rootfs, or the literal
305/// sentinel [`HANDOFF_INIT_AUTO`] which triggers a candidate probe in
306/// agentd (see [`HANDOFF_INIT_AUTO_CANDIDATES`]).
307///
308/// Examples:
309/// - `MSB_HANDOFF_INIT=/lib/systemd/systemd`
310/// - `MSB_HANDOFF_INIT=auto`
311pub const ENV_HANDOFF_INIT: &str = "MSB_HANDOFF_INIT";
312
313/// Sentinel value for [`ENV_HANDOFF_INIT`] requesting auto-detection.
314///
315/// The host may resolve this sentinel before boot when an OCI image
316/// declares a known init as the first entrypoint token. If the sentinel
317/// reaches the guest unchanged, agentd probes [`HANDOFF_INIT_AUTO_CANDIDATES`]
318/// in order and uses the first path that exists and is executable. If
319/// none match, boot fails with a clear error in `kernel.log` listing the
320/// paths it checked.
321pub const HANDOFF_INIT_AUTO: &str = "auto";
322
323/// Ordered list of image entrypoint paths that `--init auto` may treat
324/// as an explicit handoff init.
325///
326/// This host-side list is intentionally slightly wider than
327/// [`HANDOFF_INIT_AUTO_CANDIDATES`]: `/init` is common in s6-overlay
328/// images but too broad to probe blindly inside every guest rootfs.
329/// Matching it only when the image declares it as ENTRYPOINT keeps the
330/// behavior image-directed.
331pub const HANDOFF_INIT_IMAGE_ENTRYPOINT_CANDIDATES: &[&str] = &[
332 "/init",
333 "/sbin/init",
334 "/lib/systemd/systemd",
335 "/usr/lib/systemd/systemd",
336];
337
338/// Ordered list of init-binary paths agentd probes when
339/// [`ENV_HANDOFF_INIT`] is set to [`HANDOFF_INIT_AUTO`].
340///
341/// Order matters: the first match wins. The list covers the three
342/// well-known locations across major distros:
343/// - `/sbin/init` — BusyBox (Alpine), sysvinit, OpenRC's wrapper.
344/// Usually a symlink to the actual init on systemd distros, so it
345/// resolves naturally on Debian/Ubuntu too.
346/// - `/lib/systemd/systemd` — Debian, Ubuntu, derivatives.
347/// - `/usr/lib/systemd/systemd` — Fedora, RHEL, modern Debian.
348pub const HANDOFF_INIT_AUTO_CANDIDATES: &[&str] = &[
349 "/sbin/init",
350 "/lib/systemd/systemd",
351 "/usr/lib/systemd/systemd",
352];
353
354/// Argv list for the handoff init binary.
355///
356/// Format: base64url-no-padding encoded JSON array of strings.
357/// Empty or unset means the init is exec'd with `argv = [program]`.
358/// This deliberately differs from the delimiter-based `MSB_*` boot env
359/// formats because argv entries are arbitrary strings; wrapping JSON in
360/// base64url preserves spaces, separators, empty strings, and Unicode
361/// without inventing a second escaping language.
362///
363/// Example:
364/// - `MSB_HANDOFF_INIT_ARGS=WyItdW5pdD1tdWx0aS11c2VyLnRhcmdldCJd`
365pub const ENV_HANDOFF_INIT_ARGS: &str = "MSB_HANDOFF_INIT_ARGS";
366
367/// Working directory for the handoff init binary.
368///
369/// Docker applies `WORKDIR` before executing `ENTRYPOINT + CMD`. Init handoff
370/// uses this optional path so image-declared init entrypoints receive the same
371/// process cwd as they would under container startup.
372///
373/// Example:
374/// - `MSB_HANDOFF_INIT_CWD=/opt/app`
375pub const ENV_HANDOFF_INIT_CWD: &str = "MSB_HANDOFF_INIT_CWD";
376
377/// Extra environment variables for the handoff init binary.
378///
379/// Format: base64url-no-padding encoded JSON array of `[key, value]`
380/// pairs. Merged on top of the inherited env.
381/// This uses the same structured payload exception as
382/// [`ENV_HANDOFF_INIT_ARGS`] so env values can contain the delimiter
383/// characters used by older `MSB_*` boot env formats.
384///
385/// Example:
386/// - `MSB_HANDOFF_INIT_ENV=W1siY29udGFpbmVyIiwibWljcm9zYW5kYm94Il1d`
387pub const ENV_HANDOFF_INIT_ENV: &str = "MSB_HANDOFF_INIT_ENV";
388
389/// Guest-side path to the CA certificate for TLS interception.
390///
391/// Placed by the sandbox process via the runtime virtiofs mount.
392/// agentd checks for this file during init and installs it into the guest
393/// trust store.
394pub const GUEST_TLS_CA_PATH: &str = "/.msb/tls/ca.pem";
395
396/// Guest-side path to a PEM bundle of the host's extra trusted CAs.
397///
398/// Placed by the sandbox process via the runtime virtiofs mount when
399/// host-CA trust is enabled (default). agentd checks for this file during
400/// init and appends it to the guest's trust bundle, so outbound TLS works
401/// even behind a corporate MITM proxy whose gateway CA is installed on
402/// the host but unknown to the guest.
403pub const GUEST_TLS_HOST_CAS_PATH: &str = "/.msb/tls/host-cas.pem";
404
405//--------------------------------------------------------------------------------------------------
406// Exports
407//--------------------------------------------------------------------------------------------------
408
409pub mod codec;
410pub mod core;
411pub mod exec;
412pub mod fs;
413pub mod heartbeat;
414pub mod message;
415pub mod tcp;
416
417pub use error::*;