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}