sh4d0wup/
check.rs

1use crate::args;
2use crate::errors::*;
3use crate::httpd;
4use crate::keygen::EmbeddedKey;
5use crate::plot;
6use crate::plot::Cmd;
7use crate::utils;
8use nix::sched::CloneFlags;
9use nix::sys::wait::{WaitPidFlag, WaitStatus};
10use std::ffi::OsStr;
11use std::fmt;
12use std::net::SocketAddr;
13use std::process::Stdio;
14use std::sync::Arc;
15use tokio::fs;
16use tokio::io::AsyncWriteExt;
17use tokio::net::TcpListener;
18use tokio::net::TcpStream;
19use tokio::process::Command;
20use tokio::signal;
21use tokio::time::{Duration, sleep};
22
23const PODMAN_BINARY: &str = utils::compile_env!("SH4D0WUP_PODMAN_BINARY", "podman");
24
25pub async fn wait_for_server(addr: &SocketAddr) -> Result<()> {
26    debug!("Waiting for server to start up...");
27    for _ in 0..5 {
28        sleep(Duration::from_millis(100)).await;
29        if TcpStream::connect(addr).await.is_ok() {
30            debug!("Successfully connected to tcp port");
31            return Ok(());
32        }
33    }
34    bail!("Failed to connect to server");
35}
36
37pub fn test_userns_clone() -> Result<()> {
38    let cb = Box::new(|| 0);
39    let stack = &mut [0; 1024];
40    let flags = CloneFlags::CLONE_NEWNS | CloneFlags::CLONE_NEWUSER;
41
42    let pid = unsafe { nix::sched::clone(cb, stack, flags, None) }
43        .context("Failed to create user namespace")?;
44    let status = nix::sys::wait::waitpid(pid, Some(WaitPidFlag::__WCLONE))
45        .context("Failed to reap child")?;
46
47    if status != WaitStatus::Exited(pid, 0) {
48        bail!("Unexpected wait result: {:?}", status);
49    }
50
51    Ok(())
52}
53
54pub async fn test_for_unprivileged_userns_clone() -> Result<()> {
55    debug!("Testing if user namespaces can be created");
56    if let Err(err) = test_userns_clone() {
57        match fs::read("/proc/sys/kernel/unprivileged_userns_clone").await {
58            Ok(buf) => {
59                if buf == b"0\n" {
60                    warn!(
61                        "User namespaces are not enabled in /proc/sys/kernel/unprivileged_userns_clone"
62                    )
63                }
64            }
65            Err(err) => warn!(
66                "Failed to check if unprivileged_userns_clone are allowed: {:#}",
67                err
68            ),
69        }
70
71        Err(err)
72    } else {
73        debug!("Successfully tested for user namespaces");
74        Ok(())
75    }
76}
77
78pub async fn podman<I, S>(args: I, capture_stdout: bool, stdin: Option<&[u8]>) -> Result<Vec<u8>>
79where
80    I: IntoIterator<Item = S>,
81    S: AsRef<OsStr> + fmt::Debug,
82{
83    let mut cmd = Command::new(PODMAN_BINARY);
84    let args = args.into_iter().collect::<Vec<_>>();
85    cmd.args(&args);
86    if stdin.is_some() {
87        cmd.stdin(Stdio::piped());
88    }
89    if capture_stdout {
90        cmd.stdout(Stdio::piped());
91    }
92    debug!("Spawning child process: podman {:?}", args);
93    let mut child = cmd
94        .spawn()
95        .with_context(|| anyhow!("Failed to execute podman binary: {PODMAN_BINARY:?}"))?;
96
97    if let Some(data) = stdin {
98        debug!("Sending {} bytes to child process...", data.len());
99        let mut stdin = child.stdin.take().unwrap();
100        stdin.write_all(data).await?;
101        stdin.flush().await?;
102    }
103
104    let out = child.wait_with_output().await?;
105    debug!("Podman command exited: {:?}", out.status);
106    if !out.status.success() {
107        bail!(
108            "Podman command ({:?}) failed to execute: {:?}",
109            args,
110            out.status
111        );
112    }
113    Ok(out.stdout)
114}
115
116#[derive(Debug)]
117pub struct Container {
118    id: String,
119    addr: SocketAddr,
120}
121
122impl Container {
123    pub async fn create(
124        image: &str,
125        init: &[String],
126        addr: SocketAddr,
127        expose_fuse: bool,
128    ) -> Result<Container> {
129        let bin = init
130            .first()
131            .context("Command for container can't be empty")?;
132        let cmd_args = &init[1..];
133        let entrypoint = format!("--entrypoint={bin}");
134        let mut podman_args = vec![
135            "container",
136            "run",
137            "--detach",
138            "--rm",
139            "--network=host",
140            "-v=/usr/bin/catatonit:/__:ro",
141        ];
142        if expose_fuse {
143            debug!("Mapping /dev/fuse into the container");
144            podman_args.push("--device=/dev/fuse");
145        }
146
147        podman_args.extend([&entrypoint, "--", image]);
148        podman_args.extend(cmd_args.iter().map(|s| s.as_str()));
149
150        let mut out = podman(&podman_args, true, None).await?;
151        if let Some(idx) = memchr::memchr(b'\n', &out) {
152            out.truncate(idx);
153        }
154        let id = String::from_utf8(out)?;
155        Ok(Container { id, addr })
156    }
157
158    pub async fn exec<I, S>(
159        &self,
160        args: I,
161        stdin: Option<&[u8]>,
162        env: &[String],
163        user: Option<String>,
164    ) -> Result<()>
165    where
166        I: IntoIterator<Item = S>,
167        S: AsRef<str> + fmt::Debug + Clone,
168    {
169        let args = args.into_iter().collect::<Vec<_>>();
170        let mut a = vec!["container".to_string(), "exec".to_string()];
171        if let Some(user) = user {
172            a.extend(["-u".to_string(), user]);
173        }
174        if stdin.is_some() {
175            a.push("-i".to_string());
176        }
177        for env in env {
178            a.push(format!("-e={env}"));
179        }
180        a.extend(["--".to_string(), self.id.to_string()]);
181        a.extend(args.iter().map(|x| x.as_ref().to_string()));
182        podman(&a, false, stdin)
183            .await
184            .with_context(|| anyhow!("Failed to execute in container: {:?}", args))?;
185        Ok(())
186    }
187
188    pub async fn kill(self) -> Result<()> {
189        podman(&["container", "kill", &self.id], true, None)
190            .await
191            .context("Failed to remove container")?;
192        Ok(())
193    }
194
195    pub async fn exec_cmd_stdin(
196        &self,
197        cmd: &Cmd,
198        stdin: Option<&[u8]>,
199        user: Option<String>,
200    ) -> Result<()> {
201        let args = match cmd {
202            Cmd::Shell(cmd) => vec!["sh", "-c", cmd],
203            Cmd::Exec(cmd) => cmd.iter().map(|s| s.as_str()).collect::<Vec<_>>(),
204        };
205        info!("Executing process in container: {:?}", args);
206        self.exec(
207            &args,
208            stdin,
209            &[
210                format!("SH4D0WUP_BOUND_ADDR={}", self.addr),
211                format!("SH4D0WUP_BOUND_IP={}", self.addr.ip()),
212                format!("SH4D0WUP_BOUND_PORT={}", self.addr.port()),
213            ],
214            user,
215        )
216        .await
217        .map_err(|err| {
218            error!("Command failed: {:#}", err);
219            err
220        })
221        .context("Command failed")?;
222        Ok(())
223    }
224
225    pub async fn exec_cmd(&self, cmd: &Cmd, user: Option<String>) -> Result<()> {
226        self.exec_cmd_stdin(cmd, None, user).await
227    }
228
229    pub async fn run_check(
230        &self,
231        config: &plot::Check,
232        plot_extras: &plot::PlotExtras,
233        tls: Option<&httpd::Tls>,
234        keep: bool,
235    ) -> Result<()> {
236        info!("Finishing setup in container...");
237        if let (Some(tls), Some(cmd)) = (tls, &config.install_certs) {
238            info!("Installing certificates...");
239            self.exec_cmd_stdin(cmd, Some(&tls.cert), Some("0".to_string()))
240                .await
241                .context("Failed to install certificates")?;
242        }
243        for install in &config.install_keys {
244            info!("Installing key {:?} with {:?}...", install.key, install.cmd);
245            let key = plot_extras
246                .signing_keys
247                .get(&install.key)
248                .context("Invalid reference to signing key")?;
249
250            let cert = match key {
251                EmbeddedKey::Pgp(pgp) => pgp.to_cert(install.binary)?,
252                EmbeddedKey::Ssh(ssh) => ssh.to_cert()?,
253                EmbeddedKey::Openssl(openssl) => openssl.to_cert(install.binary)?,
254                EmbeddedKey::InToto(_in_toto) => {
255                    bail!("Installing in-toto keys into the container isn't supported yet")
256                }
257            };
258
259            self.exec_cmd_stdin(&install.cmd, Some(&cert), None)
260                .await
261                .context("Failed to install certificates")?;
262        }
263        for host in &config.register_hosts {
264            info!(
265                "Installing /etc/hosts entry, {:?} => {}",
266                host,
267                self.addr.ip()
268            );
269            let cmd = format!("echo \"{} {}\" >> /etc/hosts", self.addr.ip(), host);
270            self.exec_cmd(&Cmd::Shell(cmd), Some("0".to_string()))
271                .await
272                .context("Failed to register /etc/hosts entry")?;
273        }
274
275        info!("Starting test...");
276        for cmd in &config.cmds {
277            self.exec_cmd(cmd, None)
278                .await
279                .context("Attack failed to execute on test environment")?;
280        }
281        info!("Test completed successfully");
282
283        if keep {
284            info!("Keeping container around until ^C...");
285            futures::future::pending().await
286        } else {
287            Ok(())
288        }
289    }
290}
291
292pub async fn run(
293    addr: SocketAddr,
294    check: args::Check,
295    tls: Option<&httpd::Tls>,
296    ctx: Arc<plot::Ctx>,
297) -> Result<()> {
298    let check_config = ctx
299        .plot
300        .check
301        .as_ref()
302        .context("No test configured in this plot")?;
303    wait_for_server(&addr).await?;
304
305    let image = &check_config.image;
306    let init = check_config
307        .init
308        .clone()
309        .unwrap_or_else(|| vec!["/__".to_string(), "-P".to_string()]);
310
311    if check.pull
312        || podman(&["image", "exists", "--", image], false, None)
313            .await
314            .is_err()
315    {
316        info!("Pulling container image...");
317        podman(&["image", "pull", "--", image], false, None).await?;
318    }
319
320    info!("Creating container...");
321    let container = Container::create(image, &init, addr, check_config.expose_fuse).await?;
322    let container_id = container.id.clone();
323    let result = tokio::select! {
324        result = container.run_check(check_config, &ctx.extras, tls, check.keep) => result,
325        _ = signal::ctrl_c() => Err(anyhow!("Ctrl-c received")),
326    };
327    info!("Removing container...");
328    if let Err(err) = container.kill().await {
329        warn!("Failed to kill container {:?}: {:#}", container_id, err);
330    }
331    info!("Cleanup complete");
332
333    result
334}
335
336pub async fn spawn(check: args::Check, ctx: plot::Ctx) -> Result<()> {
337    test_for_unprivileged_userns_clone().await?;
338
339    let addr = if let Some(addr) = check.bind {
340        addr
341    } else {
342        let sock = TcpListener::bind("127.0.0.1:0").await?;
343        sock.local_addr()?
344    };
345
346    let tls = if let Some(tls) = ctx.plot.tls.clone() {
347        Some(httpd::Tls::try_from(tls)?)
348    } else {
349        None
350    };
351
352    let ctx = Arc::new(ctx);
353    let httpd = httpd::run(addr, tls.clone(), ctx.clone());
354    let check = run(addr, check, tls.as_ref(), ctx);
355
356    tokio::select! {
357        httpd = httpd => httpd.context("httpd thread terminated")?,
358        check = check => check?,
359    };
360
361    Ok(())
362}