| 1 | //! Inside the sandbox's VM: `g1t-runner sandbox guest …`, which the |
| 2 | //! launcher on the computer runs (through `wsl.exe --exec` on Windows, as |
| 3 | //! the VM's systemd service on macOS and Linux). |
| 4 | //! |
| 5 | //! `guest run` is the VM's supervisor. It |
| 6 | //! |
| 7 | //! 1. on WSL, takes away what WSL puts in every distribution that reaches |
| 8 | //! Windows: `/mnt/wsl` (shared by every distribution, where Docker |
| 9 | //! Desktop keeps sockets to Windows' Docker), WSLg's display and sound |
| 10 | //! sockets, Windows' GPU driver folders, and `/dev/dxg`; and checks |
| 11 | //! `/etc/wsl.conf` is the image's, which turns off drives and Windows |
| 12 | //! programs ([`WSL_CONF`]); |
| 13 | //! 2. refuses to go on if anything of the computer's is still mounted, or |
| 14 | //! a container engine's socket other than its own is reachable |
| 15 | //! ([`problems`]); |
| 16 | //! 3. starts Sysbox and a Docker Engine of the VM's own, and adds rules |
| 17 | //! that keep every container off the computer's own address (the VM's |
| 18 | //! gateway) but for DNS ([`firewall_rules`]); |
| 19 | //! 4. runs the runner's image under `--runtime=sysbox-runc` with the |
| 20 | //! registration on `/var/lib/g1t-sandbox/state`, and keeps saying how |
| 21 | //! it is in `/var/lib/g1t-sandbox/status.json` (and on the console); |
| 22 | //! 5. on `guest stop` (or SIGTERM), asks the runner to drain or stop, |
| 23 | //! waits for it, and stops everything. |
| 24 | //! |
| 25 | //! Root in this VM is not a wall on WSL: every WSL distribution, the |
| 26 | //! person's own among them, runs in one VM, and root in any of them can |
| 27 | //! reach the others' disks. That is why the runner runs under Sysbox here |
| 28 | //! and its jobs in containers inside it: a job is two walls from this |
| 29 | //! VM's root, and three from the computer. |
| 30 | |
| 31 | use std::net::Ipv4Addr; |
| 32 | use std::path::Path; |
| 33 | |
| 34 | use anyhow::{Context, Result, bail}; |
| 35 | use serde::{Deserialize, Serialize}; |
| 36 | use serde_json::{Value, json}; |
| 37 | |
| 38 | use crate::selfhosted::RunnerStatus; |
| 39 | |
| 40 | pub const ROOT: &str = "/var/lib/g1t-sandbox"; |
| 41 | /// The runner's folder (`/data` in its container): its registration, and |
| 42 | /// the status file it keeps. |
| 43 | pub const STATE_DIR: &str = "/var/lib/g1t-sandbox/state"; |
| 44 | pub const SETTINGS: &str = "/var/lib/g1t-sandbox/settings.json"; |
| 45 | pub const REGISTER_ENV: &str = "/var/lib/g1t-sandbox/register.env"; |
| 46 | pub const STATUS: &str = "/var/lib/g1t-sandbox/status.json"; |
| 47 | /// `drain` or `now`: asks the supervisor to stop. |
| 48 | pub const STOP_FILE: &str = "/var/lib/g1t-sandbox/stop"; |
| 49 | /// A runner image the launcher put here for the supervisor to load |
| 50 | /// (`sandbox up --runner-image-file`, for a build that is not released). |
| 51 | pub const IMAGE_TAR: &str = "/var/lib/g1t-sandbox/runner-image.tar"; |
| 52 | pub const RUN_DIR: &str = "/run/g1t-sandbox"; |
| 53 | /// The VM's Docker Engine: not `/var/run/docker.sock`. |
| 54 | pub const DOCKER_SOCKET: &str = "/run/g1t-sandbox/docker.sock"; |
| 55 | pub const PID_FILE: &str = "/run/g1t-sandbox/supervisor.pid"; |
| 56 | pub const RUNNER_CONTAINER: &str = "g1t-runner"; |
| 57 | /// How a status line starts on a VM's console. |
| 58 | pub const STATUS_PREFIX: &str = "G1T-SANDBOX-STATUS "; |
| 59 | |
| 60 | /// The image's `/etc/wsl.conf`. |
| 61 | pub const WSL_CONF: &str = include_str!("../../../../../deploy/sandbox/wsl.conf"); |
| 62 | |
| 63 | /// What WSL mounts into every distribution that reaches Windows or other |
| 64 | /// distributions, taken away at start. |
| 65 | pub const WSL_CHANNELS: &[&str] = &["/mnt/wsl", "/mnt/wslg", "/tmp/.X11-unix", "/usr/lib/wsl/drivers", "/usr/lib/wsl/lib"]; |
| 66 | |
| 67 | /// What the launcher tells the guest (`guest configure`, or the seed disk). |
| 68 | #[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq)] |
| 69 | pub struct Settings { |
| 70 | /// The site to register with, and a registration token: only until the |
| 71 | /// runner has registered. |
| 72 | #[serde(default)] |
| 73 | pub url: Option<String>, |
| 74 | #[serde(default)] |
| 75 | pub token: Option<String>, |
| 76 | #[serde(default)] |
| 77 | pub name: Option<String>, |
| 78 | #[serde(default)] |
| 79 | pub labels: Vec<String>, |
| 80 | #[serde(default)] |
| 81 | pub group: Option<String>, |
| 82 | /// The runner's container's share of the VM, and so its jobs'. |
| 83 | #[serde(default)] |
| 84 | pub cpus: u32, |
| 85 | #[serde(default)] |
| 86 | pub memory_mib: u64, |
| 87 | /// The runner's image, by digest from the signed release. |
| 88 | #[serde(default)] |
| 89 | pub runner_image: String, |
| 90 | #[serde(default)] |
| 91 | pub image_version: String, |
| 92 | /// For a VM, how to stop when it is shut down: `drain` or `now`. |
| 93 | #[serde(default)] |
| 94 | pub stop: Option<String>, |
| 95 | } |
| 96 | |
| 97 | /// What `guest status` says, and the launcher reports. |
| 98 | #[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq)] |
| 99 | pub struct GuestStatus { |
| 100 | /// `starting`, `running`, `stopping`, `stopped` or `error`. |
| 101 | pub state: String, |
| 102 | #[serde(default)] |
| 103 | pub runner: Option<RunnerStatus>, |
| 104 | #[serde(default)] |
| 105 | pub error: Option<String>, |
| 106 | #[serde(default)] |
| 107 | pub image_version: Option<String>, |
| 108 | #[serde(default)] |
| 109 | pub updated_at: String, |
| 110 | } |
| 111 | |
| 112 | /// A mount that is the computer's, or WSL's way to it, from |
| 113 | /// `/proc/self/mountinfo`. |
| 114 | pub fn host_mounts(mountinfo: &str) -> Vec<String> { |
| 115 | let mut found = Vec::new(); |
| 116 | for line in mountinfo.lines() { |
| 117 | let fields: Vec<&str> = line.split_whitespace().collect(); |
| 118 | if fields.len() < 5 { |
| 119 | continue; |
| 120 | } |
| 121 | let point = fields[4]; |
| 122 | let fstype = line.split(" - ").nth(1).and_then(|rest| rest.split_whitespace().next()).unwrap_or(""); |
| 123 | let shared_fs = matches!(fstype, "9p" | "drvfs" | "virtiofs" | "vboxsf" | "prl_fs" | "vmhgfs" | "fuse.vmhgfs-fuse" | "smb3" | "cifs"); |
| 124 | let channel = WSL_CHANNELS.iter().any(|c| point == *c || point.starts_with(&format!("{c}/"))); |
| 125 | if shared_fs || channel || point.starts_with("/mnt/") { |
| 126 | found.push(format!("{point} ({fstype})")); |
| 127 | } |
| 128 | } |
| 129 | found |
| 130 | } |
| 131 | |
| 132 | /// What keeps the VM from being a sandbox: mounts of the computer's, an |
| 133 | /// engine socket that is not this VM's own, WSL's interop with Windows. |
| 134 | pub fn problems(mountinfo: &str, sockets: &[String], interop: bool, wsl_conf: Option<&str>) -> Vec<String> { |
| 135 | let mut problems: Vec<String> = host_mounts(mountinfo).into_iter().map(|m| format!("{m} is mounted from the computer, or is WSL's way to it")).collect(); |
| 136 | for socket in sockets { |
| 137 | problems.push(format!("{socket} is a container engine's socket that is not this sandbox's own")); |
| 138 | } |
| 139 | if interop { |
| 140 | problems.push("WSL interop is on: Windows programs can be started from inside the sandbox".to_owned()); |
| 141 | } |
| 142 | if let Some(conf) = wsl_conf |
| 143 | && conf.trim() != WSL_CONF.trim() |
| 144 | { |
| 145 | problems.push("/etc/wsl.conf is not the sandbox image's, which turns off Windows drives and programs".to_owned()); |
| 146 | } |
| 147 | problems |
| 148 | } |
| 149 | |
| 150 | /// The VM's default gateway, which is the computer, from `/proc/net/route` |
| 151 | /// (addresses in little-endian hex). |
| 152 | pub fn gateway(route: &str) -> Option<Ipv4Addr> { |
| 153 | route.lines().skip(1).find_map(|line| { |
| 154 | let fields: Vec<&str> = line.split_whitespace().collect(); |
| 155 | if fields.len() > 2 && fields[1] == "00000000" { |
| 156 | u32::from_str_radix(fields[2], 16).ok().map(|n| Ipv4Addr::from(n.swap_bytes())).filter(|a| !a.is_unspecified()) |
| 157 | } else { |
| 158 | None |
| 159 | } |
| 160 | }) |
| 161 | } |
| 162 | |
| 163 | /// The sandbox's `/etc/resolv.conf`: the resolvers WSL offered, else the |
| 164 | /// gateway, which answers DNS on WSL's NAT. |
| 165 | pub fn resolv_conf(offered: &str, gateway: Option<Ipv4Addr>) -> String { |
| 166 | let mut servers = nameservers(offered); |
| 167 | if servers.is_empty() { |
| 168 | servers.extend(gateway); |
| 169 | } |
| 170 | servers.iter().map(|s| format!("nameserver {s}\n")).collect() |
| 171 | } |
| 172 | |
| 173 | pub fn nameservers(resolv: &str) -> Vec<Ipv4Addr> { |
| 174 | resolv |
| 175 | .lines() |
| 176 | .filter_map(|l| l.trim().strip_prefix("nameserver")) |
| 177 | .filter_map(|a| a.trim().parse().ok()) |
| 178 | .collect() |
| 179 | } |
| 180 | |
| 181 | /// `iptables` arguments, in order, for the `DOCKER-USER` chain, which |
| 182 | /// every container's traffic passes: DNS to the VM's resolvers, and |
| 183 | /// nothing else to the computer's address (the gateway), to a resolver |
| 184 | /// the computer answers on, or to link-local addresses. The internet |
| 185 | /// stays reachable, as a job needs. |
| 186 | pub fn firewall_rules(gateway: Option<Ipv4Addr>, resolvers: &[Ipv4Addr]) -> Vec<Vec<String>> { |
| 187 | let s = |items: &[&str]| items.iter().map(|i| i.to_string()).collect::<Vec<String>>(); |
| 188 | let mut rules = Vec::new(); |
| 189 | let mut walled: Vec<Ipv4Addr> = gateway.into_iter().collect(); |
| 190 | for resolver in resolvers { |
| 191 | if resolver.is_loopback() { |
| 192 | continue; |
| 193 | } |
| 194 | let address = resolver.to_string(); |
| 195 | for proto in ["udp", "tcp"] { |
| 196 | rules.push(s(&["-d", &address, "-p", proto, "--dport", "53", "-j", "ACCEPT"])); |
| 197 | } |
| 198 | // A resolver that is the computer, or WSL's DNS tunnel, takes DNS |
| 199 | // and nothing else. |
| 200 | if (resolver.is_private() || resolver.is_link_local()) && !walled.contains(resolver) { |
| 201 | walled.push(*resolver); |
| 202 | } |
| 203 | } |
| 204 | for address in walled { |
| 205 | rules.push(s(&["-d", &address.to_string(), "-j", "DROP"])); |
| 206 | } |
| 207 | rules.push(s(&["-d", "169.254.0.0/16", "-j", "DROP"])); |
| 208 | rules |
| 209 | } |
| 210 | |
| 211 | /// The VM's Docker Engine: Sysbox as a runtime, its own socket, address |
| 212 | /// ranges away from Docker's defaults (the runner's own engine, inside, |
| 213 | /// uses those). |
| 214 | pub fn daemon_json() -> Value { |
| 215 | json!({ |
| 216 | "hosts": [format!("unix://{DOCKER_SOCKET}")], |
| 217 | "pidfile": format!("{RUN_DIR}/dockerd.pid"), |
| 218 | "runtimes": { "sysbox-runc": { "path": "/usr/local/bin/sysbox-runc" } }, |
| 219 | "bip": "172.29.254.1/24", |
| 220 | "default-address-pools": [{ "base": "172.28.0.0/16", "size": 24 }], |
| 221 | "log-driver": "json-file", |
| 222 | "log-opts": { "max-size": "20m", "max-file": "2" }, |
| 223 | }) |
| 224 | } |
| 225 | |
| 226 | /// The registration's variables, for `register-and-run` in the runner's |
| 227 | /// image: a file, so the token is on no command line. |
| 228 | pub fn register_env(settings: &Settings) -> Result<String> { |
| 229 | let (Some(url), Some(token)) = (&settings.url, &settings.token) else { bail!("the sandbox's runner is not registered, and no --url and --token were given") }; |
| 230 | let mut lines = vec![format!("G1T_RUNNER_URL={url}"), format!("G1T_RUNNER_TOKEN={token}")]; |
| 231 | if let Some(name) = &settings.name { |
| 232 | lines.push(format!("G1T_RUNNER_NAME={name}")); |
| 233 | } |
| 234 | if !settings.labels.is_empty() { |
| 235 | lines.push(format!("G1T_RUNNER_LABELS={}", settings.labels.join(","))); |
| 236 | } |
| 237 | if let Some(group) = &settings.group { |
| 238 | lines.push(format!("G1T_RUNNER_GROUP={group}")); |
| 239 | } |
| 240 | if lines.iter().any(|l| l.contains('\n') || l.contains('\r')) { |
| 241 | bail!("a registration setting has a line break in it"); |
| 242 | } |
| 243 | Ok(lines.join("\n") + "\n") |
| 244 | } |
| 245 | |
| 246 | /// `docker run` for the runner in the VM. |
| 247 | pub fn runner_run_args(settings: &Settings, register: bool) -> Vec<String> { |
| 248 | sysbox_run_args(settings, RUNNER_CONTAINER, STATE_DIR, register.then_some(REGISTER_ENV)) |
| 249 | } |
| 250 | |
| 251 | /// `docker run` for the runner: its image under Sysbox, `data` (a folder |
| 252 | /// or a volume) as `/data`, no socket, no host network, its share of the |
| 253 | /// machine, and no self-update (the launcher updates the whole sandbox). |
| 254 | pub fn sysbox_run_args(settings: &Settings, name: &str, data: &str, env_file: Option<&str>) -> Vec<String> { |
| 255 | let mut args: Vec<String> = ["run", "-d", "--name", name, "--runtime=sysbox-runc", "--restart", "on-failure:5"] |
| 256 | .iter() |
| 257 | .map(|s| s.to_string()) |
| 258 | .collect(); |
| 259 | if settings.cpus > 0 { |
| 260 | args.extend(["--cpus".into(), settings.cpus.to_string()]); |
| 261 | } |
| 262 | if settings.memory_mib > 0 { |
| 263 | let memory = format!("{}m", settings.memory_mib); |
| 264 | args.extend(["--memory".into(), memory.clone(), "--memory-swap".into(), memory]); |
| 265 | } |
| 266 | args.extend(["-v".into(), format!("{data}:/data")]); |
| 267 | args.extend(["-e".into(), "G1T_RUNNER_NO_AUTO_UPDATE=1".into()]); |
| 268 | if let Some(file) = env_file { |
| 269 | args.extend(["--env-file".into(), file.into()]); |
| 270 | } |
| 271 | args.extend(["--label".into(), "sh.g1t.sandbox=runner".into()]); |
| 272 | args.push(settings.runner_image.clone()); |
| 273 | args |
| 274 | } |
| 275 | |
| 276 | /// Reads a seed disk: JSON, then zeros. |
| 277 | pub fn read_seed(bytes: &[u8]) -> Result<Settings> { |
| 278 | let end = bytes.iter().position(|b| *b == 0).unwrap_or(bytes.len()); |
| 279 | serde_json::from_slice(&bytes[..end]).context("the seed disk does not read") |
| 280 | } |
| 281 | |
| 282 | /// A seed disk's bytes: the settings' JSON, padded to `size`. |
| 283 | pub fn write_seed(settings: &Settings, size: usize) -> Result<Vec<u8>> { |
| 284 | let mut bytes = serde_json::to_vec(settings)?; |
| 285 | if bytes.len() >= size { |
| 286 | bail!("the sandbox's settings do not fit its seed disk"); |
| 287 | } |
| 288 | bytes.resize(size, 0); |
| 289 | Ok(bytes) |
| 290 | } |
| 291 | |
| 292 | /// The last status line on a VM's console. |
| 293 | pub fn status_from_console(log: &str) -> Option<GuestStatus> { |
| 294 | log.lines().rev().find_map(|line| line.split_once(STATUS_PREFIX).and_then(|(_, json)| serde_json::from_str(json.trim()).ok())) |
| 295 | } |
| 296 | |
| 297 | fn read_json<T: for<'a> Deserialize<'a>>(path: &str) -> Option<T> { |
| 298 | std::fs::read(path).ok().and_then(|bytes| serde_json::from_slice(&bytes).ok()) |
| 299 | } |
| 300 | |
| 301 | fn write_private(path: &str, bytes: &[u8]) -> Result<()> { |
| 302 | let temp = format!("{path}.new"); |
| 303 | std::fs::write(&temp, bytes).with_context(|| format!("could not write {temp}"))?; |
| 304 | #[cfg(unix)] |
| 305 | { |
| 306 | use std::os::unix::fs::PermissionsExt; |
| 307 | std::fs::set_permissions(&temp, std::fs::Permissions::from_mode(0o600))?; |
| 308 | } |
| 309 | std::fs::rename(&temp, path).with_context(|| format!("could not write {path}"))?; |
| 310 | Ok(()) |
| 311 | } |
| 312 | |
| 313 | fn registered() -> bool { |
| 314 | Path::new(STATE_DIR).join("config.json").exists() |
| 315 | } |
| 316 | |
| 317 | /// `g1t-runner sandbox guest <command>`. |
| 318 | pub fn main(args: &[String]) -> Result<()> { |
| 319 | let command = args.first().map(String::as_str).unwrap_or(""); |
| 320 | let value = |name: &str| args.windows(2).find(|w| w[0] == name).map(|w| w[1].clone()); |
| 321 | match command { |
| 322 | "configure" => configure(&read_stdin()?), |
| 323 | "status" => { |
| 324 | println!("{}", serde_json::to_string(¤t_status())?); |
| 325 | Ok(()) |
| 326 | } |
| 327 | "export-state" => { |
| 328 | use std::io::Write; |
| 329 | let bytes = std::fs::read(Path::new(STATE_DIR).join("config.json")).context("the sandbox's runner is not registered")?; |
| 330 | std::io::stdout().write_all(&bytes)?; |
| 331 | Ok(()) |
| 332 | } |
| 333 | "import-state" => { |
| 334 | std::fs::create_dir_all(STATE_DIR)?; |
| 335 | let bytes = read_stdin()?; |
| 336 | serde_json::from_slice::<Value>(&bytes).context("that is not a runner's configuration")?; |
| 337 | write_private(&format!("{STATE_DIR}/config.json"), &bytes) |
| 338 | } |
| 339 | "load-image" => { |
| 340 | std::fs::create_dir_all(ROOT)?; |
| 341 | let bytes = read_stdin()?; |
| 342 | write_private(IMAGE_TAR, &bytes) |
| 343 | } |
| 344 | "check" => { |
| 345 | let found = check(); |
| 346 | if found.is_empty() { |
| 347 | println!("Nothing of the computer's reaches this sandbox."); |
| 348 | Ok(()) |
| 349 | } else { |
| 350 | bail!("this sandbox can reach the computer:\n - {}", found.join("\n - ")) |
| 351 | } |
| 352 | } |
| 353 | "stop" => stop(if args.iter().any(|a| a == "--now") { "now" } else { "drain" }, value("--wait").and_then(|w| w.parse().ok())), |
| 354 | "run" => run(value("--seed"), value("--state-disk"), value("--console")), |
| 355 | _ => bail!("guest takes configure, run, status, stop, check, export-state, import-state or load-image"), |
| 356 | } |
| 357 | } |
| 358 | |
| 359 | fn read_stdin() -> Result<Vec<u8>> { |
| 360 | use std::io::Read; |
| 361 | let mut bytes = Vec::new(); |
| 362 | std::io::stdin().read_to_end(&mut bytes)?; |
| 363 | // Windows' tools may put a byte-order mark before JSON. |
| 364 | if bytes.starts_with(&[0xEF, 0xBB, 0xBF]) { |
| 365 | bytes.drain(..3); |
| 366 | } |
| 367 | Ok(bytes) |
| 368 | } |
| 369 | |
| 370 | /// Keeps what the launcher sent, over what was there: a token only until |
| 371 | /// the runner has registered. |
| 372 | fn configure(bytes: &[u8]) -> Result<()> { |
| 373 | let given: Settings = serde_json::from_slice(bytes).context("the sandbox's settings do not read")?; |
| 374 | std::fs::create_dir_all(STATE_DIR)?; |
| 375 | let mut settings: Settings = read_json(SETTINGS).unwrap_or_default(); |
| 376 | if given.url.is_some() { |
| 377 | settings.url = given.url; |
| 378 | } |
| 379 | if given.token.is_some() && !registered() { |
| 380 | settings.token = given.token; |
| 381 | } |
| 382 | if given.name.is_some() { |
| 383 | settings.name = given.name; |
| 384 | } |
| 385 | if !given.labels.is_empty() { |
| 386 | settings.labels = given.labels; |
| 387 | } |
| 388 | if given.group.is_some() { |
| 389 | settings.group = given.group; |
| 390 | } |
| 391 | if given.cpus > 0 { |
| 392 | settings.cpus = given.cpus; |
| 393 | } |
| 394 | if given.memory_mib > 0 { |
| 395 | settings.memory_mib = given.memory_mib; |
| 396 | } |
| 397 | if !given.runner_image.is_empty() { |
| 398 | settings.runner_image = given.runner_image; |
| 399 | } |
| 400 | if !given.image_version.is_empty() { |
| 401 | settings.image_version = given.image_version; |
| 402 | } |
| 403 | write_private(SETTINGS, &serde_json::to_vec_pretty(&settings)?) |
| 404 | } |
| 405 | |
| 406 | /// The supervisor's status, or `stopped` when it is not running. |
| 407 | fn current_status() -> GuestStatus { |
| 408 | let status: Option<GuestStatus> = read_json(STATUS); |
| 409 | let alive = supervisor_alive(); |
| 410 | match status { |
| 411 | Some(status) if alive || status.state == "error" || status.state == "stopped" => status, |
| 412 | Some(status) => GuestStatus { state: "stopped".into(), ..status }, |
| 413 | None => GuestStatus { state: "stopped".into(), ..GuestStatus::default() }, |
| 414 | } |
| 415 | } |
| 416 | |
| 417 | fn supervisor_alive() -> bool { |
| 418 | std::fs::read_to_string(PID_FILE) |
| 419 | .ok() |
| 420 | .and_then(|p| p.trim().parse::<u32>().ok()) |
| 421 | .is_some_and(|pid| Path::new(&format!("/proc/{pid}")).exists()) |
| 422 | } |
| 423 | |
| 424 | /// What reaches the computer from here, now. |
| 425 | pub fn check() -> Vec<String> { |
| 426 | let mountinfo = std::fs::read_to_string("/proc/self/mountinfo").unwrap_or_default(); |
| 427 | let mut sockets = Vec::new(); |
| 428 | // `/var/run` is `/run`. |
| 429 | for dir in ["/mnt", "/run", "/tmp"] { |
| 430 | find_sockets(Path::new(dir), 3, &mut sockets); |
| 431 | } |
| 432 | let sockets: Vec<String> = sockets.into_iter().filter(|s| !own_socket(s)).collect(); |
| 433 | let on_wsl = on_wsl(); |
| 434 | // With interop off, WSL starts no interop server for the distribution's |
| 435 | // processes, and `WSL_INTEROP` names none. |
| 436 | let interop = on_wsl && std::env::var_os("WSL_INTEROP").is_some(); |
| 437 | let conf = on_wsl.then(|| std::fs::read_to_string("/etc/wsl.conf").unwrap_or_default()); |
| 438 | problems(&mountinfo, &sockets, interop, conf.as_deref()) |
| 439 | } |
| 440 | |
| 441 | /// The sandbox's own engine's sockets, and the containerd and Sysbox it |
| 442 | /// runs. |
| 443 | pub fn own_socket(path: &str) -> bool { |
| 444 | [RUN_DIR, "/run/docker/", "/run/containerd/", "/run/sysbox/"].iter().any(|own| path.starts_with(own)) |
| 445 | } |
| 446 | |
| 447 | #[cfg_attr(not(unix), allow(clippy::only_used_in_recursion))] |
| 448 | fn find_sockets(dir: &Path, depth: u32, found: &mut Vec<String>) { |
| 449 | let Ok(entries) = std::fs::read_dir(dir) else { return }; |
| 450 | for entry in entries.flatten() { |
| 451 | let Ok(kind) = entry.file_type() else { continue }; |
| 452 | let path = entry.path(); |
| 453 | #[cfg(unix)] |
| 454 | { |
| 455 | use std::os::unix::fs::FileTypeExt; |
| 456 | if kind.is_socket() { |
| 457 | let name = path.file_name().and_then(|n| n.to_str()).unwrap_or(""); |
| 458 | if ["docker.sock", "containerd.sock", "podman.sock", "docker.proxy.sock", "backend.sock", "filesystem.sock"].contains(&name) { |
| 459 | found.push(path.display().to_string()); |
| 460 | } |
| 461 | continue; |
| 462 | } |
| 463 | } |
| 464 | if kind.is_dir() && depth > 0 { |
| 465 | find_sockets(&path, depth - 1, found); |
| 466 | } |
| 467 | } |
| 468 | } |
| 469 | |
| 470 | fn on_wsl() -> bool { |
| 471 | std::env::var_os("WSL_DISTRO_NAME").is_some() || Path::new("/proc/sys/fs/binfmt_misc/WSLInterop").exists() || Path::new("/run/WSL").exists() |
| 472 | } |
| 473 | |
| 474 | /// Asks the supervisor to stop (`drain`: once the runner's job is done), |
| 475 | /// and waits up to `wait` seconds for it, or forever. |
| 476 | fn stop(mode: &str, wait: Option<u64>) -> Result<()> { |
| 477 | if !supervisor_alive() { |
| 478 | return Ok(()); |
| 479 | } |
| 480 | std::fs::write(STOP_FILE, mode)?; |
| 481 | let until = wait.map(|w| std::time::Instant::now() + std::time::Duration::from_secs(w)); |
| 482 | while supervisor_alive() { |
| 483 | if until.is_some_and(|u| std::time::Instant::now() > u) { |
| 484 | bail!("the sandbox is still stopping"); |
| 485 | } |
| 486 | std::thread::sleep(std::time::Duration::from_millis(500)); |
| 487 | } |
| 488 | Ok(()) |
| 489 | } |
| 490 | |
| 491 | #[cfg(not(target_os = "linux"))] |
| 492 | fn run(_seed: Option<String>, _state_disk: Option<String>, _console: Option<String>) -> Result<()> { |
| 493 | bail!("the sandbox's supervisor runs inside its Linux VM") |
| 494 | } |
| 495 | |
| 496 | #[cfg(target_os = "linux")] |
| 497 | fn run(seed: Option<String>, state_disk: Option<String>, console: Option<String>) -> Result<()> { |
| 498 | supervisor::run(seed, state_disk, console) |
| 499 | } |
| 500 | |
| 501 | #[cfg(target_os = "linux")] |
| 502 | mod supervisor { |
| 503 | use std::path::Path; |
| 504 | use std::process::{Child, Command, Stdio}; |
| 505 | use std::sync::atomic::Ordering; |
| 506 | use std::time::{Duration, Instant}; |
| 507 | |
| 508 | use anyhow::{Context, Result, bail}; |
| 509 | |
| 510 | use super::*; |
| 511 | use crate::selfhosted::{STOP, g1t_now}; |
| 512 | |
| 513 | extern "C" fn on_term(_: libc::c_int) { |
| 514 | STOP.store(true, Ordering::SeqCst); |
| 515 | } |
| 516 | |
| 517 | fn now() -> String { |
| 518 | g1t_now(std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).map(|d| d.as_millis() as u64).unwrap_or(0)) |
| 519 | } |
| 520 | |
| 521 | /// A clean environment: WSL's session points `XDG_RUNTIME_DIR` and |
| 522 | /// the display variables into /mnt/wslg, which is gone. |
| 523 | fn clean(command: &mut Command) -> &mut Command { |
| 524 | command.env_clear().env("PATH", "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin").env("HOME", "/root") |
| 525 | } |
| 526 | |
| 527 | fn docker() -> Command { |
| 528 | let mut command = Command::new("docker"); |
| 529 | clean(&mut command).env("DOCKER_HOST", format!("unix://{DOCKER_SOCKET}")); |
| 530 | command |
| 531 | } |
| 532 | |
| 533 | fn ok(command: &mut Command) -> bool { |
| 534 | command.stdout(Stdio::null()).stderr(Stdio::null()).status().is_ok_and(|s| s.success()) |
| 535 | } |
| 536 | |
| 537 | struct Say { |
| 538 | console: Option<String>, |
| 539 | version: Option<String>, |
| 540 | } |
| 541 | |
| 542 | impl Say { |
| 543 | fn status(&self, state: &str, runner: Option<RunnerStatus>, error: Option<String>) { |
| 544 | let status = GuestStatus { state: state.into(), runner, error, image_version: self.version.clone(), updated_at: now() }; |
| 545 | let Ok(bytes) = serde_json::to_vec(&status) else { return }; |
| 546 | let _ = write_private(STATUS, &bytes); |
| 547 | if let Some(console) = &self.console { |
| 548 | let _ = std::fs::OpenOptions::new() |
| 549 | .append(true) |
| 550 | .open(console) |
| 551 | .and_then(|mut c| std::io::Write::write_all(&mut c, format!("{STATUS_PREFIX}{}\n", String::from_utf8_lossy(&bytes)).as_bytes())); |
| 552 | } |
| 553 | } |
| 554 | } |
| 555 | |
| 556 | fn daemon(program: &str, args: &[&str], log: &str) -> Result<Child> { |
| 557 | let out = std::fs::File::create(format!("{RUN_DIR}/{log}"))?; |
| 558 | let mut command = Command::new(program); |
| 559 | clean(&mut command) |
| 560 | .args(args) |
| 561 | .stdout(out.try_clone()?) |
| 562 | .stderr(out) |
| 563 | .stdin(Stdio::null()) |
| 564 | .spawn() |
| 565 | .with_context(|| format!("could not start {program}")) |
| 566 | } |
| 567 | |
| 568 | fn wait_for(what: &str, limit: Duration, mut ready: impl FnMut() -> bool) -> Result<()> { |
| 569 | let until = Instant::now() + limit; |
| 570 | while !ready() { |
| 571 | if Instant::now() > until { |
| 572 | bail!("{what} did not start"); |
| 573 | } |
| 574 | std::thread::sleep(Duration::from_millis(250)); |
| 575 | } |
| 576 | Ok(()) |
| 577 | } |
| 578 | |
| 579 | /// WSL's ways to Windows and to other distributions, taken away. |
| 580 | fn wsl_prepare() -> Result<()> { |
| 581 | // The resolvers WSL offers (its DNS tunnel, or the gateway), in a |
| 582 | // file of the sandbox's own: WSL's is in /mnt/wsl, unmounted next. |
| 583 | let offered = ["/mnt/wsl/resolv.conf", "/etc/resolv.conf"].iter().find_map(|p| std::fs::read_to_string(p).ok()).unwrap_or_default(); |
| 584 | let route = std::fs::read_to_string("/proc/net/route").unwrap_or_default(); |
| 585 | let _ = std::fs::remove_file("/etc/resolv.conf"); |
| 586 | std::fs::write("/etc/resolv.conf", resolv_conf(&offered, gateway(&route)))?; |
| 587 | for point in WSL_CHANNELS { |
| 588 | let _ = Command::new("umount").args(["-R", "-l", point]).stdout(Stdio::null()).stderr(Stdio::null()).status(); |
| 589 | } |
| 590 | // The GPU's paravirtual device: a way to Windows' driver. |
| 591 | let _ = std::fs::remove_file("/dev/dxg"); |
| 592 | Ok(()) |
| 593 | } |
| 594 | |
| 595 | fn mount_state_disk(device: &str) -> Result<()> { |
| 596 | std::fs::create_dir_all(STATE_DIR)?; |
| 597 | let blank = !Command::new("blkid").arg(device).output().is_ok_and(|o| o.status.success() && !o.stdout.is_empty()); |
| 598 | if blank && !ok(Command::new("mkfs.ext4").args(["-q", "-L", "g1t-state", device])) { |
| 599 | bail!("could not format the state disk {device}"); |
| 600 | } |
| 601 | if !ok(Command::new("mount").args([device, STATE_DIR])) { |
| 602 | bail!("could not mount the state disk {device}"); |
| 603 | } |
| 604 | Ok(()) |
| 605 | } |
| 606 | |
| 607 | fn read_seed_device(device: &str) -> Result<Settings> { |
| 608 | // The launcher may have rewritten it since boot: read past the cache. |
| 609 | let _ = Command::new("blockdev").args(["--flushbufs", device]).status(); |
| 610 | let bytes = std::fs::read(device).with_context(|| format!("could not read the seed disk {device}"))?; |
| 611 | read_seed(&bytes) |
| 612 | } |
| 613 | |
| 614 | fn firewall() -> Result<()> { |
| 615 | let route = std::fs::read_to_string("/proc/net/route").unwrap_or_default(); |
| 616 | let resolv = std::fs::read_to_string("/etc/resolv.conf").unwrap_or_default(); |
| 617 | let rules = firewall_rules(gateway(&route), &nameservers(&resolv)); |
| 618 | let _ = Command::new("iptables").args(["-N", "DOCKER-USER"]).stderr(Stdio::null()).status(); |
| 619 | // Only these rules, however many times the sandbox has started. |
| 620 | let _ = Command::new("iptables").args(["-F", "DOCKER-USER"]).status(); |
| 621 | // Inserted last first, so they end up in order at the chain's top. |
| 622 | for (at, rule) in rules.iter().enumerate() { |
| 623 | let position = (at + 1).to_string(); |
| 624 | let mut args = vec!["-I", "DOCKER-USER", position.as_str()]; |
| 625 | args.extend(rule.iter().map(String::as_str)); |
| 626 | if !ok(Command::new("iptables").args(&args)) { |
| 627 | bail!("could not add the rule that keeps containers off the computer: iptables {}", args.join(" ")); |
| 628 | } |
| 629 | } |
| 630 | Ok(()) |
| 631 | } |
| 632 | |
| 633 | fn container_state() -> Option<(bool, i32)> { |
| 634 | let out = docker().args(["inspect", "-f", "{{.State.Running}} {{.State.ExitCode}}", RUNNER_CONTAINER]).output().ok()?; |
| 635 | if !out.status.success() { |
| 636 | return None; |
| 637 | } |
| 638 | let text = String::from_utf8_lossy(&out.stdout); |
| 639 | let mut parts = text.split_whitespace(); |
| 640 | Some((parts.next() == Some("true"), parts.next().and_then(|c| c.parse().ok()).unwrap_or(0))) |
| 641 | } |
| 642 | |
| 643 | /// The runner's last words: its last line of output. |
| 644 | fn logs_tail() -> String { |
| 645 | docker() |
| 646 | .args(["logs", "--tail", "1", RUNNER_CONTAINER]) |
| 647 | .output() |
| 648 | .map(|o| format!("{}{}", String::from_utf8_lossy(&o.stdout), String::from_utf8_lossy(&o.stderr)).trim().to_owned()) |
| 649 | .unwrap_or_default() |
| 650 | .trim_start_matches("g1t-runner: ") |
| 651 | .to_owned() |
| 652 | } |
| 653 | |
| 654 | fn start_runner(settings: &Settings) -> Result<()> { |
| 655 | if let Some((true, _)) = container_state() { |
| 656 | return Ok(()); |
| 657 | } |
| 658 | let _ = docker().args(["rm", "-f", RUNNER_CONTAINER]).stdout(Stdio::null()).stderr(Stdio::null()).status(); |
| 659 | // What an earlier runner said is not what this one says. |
| 660 | let _ = std::fs::remove_file(format!("{STATE_DIR}/status.json")); |
| 661 | let register = !registered(); |
| 662 | if register { |
| 663 | write_private(REGISTER_ENV, register_env(settings)?.as_bytes())?; |
| 664 | } |
| 665 | let out = docker().args(runner_run_args(settings, register)).output().context("could not run docker")?; |
| 666 | if !out.status.success() { |
| 667 | bail!("could not start the runner: {}", String::from_utf8_lossy(&out.stderr).trim()); |
| 668 | } |
| 669 | Ok(()) |
| 670 | } |
| 671 | |
| 672 | pub fn run(seed: Option<String>, state_disk: Option<String>, console: Option<String>) -> Result<()> { |
| 673 | unsafe { |
| 674 | libc::signal(libc::SIGTERM, on_term as *const () as libc::sighandler_t); |
| 675 | libc::signal(libc::SIGINT, on_term as *const () as libc::sighandler_t); |
| 676 | } |
| 677 | std::fs::create_dir_all(RUN_DIR)?; |
| 678 | std::fs::create_dir_all(ROOT)?; |
| 679 | std::fs::write(PID_FILE, std::process::id().to_string())?; |
| 680 | let _ = std::fs::remove_file(STOP_FILE); |
| 681 | let version = std::fs::read_to_string("/etc/g1t-sandbox-version").ok().map(|v| v.trim().to_owned()); |
| 682 | let say = Say { console, version }; |
| 683 | say.status("starting", None, None); |
| 684 | let outcome = supervise(&say, seed, state_disk); |
| 685 | if let Err(error) = &outcome { |
| 686 | say.status("error", None, Some(format!("{error:#}"))); |
| 687 | } |
| 688 | let _ = std::fs::remove_file(PID_FILE); |
| 689 | outcome |
| 690 | } |
| 691 | |
| 692 | fn supervise(say: &Say, seed: Option<String>, state_disk: Option<String>) -> Result<()> { |
| 693 | if on_wsl() { |
| 694 | wsl_prepare()?; |
| 695 | } |
| 696 | if let Some(device) = &state_disk { |
| 697 | // A VM's system disk was grown to the size asked for: its file |
| 698 | // system grows to fill it. |
| 699 | let _ = Command::new("resize2fs").arg("/dev/vda").stdout(Stdio::null()).stderr(Stdio::null()).status(); |
| 700 | mount_state_disk(device)?; |
| 701 | } |
| 702 | std::fs::create_dir_all(STATE_DIR)?; |
| 703 | if let Some(device) = &seed { |
| 704 | let settings = read_seed_device(device)?; |
| 705 | configure(&serde_json::to_vec(&settings)?)?; |
| 706 | } |
| 707 | let found = check(); |
| 708 | if !found.is_empty() { |
| 709 | bail!("this sandbox can reach the computer, so it will not run jobs:\n - {}", found.join("\n - ")); |
| 710 | } |
| 711 | let settings: Settings = read_json(SETTINGS).context("the sandbox has no settings: run `g1t-runner sandbox up` on the computer")?; |
| 712 | if settings.runner_image.is_empty() { |
| 713 | bail!("the sandbox's settings name no runner image"); |
| 714 | } |
| 715 | |
| 716 | let mut daemons: Vec<Child> = Vec::new(); |
| 717 | let _ = std::fs::remove_file("/run/sysbox/sysmgr.sock"); |
| 718 | daemons.push(daemon("sysbox-mgr", &[], "sysbox-mgr.log")?); |
| 719 | wait_for("Sysbox's manager", Duration::from_secs(30), || Path::new("/run/sysbox/sysmgr.sock").exists())?; |
| 720 | daemons.push(daemon("sysbox-fs", &[], "sysbox-fs.log")?); |
| 721 | wait_for("Sysbox's file system", Duration::from_secs(30), || Path::new("/run/sysbox/sysfs.sock").exists())?; |
| 722 | std::fs::write("/etc/docker/daemon.json", serde_json::to_vec_pretty(&daemon_json())?)?; |
| 723 | daemons.push(daemon("dockerd", &[], "dockerd.log")?); |
| 724 | let outcome = (|| -> Result<()> { |
| 725 | wait_for("The sandbox's Docker Engine", Duration::from_secs(60), || ok(docker().args(["version"])))?; |
| 726 | firewall()?; |
| 727 | if Path::new(IMAGE_TAR).exists() { |
| 728 | let loaded = docker().args(["load", "-i", IMAGE_TAR]).stdout(Stdio::null()).status(); |
| 729 | let _ = std::fs::remove_file(IMAGE_TAR); |
| 730 | if !loaded.is_ok_and(|s| s.success()) { |
| 731 | bail!("could not load the runner image the launcher gave"); |
| 732 | } |
| 733 | } |
| 734 | if !ok(docker().args(["image", "inspect", &settings.runner_image])) && !ok(docker().args(["pull", "-q", &settings.runner_image])) { |
| 735 | bail!("could not fetch the runner's image {}", settings.runner_image); |
| 736 | } |
| 737 | start_runner(&settings)?; |
| 738 | watch(say) |
| 739 | })(); |
| 740 | // Everything stops with the supervisor; the runner first, so it can |
| 741 | // say how its job ended. |
| 742 | let _ = docker().args(["stop", "-t", "60", RUNNER_CONTAINER]).stdout(Stdio::null()).stderr(Stdio::null()).status(); |
| 743 | for child in daemons.iter_mut().rev() { |
| 744 | unsafe { |
| 745 | libc::kill(child.id() as libc::pid_t, libc::SIGTERM); |
| 746 | } |
| 747 | let until = Instant::now() + Duration::from_secs(20); |
| 748 | while matches!(child.try_wait(), Ok(None)) && Instant::now() < until { |
| 749 | std::thread::sleep(Duration::from_millis(200)); |
| 750 | } |
| 751 | let _ = child.kill(); |
| 752 | let _ = child.wait(); |
| 753 | } |
| 754 | if outcome.is_ok() { |
| 755 | say.status("stopped", None, None); |
| 756 | } |
| 757 | outcome |
| 758 | } |
| 759 | |
| 760 | /// Says how the runner is until asked to stop, then stops it. |
| 761 | fn watch(say: &Say) -> Result<()> { |
| 762 | let data = Path::new(STATE_DIR); |
| 763 | let mut asked: Option<String> = None; |
| 764 | let mut tried_again = Instant::now(); |
| 765 | loop { |
| 766 | if asked.is_none() { |
| 767 | let mode = std::fs::read_to_string(STOP_FILE).ok().map(|m| m.trim().to_owned()); |
| 768 | asked = mode.or_else(|| STOP.load(Ordering::SeqCst).then(|| read_json::<Settings>(SETTINGS).and_then(|s| s.stop).unwrap_or_else(|| "now".into()))); |
| 769 | if let Some(mode) = &asked { |
| 770 | // The runner reads these in its folder (selfhosted/mod.rs). |
| 771 | std::fs::write(data.join(if mode == "drain" { "drain" } else { "stop" }), "")?; |
| 772 | } |
| 773 | } |
| 774 | if registered() && Path::new(REGISTER_ENV).exists() { |
| 775 | // Registered: the token is no longer kept. |
| 776 | let _ = std::fs::remove_file(REGISTER_ENV); |
| 777 | if let Some(mut settings) = read_json::<Settings>(SETTINGS) { |
| 778 | settings.token = None; |
| 779 | let _ = write_private(SETTINGS, &serde_json::to_vec_pretty(&settings)?); |
| 780 | } |
| 781 | } |
| 782 | let runner: Option<RunnerStatus> = read_json(&format!("{STATE_DIR}/status.json")); |
| 783 | match container_state() { |
| 784 | Some((true, _)) => { |
| 785 | let state = if asked.is_some() { "stopping" } else { "running" }; |
| 786 | say.status(state, runner, None); |
| 787 | } |
| 788 | Some((false, code)) if asked.is_some() || code == 0 => { |
| 789 | let _ = std::fs::remove_file(data.join("stop")); |
| 790 | let _ = std::fs::remove_file(data.join("drain")); |
| 791 | return Ok(()); |
| 792 | } |
| 793 | Some((false, code)) => { |
| 794 | say.status("error", runner, Some(format!("The runner stopped ({code}): {}", logs_tail()))); |
| 795 | if tried_again.elapsed() > Duration::from_secs(30) { |
| 796 | tried_again = Instant::now(); |
| 797 | let _ = docker().args(["start", RUNNER_CONTAINER]).stdout(Stdio::null()).stderr(Stdio::null()).status(); |
| 798 | } |
| 799 | } |
| 800 | None => bail!("the runner's container is gone"), |
| 801 | } |
| 802 | std::thread::sleep(Duration::from_secs(2)); |
| 803 | } |
| 804 | } |
| 805 | } |
| 806 | |
| 807 | #[cfg(test)] |
| 808 | mod tests { |
| 809 | use super::*; |
| 810 | |
| 811 | const WSL_MOUNTINFO: &str = "\ |
| 812 | 353 361 0:29 / /usr/lib/modules/6.6.87.2-microsoft-standard-WSL2 rw,nosuid,nodev,noatime - overlay none rw,lowerdir=/modules |
| 813 | 354 361 0:32 / /mnt/wsl rw,relatime shared:1 - tmpfs none rw |
| 814 | 355 354 0:61 /guest-services /mnt/wsl/docker-desktop/shared-sockets/guest-services rw,nosuid,nodev shared:18 - tmpfs none rw,mode=755 |
| 815 | 358 361 0:34 / /usr/lib/wsl/drivers ro,nosuid,nodev,noatime - 9p drivers ro,aname=drivers;fmask=222;dmask=222 |
| 816 | 361 343 8:96 / / rw,relatime - ext4 /dev/sdg rw,discard,errors=remount-ro,data=ordered |
| 817 | 362 361 0:87 / /mnt/wslg rw,relatime shared:32 - tmpfs none rw |
| 818 | 389 361 0:104 / /usr/lib/wsl/lib rw,nosuid,nodev,noatime - overlay none rw,lowerdir=/gpu_lib_packaged:/gpu_lib_inbox |
| 819 | 393 361 0:108 / /proc rw,nosuid,nodev,noexec,noatime shared:46 - proc proc rw |
| 820 | 411 361 0:87 /.X11-unix /tmp/.X11-unix ro,relatime shared:32 - tmpfs none rw |
| 821 | "; |
| 822 | |
| 823 | const CLEAN: &str = "\ |
| 824 | 361 343 8:96 / / rw,relatime - ext4 /dev/sdg rw,discard,errors=remount-ro,data=ordered |
| 825 | 393 361 0:108 / /proc rw,nosuid,nodev,noexec,noatime shared:46 - proc proc rw |
| 826 | 395 361 0:110 / /run rw,nosuid,nodev shared:48 - tmpfs none rw,mode=755 |
| 827 | "; |
| 828 | |
| 829 | #[test] |
| 830 | fn what_wsl_mounts_is_found_and_a_cleaned_distribution_is_clean() { |
| 831 | let found = host_mounts(WSL_MOUNTINFO); |
| 832 | for point in ["/mnt/wsl ", "/mnt/wsl/docker-desktop", "/usr/lib/wsl/drivers", "/mnt/wslg", "/usr/lib/wsl/lib", "/tmp/.X11-unix"] { |
| 833 | assert!(found.iter().any(|f| f.starts_with(point)), "{point} in {found:?}"); |
| 834 | } |
| 835 | assert!(!found.iter().any(|f| f.starts_with("/usr/lib/modules"))); |
| 836 | assert!(host_mounts(CLEAN).is_empty()); |
| 837 | let drive = "500 361 0:90 / /mnt/c rw,noatime - 9p C:\\ rw,aname=drvfs;path=C:\\"; |
| 838 | assert_eq!(host_mounts(drive), vec!["/mnt/c (9p)".to_owned()]); |
| 839 | assert_eq!(host_mounts("501 361 0:91 / /work rw - virtiofs share rw").len(), 1); |
| 840 | } |
| 841 | |
| 842 | #[test] |
| 843 | fn the_sandbox_refuses_what_reaches_the_computer() { |
| 844 | assert!(problems(CLEAN, &[], false, Some(WSL_CONF)).is_empty()); |
| 845 | assert!(problems(CLEAN, &[], false, None).is_empty()); |
| 846 | let all = problems(WSL_MOUNTINFO, &["/mnt/wsl/docker-desktop/shared-sockets/host-services/docker.proxy.sock".into()], true, Some("[automount]\nenabled=true\n")); |
| 847 | assert!(all.iter().any(|p| p.contains("docker.proxy.sock"))); |
| 848 | assert!(all.iter().any(|p| p.contains("interop is on"))); |
| 849 | assert!(all.iter().any(|p| p.contains("/etc/wsl.conf"))); |
| 850 | } |
| 851 | |
| 852 | #[test] |
| 853 | fn the_image_turns_off_every_way_to_windows() { |
| 854 | let conf = WSL_CONF.replace(' ', ""); |
| 855 | for setting in ["[automount]\nenabled=false", "mountFsTab=false", "[interop]\nenabled=false", "appendWindowsPath=false", "default=root"] { |
| 856 | assert!(conf.contains(setting), "{setting}"); |
| 857 | } |
| 858 | } |
| 859 | |
| 860 | #[test] |
| 861 | fn the_gateway_and_resolvers_read() { |
| 862 | let route = "Iface\tDestination\tGateway \tFlags\tRefCnt\tUse\tMetric\tMask\t\tMTU\tWindow\tIRTT\n\ |
| 863 | eth0\t00000000\t01E01EAC\t0003\t0\t0\t0\t00000000\t0\t0\t0\n\ |
| 864 | eth0\t00E01EAC\t00000000\t0001\t0\t0\t0\t00F0FFFF\t0\t0\t0\n"; |
| 865 | assert_eq!(gateway(route), Some(Ipv4Addr::new(172, 30, 224, 1))); |
| 866 | assert_eq!(gateway("Iface\tDestination\n"), None); |
| 867 | assert_eq!(nameservers("# x\nnameserver 10.255.255.254\nsearch lan\nnameserver ::1\n"), vec![Ipv4Addr::new(10, 255, 255, 254)]); |
| 868 | assert_eq!(resolv_conf("nameserver 10.255.255.254\n", None), "nameserver 10.255.255.254\n"); |
| 869 | assert_eq!(resolv_conf("", Some(Ipv4Addr::new(172, 30, 224, 1))), "nameserver 172.30.224.1\n"); |
| 870 | } |
| 871 | |
| 872 | #[test] |
| 873 | fn containers_get_dns_and_nothing_else_of_the_computer() { |
| 874 | let gw = Ipv4Addr::new(172, 30, 224, 1); |
| 875 | let rules = firewall_rules(Some(gw), &[Ipv4Addr::new(10, 255, 255, 254), Ipv4Addr::new(1, 1, 1, 1), Ipv4Addr::LOCALHOST]); |
| 876 | let text: Vec<String> = rules.iter().map(|r| r.join(" ")).collect(); |
| 877 | assert_eq!(text[0], "-d 10.255.255.254 -p udp --dport 53 -j ACCEPT"); |
| 878 | assert!(text.contains(&"-d 1.1.1.1 -p tcp --dport 53 -j ACCEPT".to_owned())); |
| 879 | let first_drop = text.iter().position(|r| r.ends_with("DROP")).unwrap(); |
| 880 | assert!(text[..first_drop].iter().all(|r| r.ends_with("ACCEPT")), "accepts come first: {text:?}"); |
| 881 | assert!(text.contains(&"-d 172.30.224.1 -j DROP".to_owned())); |
| 882 | assert!(text.contains(&"-d 10.255.255.254 -j DROP".to_owned())); |
| 883 | assert!(!text.contains(&"-d 1.1.1.1 -j DROP".to_owned()), "a public resolver is the internet"); |
| 884 | assert!(!text.iter().any(|r| r.contains("127.0.0.1"))); |
| 885 | assert_eq!(text.last().unwrap(), "-d 169.254.0.0/16 -j DROP"); |
| 886 | } |
| 887 | |
| 888 | #[test] |
| 889 | fn the_runner_runs_under_sysbox_with_no_socket_and_no_self_update() { |
| 890 | let settings = Settings { cpus: 4, memory_mib: 8192, runner_image: "g1t.sh/flagon-io/g1t-runner@sha256:ab".into(), ..Settings::default() }; |
| 891 | let args = runner_run_args(&settings, false).join(" "); |
| 892 | assert!(args.starts_with("run -d --name g1t-runner --runtime=sysbox-runc")); |
| 893 | assert!(args.contains("--cpus 4 --memory 8192m --memory-swap 8192m")); |
| 894 | assert!(args.contains("-v /var/lib/g1t-sandbox/state:/data")); |
| 895 | assert!(args.contains("G1T_RUNNER_NO_AUTO_UPDATE=1")); |
| 896 | assert!(!args.contains("docker.sock") && !args.contains("--privileged") && !args.contains("--network host")); |
| 897 | assert!(!args.contains("--env-file")); |
| 898 | assert!(args.ends_with("g1t.sh/flagon-io/g1t-runner@sha256:ab")); |
| 899 | assert!(runner_run_args(&settings, true).join(" ").contains("--env-file /var/lib/g1t-sandbox/register.env")); |
| 900 | } |
| 901 | |
| 902 | #[test] |
| 903 | fn registration_goes_by_file_and_refuses_line_breaks() { |
| 904 | let mut settings = Settings { |
| 905 | url: Some("https://g1t.sh".into()), |
| 906 | token: Some("g1trt_x".into()), |
| 907 | name: Some("box-linux".into()), |
| 908 | labels: vec!["gpu".into()], |
| 909 | ..Settings::default() |
| 910 | }; |
| 911 | let env = register_env(&settings).unwrap(); |
| 912 | assert_eq!(env, "G1T_RUNNER_URL=https://g1t.sh\nG1T_RUNNER_TOKEN=g1trt_x\nG1T_RUNNER_NAME=box-linux\nG1T_RUNNER_LABELS=gpu\n"); |
| 913 | settings.name = Some("a\nG1T_RUNNER_URL=https://evil".into()); |
| 914 | assert!(register_env(&settings).is_err()); |
| 915 | assert!(register_env(&Settings::default()).is_err()); |
| 916 | } |
| 917 | |
| 918 | #[test] |
| 919 | fn a_seed_disk_and_a_console_line_round_trip() { |
| 920 | let settings = Settings { runner_image: "img".into(), cpus: 2, stop: Some("drain".into()), ..Settings::default() }; |
| 921 | let bytes = write_seed(&settings, 4096).unwrap(); |
| 922 | assert_eq!(bytes.len(), 4096); |
| 923 | assert_eq!(read_seed(&bytes).unwrap(), settings); |
| 924 | assert!(write_seed(&settings, 8).is_err()); |
| 925 | let log = format!("[ 1.0] booting\n{STATUS_PREFIX}{{\"state\":\"starting\"}}\nnoise\n{STATUS_PREFIX}{{\"state\":\"running\",\"updated_at\":\"t\"}}\n"); |
| 926 | assert_eq!(status_from_console(&log).unwrap().state, "running"); |
| 927 | assert!(status_from_console("nothing").is_none()); |
| 928 | } |
| 929 | |
| 930 | #[test] |
| 931 | fn the_vm_engine_has_sysbox_and_its_own_socket() { |
| 932 | let config = daemon_json(); |
| 933 | assert_eq!(config["hosts"][0], "unix:///run/g1t-sandbox/docker.sock"); |
| 934 | assert_eq!(config["runtimes"]["sysbox-runc"]["path"], "/usr/local/bin/sysbox-runc"); |
| 935 | } |
| 936 | } |