Skip to main content

boatramp_node/
compute.rs

1//! Compute-backend assembly (moved from the binary — node-library N2).
2//!
3//! Builds this node's compute [`BackendRegistry`](boatramp_core::compute::BackendRegistry)
4//! and scheduler [`Node`](boatramp_core::compute::Node) inventory from the optional
5//! `[compute]` config, capability-detecting Docker, native-container, and
6//! embedded-VMM backends. Lives here (not in the backend-agnostic
7//! `boatramp-server`) because it depends on the concrete backend crates; the
8//! binary and future `assemble()` call [`build_compute`].
9
10use std::sync::Arc;
11
12/// The posture-scaled kernel-trust gate wired into the compute backends: it runs
13/// [`boatramp_core::kernel_trust::verify_kernel`] on the staged kernel right
14/// before boot. The always-on check is the content hash; under the strict
15/// (multi-tenant) posture it additionally requires the pinned hash to be on the
16/// static allow-list and to carry a signature — sourced from the **live fleet
17/// default kernel** — verifying against a static signing key. No daemon, or a hash
18/// that isn't the current signed default, has no signature source and so **fails
19/// closed** under strict: the kernel does not boot.
20#[cfg(target_os = "linux")]
21struct PostureKernelVerifier {
22    strict: bool,
23    signing_keys: Vec<String>,
24    allowed_hashes: Vec<String>,
25    daemon: Option<Arc<boatramp_server::DaemonRuntime>>,
26}
27
28// `KernelVerifier` requires `Debug`, but `DaemonRuntime` isn't `Debug` (it holds a
29// lock + a `Notify`); summarise instead of recursing into it.
30#[cfg(target_os = "linux")]
31impl std::fmt::Debug for PostureKernelVerifier {
32    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
33        f.debug_struct("PostureKernelVerifier")
34            .field("strict", &self.strict)
35            .field("signing_keys", &self.signing_keys.len())
36            .field("allowed_hashes", &self.allowed_hashes.len())
37            .field("has_daemon", &self.daemon.is_some())
38            .finish()
39    }
40}
41
42#[cfg(target_os = "linux")]
43impl boatramp_firecracker::KernelVerifier for PostureKernelVerifier {
44    // Fully-qualified: this module aliases `Result<T>` to its own error type.
45    fn verify(&self, bytes: &[u8], expected_hash: &str) -> std::result::Result<(), String> {
46        // The only signature we trust for this hash is the one on the current
47        // fleet default kernel (the operator-vetted kernel); any other hash has no
48        // signature source and fails the strict bar.
49        let sig = self
50            .daemon
51            .as_ref()
52            .and_then(|d| d.effective().default_kernel.clone())
53            .filter(|dk| dk.sha256 == expected_hash)
54            .and_then(|dk| dk.sig);
55        let kref = boatramp_core::daemon_config::KernelRef {
56            source: expected_hash.to_string(),
57            sha256: expected_hash.to_string(),
58            sig,
59        };
60        boatramp_core::kernel_trust::verify_kernel(
61            bytes,
62            &kref,
63            self.strict,
64            &self.signing_keys,
65            &self.allowed_hashes,
66        )
67        .map_err(|e| e.to_string())
68    }
69}
70
71/// The macOS-VMM twin of [`PostureKernelVerifier`], implementing
72/// [`boatramp_vz::KernelVerifier`] with the identical posture-scaled trust logic
73/// so the Virtualization.framework backend enforces the same verify-before-boot
74/// bar as the KVM backend (the kernel is ring-0 code on either substrate).
75#[cfg(target_os = "macos")]
76struct VzPostureKernelVerifier {
77    strict: bool,
78    signing_keys: Vec<String>,
79    allowed_hashes: Vec<String>,
80    daemon: Option<Arc<boatramp_server::DaemonRuntime>>,
81}
82
83#[cfg(target_os = "macos")]
84impl std::fmt::Debug for VzPostureKernelVerifier {
85    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
86        f.debug_struct("VzPostureKernelVerifier")
87            .field("strict", &self.strict)
88            .field("signing_keys", &self.signing_keys.len())
89            .field("allowed_hashes", &self.allowed_hashes.len())
90            .field("has_daemon", &self.daemon.is_some())
91            .finish()
92    }
93}
94
95#[cfg(target_os = "macos")]
96impl boatramp_vz::KernelVerifier for VzPostureKernelVerifier {
97    fn verify(&self, bytes: &[u8], expected_hash: &str) -> std::result::Result<(), String> {
98        let sig = self
99            .daemon
100            .as_ref()
101            .and_then(|d| d.effective().default_kernel.clone())
102            .filter(|dk| dk.sha256 == expected_hash)
103            .and_then(|dk| dk.sig);
104        let kref = boatramp_core::daemon_config::KernelRef {
105            source: expected_hash.to_string(),
106            sha256: expected_hash.to_string(),
107            sig,
108        };
109        boatramp_core::kernel_trust::verify_kernel(
110            bytes,
111            &kref,
112            self.strict,
113            &self.signing_keys,
114            &self.allowed_hashes,
115        )
116        .map_err(|e| e.to_string())
117    }
118}
119
120/// Whether this host can run the macOS VMM backend: **Apple silicon** (arm64) on
121/// **macOS 15+** (the Virtualization.framework Linux-container floor). Detected via
122/// `sysctl` — `hw.optional.arm64 == 1` and `kern.osproductversion >= 15`. macOS 26
123/// is recommended (macOS 15 lacks container-to-container networking over vmnet),
124/// but single-node serve works on 15, so 15 is the floor; the operator's macOS
125/// version determines multi-replica cross-VM reachability, not the user surface.
126#[cfg(target_os = "macos")]
127fn macos_supports_vz() -> bool {
128    // Apple silicon: the Virtualization.framework Linux path is arm64-only.
129    if cfg!(not(target_arch = "aarch64")) {
130        return false;
131    }
132    let major = sysctl_string("kern.osproductversion")
133        .and_then(|v| v.split('.').next().and_then(|m| m.parse::<u32>().ok()));
134    matches!(major, Some(m) if m >= 15)
135}
136
137/// Read a string `sysctl` by name (e.g. `kern.osproductversion`). `None` on any
138/// failure — the caller treats an unreadable sysctl as "unsupported" (fail-closed).
139#[cfg(target_os = "macos")]
140fn sysctl_string(name: &str) -> Option<String> {
141    let out = std::process::Command::new("sysctl")
142        .args(["-n", name])
143        .output()
144        .ok()?;
145    if !out.status.success() {
146        return None;
147    }
148    Some(String::from_utf8_lossy(&out.stdout).trim().to_string())
149}
150
151/// Build this node's compute [`BackendRegistry`] + scheduler [`Node`] inventory
152/// from the optional `[compute]` config. Backends
153/// are **capability-detected**: a reachable Docker daemon ⇒ `docker`; Linux ⇒ the
154/// native `container` backend; Linux + `/dev/kvm` ⇒ the in-process
155/// `vmm-embedded` microVM backend (strongest isolation). Absent config ⇒ an empty
156/// registry + a node advertising nothing, so the reconcile loop stays a no-op.
157pub async fn build_compute(
158    cfg: Option<&crate::config::ComputeConfig>,
159    storage: std::sync::Arc<dyn boatramp_core::Storage>,
160    data_dir: &std::path::Path,
161    node_id: u64,
162    strict: bool,
163    daemon: Option<Arc<boatramp_server::DaemonRuntime>>,
164    // The binary the re-exec'd container/microVM workers run as; `None` ⇒ this
165    // process's own executable (`current_exe`). An embedding harness points it at a
166    // built `boatramp` binary so the workers find the `__sandbox`/`__vmm-run`/
167    // `__vz-run` subcommands. See [`crate::node::NodeInput::worker_exe`].
168    worker_exe: Option<&std::path::Path>,
169) -> (
170    boatramp_core::compute::BackendRegistry,
171    boatramp_core::compute::Node,
172) {
173    use boatramp_core::compute::{BackendKind, BackendRegistry, Node};
174    let mut backends: BackendRegistry = std::collections::BTreeMap::new();
175    let empty_node = |id| Node {
176        id,
177        region: None,
178        labels: std::collections::BTreeMap::new(),
179        free_vcpus: 0,
180        free_mem_mib: 0,
181        backends: Vec::new(),
182    };
183    let Some(cfg) = cfg else {
184        return (backends, empty_node(node_id));
185    };
186
187    // Remote docker: register only if a daemon actually answers.
188    match boatramp_docker::DockerBackend::connect() {
189        Ok(docker) => {
190            // `writable_root` and `cap_add` are honored only under the single-tenant
191            // posture (`!strict`); the multi-tenant guard keeps the hardened read-only
192            // root and every capability dropped.
193            let docker = docker
194                .with_endpoint(cfg.docker_endpoint)
195                .with_volume_mode(cfg.docker_volume_mode)
196                .with_data_dir(data_dir)
197                .with_writable_root_allowed(!strict)
198                .with_cap_add_allowed(!strict);
199            if docker.reachable().await {
200                backends.insert("docker".to_string(), std::sync::Arc::new(docker));
201            } else {
202                tracing::debug!("no reachable docker daemon; skipping docker backend");
203            }
204        }
205        Err(e) => tracing::debug!(%e, "docker backend unavailable"),
206    }
207
208    // Native container backend (Linux only).
209    #[cfg(target_os = "linux")]
210    match worker_exe.map_or_else(std::env::current_exe, |p| Ok(p.to_path_buf())) {
211        Ok(self_exe) => match boatramp_container::ContainerBackend::new(
212            storage.clone(),
213            data_dir.to_path_buf(),
214            cfg.bridge.clone(),
215            &cfg.subnet,
216            self_exe,
217        ) {
218            Ok(c) => {
219                // Single-tenant posture (`!strict`) may honor `cap_add`; multi-tenant
220                // keeps every capability dropped.
221                let c = c.with_cap_add_allowed(!strict);
222                backends.insert("container".to_string(), std::sync::Arc::new(c));
223            }
224            Err(e) => tracing::warn!(%e, "container backend unavailable"),
225        },
226        Err(e) => tracing::warn!(%e, "current_exe for container backend"),
227    }
228    // Embedded VMM backend (Linux + x86_64 + `/dev/kvm`): in-process microVMs, no
229    // external `firecracker` binary — the strongest isolation when KVM is available.
230    // Like the container backend it enslaves each tap to `cfg.bridge` (assumed set
231    // up). The embedded VMM is KVM-x86-specific, so this is x86_64-only; boatramp
232    // still serves on linux/aarch64 (with the container backend, no embedded VMM).
233    #[cfg(all(target_os = "linux", target_arch = "x86_64"))]
234    if std::path::Path::new("/dev/kvm").exists() {
235        match (
236            worker_exe.map_or_else(std::env::current_exe, |p| Ok(p.to_path_buf())),
237            boatramp_core::ipam::IpPool::new(&cfg.subnet),
238        ) {
239            (Ok(self_exe), Ok(pool)) => {
240                let gateway = pool.gateway().to_string();
241                // Verify-before-boot gate for every kernel this backend stages.
242                let verifier: Arc<dyn boatramp_firecracker::KernelVerifier> =
243                    Arc::new(PostureKernelVerifier {
244                        strict,
245                        signing_keys: cfg.kernel_signing_pubkeys.clone(),
246                        allowed_hashes: cfg.kernel_allowed_hashes.clone(),
247                        daemon: daemon.clone(),
248                    });
249                match boatramp_firecracker::EmbeddedVmmBackend::new(
250                    storage.clone(),
251                    self_exe, // re-exec'd as `__vmm-run` per VM (jailed subprocess)
252                    data_dir.to_path_buf(),
253                    cfg.bridge.clone(),
254                    gateway,
255                    &cfg.subnet,
256                    verifier,
257                ) {
258                    Ok(vmm) => {
259                        backends.insert("vmm-embedded".to_string(), std::sync::Arc::new(vmm));
260                    }
261                    Err(e) => tracing::warn!(%e, "embedded VMM backend unavailable"),
262                }
263            }
264            (Err(e), _) => tracing::warn!(%e, "current_exe for VMM backend"),
265            (_, Err(e)) => tracing::warn!(%e, "bad compute subnet for VMM backend"),
266        }
267    } else {
268        tracing::debug!("no /dev/kvm; skipping embedded VMM backend");
269    }
270
271    // macOS-native VMM backend (Apple silicon + macOS): each replica is a Linux
272    // microVM under Virtualization.framework, run by a re-exec'd `__vz-run`
273    // worker. Strong isolation (VmKvm), matching the KVM backend's user surface.
274    // Capability-detected + log-skipped on Intel / older macOS, exactly like the
275    // `/dev/kvm` check gates the Linux VMM.
276    #[cfg(target_os = "macos")]
277    if macos_supports_vz() {
278        match (
279            worker_exe.map_or_else(std::env::current_exe, |p| Ok(p.to_path_buf())),
280            boatramp_core::ipam::IpPool::new(&cfg.subnet),
281        ) {
282            (Ok(self_exe), Ok(_pool)) => {
283                let verifier: Arc<dyn boatramp_vz::KernelVerifier> =
284                    Arc::new(VzPostureKernelVerifier {
285                        strict,
286                        signing_keys: cfg.kernel_signing_pubkeys.clone(),
287                        allowed_hashes: cfg.kernel_allowed_hashes.clone(),
288                        daemon: daemon.clone(),
289                    });
290                match boatramp_vz::VzBackend::new(
291                    storage.clone(),
292                    self_exe, // re-exec'd as `__vz-run` per VM
293                    data_dir.to_path_buf(),
294                    &cfg.subnet, // the vmnet range (e.g. 192.168.64.0/24); `.1` = gateway
295                    verifier,
296                ) {
297                    // `writable_root` honored only under the single-tenant posture.
298                    Ok(vz) => {
299                        let vz = vz.with_writable_root_allowed(!strict);
300                        backends.insert("vmm-vz".to_string(), std::sync::Arc::new(vz));
301                    }
302                    Err(e) => tracing::warn!(%e, "macOS VMM backend unavailable"),
303                }
304            }
305            (Err(e), _) => tracing::warn!(%e, "current_exe for macOS VMM backend"),
306            (_, Err(e)) => tracing::warn!(%e, "bad compute subnet for macOS VMM backend"),
307        }
308    } else {
309        tracing::debug!("not Apple silicon + macOS 15+; skipping macOS VMM backend");
310    }
311
312    let _ = (&storage, data_dir); // used only on Linux/macOS (container / VMM backends)
313                                  // The kernel-trust verifier is wired for the embedded VMM (x86_64 Linux) and
314                                  // the macOS VMM; silence `strict`/`daemon` on the platforms that wire neither
315                                  // (linux/aarch64, and any non-Linux non-macOS host).
316    #[cfg(not(any(all(target_os = "linux", target_arch = "x86_64"), target_os = "macos")))]
317    let _ = (strict, &daemon);
318
319    let free_vcpus = if cfg.vcpus > 0 {
320        cfg.vcpus
321    } else {
322        std::thread::available_parallelism()
323            .map(|n| n.get() as u32)
324            .unwrap_or(1)
325    };
326    let free_mem_mib = if cfg.mem_mib > 0 { cfg.mem_mib } else { 1024 };
327    let advertised: Vec<BackendKind> = backends
328        .iter()
329        .map(|(id, b)| {
330            let caps = b.capabilities();
331            BackendKind {
332                id: id.clone(),
333                isolation: caps.isolation,
334                persistent_volumes: caps.persistent_volumes,
335                scale_to_zero: caps.scale_to_zero,
336            }
337        })
338        .collect();
339    tracing::info!(backends = ?advertised, free_vcpus, free_mem_mib, "compute node inventory");
340    let node = Node {
341        id: node_id,
342        region: cfg.region.clone(),
343        labels: std::collections::BTreeMap::new(),
344        free_vcpus,
345        free_mem_mib,
346        backends: advertised,
347    };
348    (backends, node)
349}