Prepare, configure, and manage Firecracker microVMs in seconds!
virtualization
linux
microvm
firecracker
1use std::{env, fs};
2
3use anyhow::Result;
4use owo_colors::OwoColorize;
5use serde::{Deserialize, Serialize};
6use sha2::{Digest, Sha256};
7
8use crate::{
9 command::{run_command, run_command_with_stdout_inherit},
10 vmlinuz::extract_vmlinuz,
11};
12
13pub mod command;
14pub mod config;
15pub mod downloader;
16pub mod rootfs;
17pub mod ssh;
18pub mod vmlinuz;
19
20const BRIDGE_IP: &str = "172.16.0.1";
21
22fn get_kernel_version() -> String {
23 env::var("KERNEL_VERSION").unwrap_or_else(|_| "6.16.7".to_string())
24}
25
26#[derive(Clone, Copy, PartialEq, Serialize, Deserialize, Debug)]
27pub enum Distro {
28 Debian,
29 Alpine,
30 Ubuntu,
31 NixOS,
32 Fedora,
33 Gentoo,
34 Slackware,
35 Opensuse,
36 OpensuseTumbleweed,
37 Almalinux,
38 RockyLinux,
39 Archlinux,
40}
41
42impl ToString for Distro {
43 fn to_string(&self) -> String {
44 match self {
45 Distro::Debian => "debian".to_string(),
46 Distro::Alpine => "alpine".to_string(),
47 Distro::Ubuntu => "ubuntu".to_string(),
48 Distro::NixOS => "nixos".to_string(),
49 Distro::Fedora => "fedora".to_string(),
50 Distro::Gentoo => "gentoo".to_string(),
51 Distro::Slackware => "slackware".to_string(),
52 Distro::Opensuse => "opensuse".to_string(),
53 Distro::OpensuseTumbleweed => "opensuse-tumbleweed".to_string(),
54 Distro::Almalinux => "almalinux".to_string(),
55 Distro::RockyLinux => "rockylinux".to_string(),
56 Distro::Archlinux => "archlinux".to_string(),
57 }
58 }
59}
60
61pub fn prepare(
62 distro: Distro,
63 kernel_file: Option<String>,
64 ssh_keys: Option<Vec<String>>,
65) -> Result<String> {
66 let arch = run_command("uname", &["-m"], false)?.stdout;
67 let arch = String::from_utf8_lossy(&arch).trim().to_string();
68 println!("[+] Detected architecture: {}", arch.bright_green());
69
70 if let Some(ref vmlinuz_file) = kernel_file {
71 if !std::path::Path::new(vmlinuz_file).exists() {
72 println!(
73 "{} {}",
74 "[!]".red(),
75 format!("vmlinuz file {} does not exist", vmlinuz_file).red()
76 );
77 std::process::exit(1);
78 }
79 }
80
81 let app_dir = config::get_config_dir()?;
82 let preparer: Box<dyn RootfsPreparer> = match distro {
83 Distro::Debian => Box::new(DebianPreparer),
84 Distro::Alpine => Box::new(AlpinePreparer),
85 Distro::Ubuntu => Box::new(UbuntuPreparer),
86 Distro::NixOS => Box::new(NixOSPreparer),
87 Distro::Fedora => Box::new(FedoraPreparer),
88 Distro::Gentoo => Box::new(GentooPreparer),
89 Distro::Slackware => Box::new(SlackwarePreparer),
90 Distro::Opensuse => Box::new(OpensusePreparer),
91 Distro::OpensuseTumbleweed => Box::new(OpensuseTumbleweedPreparer),
92 Distro::Almalinux => Box::new(AlmalinuxPreparer),
93 Distro::RockyLinux => Box::new(RockyLinuxPreparer),
94 Distro::Archlinux => Box::new(ArchlinuxPreparer),
95 };
96
97 let (kernel_file, img_file, ssh_key_file) =
98 preparer.prepare(&arch, &app_dir, kernel_file, ssh_keys)?;
99
100 extract_vmlinuz(&kernel_file)?;
101
102 println!("[✓] Kernel: {}", kernel_file.bright_green());
103 println!("[✓] Rootfs: {}", img_file.bright_green());
104 match ssh_key_file {
105 None => println!("[✓] SSH Keys: User provided"),
106 Some(ssh_key_file) => println!("[✓] SSH Key: {}", ssh_key_file.bright_green()),
107 }
108
109 Ok(kernel_file)
110}
111
112pub trait RootfsPreparer {
113 fn prepare(
114 &self,
115 arch: &str,
116 app_dir: &str,
117 kernel_file: Option<String>,
118 ssh_keys: Option<Vec<String>>,
119 ) -> Result<(String, String, Option<String>)>;
120 fn name(&self) -> &'static str;
121}
122
123pub struct DebianPreparer;
124pub struct AlpinePreparer;
125pub struct UbuntuPreparer;
126pub struct NixOSPreparer;
127pub struct FedoraPreparer;
128pub struct GentooPreparer;
129pub struct SlackwarePreparer;
130pub struct OpensusePreparer;
131pub struct OpensuseTumbleweedPreparer;
132pub struct AlmalinuxPreparer;
133pub struct RockyLinuxPreparer;
134pub struct ArchlinuxPreparer;
135
136impl RootfsPreparer for DebianPreparer {
137 fn name(&self) -> &'static str {
138 "Debian"
139 }
140
141 fn prepare(
142 &self,
143 arch: &str,
144 app_dir: &str,
145 kernel_file: Option<String>,
146 ssh_keys: Option<Vec<String>>,
147 ) -> Result<(String, String, Option<String>)> {
148 println!(
149 "[+] Preparing {} rootfs for {}...",
150 self.name(),
151 arch.bright_green()
152 );
153
154 let kernel_file = match kernel_file {
155 Some(k) => fs::canonicalize(k)?.to_str().unwrap().to_string(),
156 None => downloader::download_kernel(&get_kernel_version(), arch)?,
157 };
158
159 let debootstrap_dir = format!("{}/debian-rootfs", app_dir);
160
161 let arch = match arch {
162 "x86_64" => "amd64",
163 "aarch64" => "arm64",
164 _ => arch,
165 };
166
167 if !std::path::Path::new(&debootstrap_dir).exists() {
168 fs::create_dir_all(&debootstrap_dir)?;
169 run_command_with_stdout_inherit(
170 "debootstrap",
171 &[
172 &format!("--arch={}", arch),
173 "stable",
174 &debootstrap_dir,
175 "http://deb.debian.org/debian/",
176 ],
177 true,
178 )?;
179 }
180
181 run_command(
182 "chroot",
183 &[
184 &debootstrap_dir,
185 "sh",
186 "-c",
187 "apt-get install -y systemd-resolved ca-certificates curl",
188 ],
189 true,
190 )?;
191 run_command(
192 "chroot",
193 &[
194 &debootstrap_dir,
195 "systemctl",
196 "enable",
197 "systemd-networkd",
198 "systemd-resolved",
199 ],
200 true,
201 )?;
202
203 const RESOLVED_CONF: &str = include_str!("./config/resolved.conf");
204 run_command(
205 "chroot",
206 &[
207 &debootstrap_dir,
208 "sh",
209 "-c",
210 &format!("echo '{}' > /etc/systemd/resolved.conf", RESOLVED_CONF),
211 ],
212 true,
213 )?;
214
215 let ssh_key_name = "id_rsa";
216 run_command(
217 "mkdir",
218 &["-p", &format!("{}/root/.ssh", debootstrap_dir)],
219 true,
220 )?;
221
222 let img_file = format!("{}/debian-rootfs.img", app_dir);
223 if ssh_keys_changed(
224 &ssh_keys,
225 &format!("{}/root/.ssh/authorized_keys", debootstrap_dir),
226 )? {
227 println!("[+] SSH keys have changed, removing existing image to regenerate.");
228 run_command("rm", &["-f", &img_file], true)?;
229 }
230
231 match ssh_keys {
232 Some(ref keys) => ssh::copy_ssh_keys(keys, &debootstrap_dir)?,
233 None => ssh::generate_and_copy_ssh_key(&ssh_key_name, &debootstrap_dir)?,
234 }
235
236 if !run_command("chroot", &[&debootstrap_dir, "which", "sshd"], true)
237 .map(|output| output.status.success())
238 .unwrap_or(false)
239 {
240 run_command_with_stdout_inherit(
241 "chroot",
242 &[&debootstrap_dir, "apt-get", "update"],
243 true,
244 )?;
245 run_command_with_stdout_inherit(
246 "chroot",
247 &[
248 &debootstrap_dir,
249 "apt-get",
250 "install",
251 "-y",
252 "openssh-server",
253 ],
254 true,
255 )?;
256 run_command(
257 "chroot",
258 &[&debootstrap_dir, "systemctl", "enable", "ssh"],
259 true,
260 )?;
261 }
262
263 rootfs::create_overlay_dirs(&debootstrap_dir)?;
264 rootfs::add_overlay_init(&debootstrap_dir)?;
265 rootfs::create_squashfs(&debootstrap_dir, &img_file)?;
266
267 let ssh_key_file = match ssh_keys {
268 Some(_) => None,
269 None => Some(format!("{}/{}", app_dir, ssh_key_name)),
270 };
271
272 Ok((kernel_file, img_file, ssh_key_file))
273 }
274}
275
276impl RootfsPreparer for AlpinePreparer {
277 fn name(&self) -> &'static str {
278 "Alpine"
279 }
280
281 fn prepare(
282 &self,
283 arch: &str,
284 app_dir: &str,
285 kernel_file: Option<String>,
286 ssh_keys: Option<Vec<String>>,
287 ) -> Result<(String, String, Option<String>)> {
288 println!(
289 "[+] Preparing {} rootfs for {}...",
290 self.name(),
291 arch.bright_green()
292 );
293
294 let kernel_file = match kernel_file {
295 Some(k) => fs::canonicalize(k)?.to_str().unwrap().to_string(),
296 None => downloader::download_kernel(&get_kernel_version(), arch)?,
297 };
298 let minirootfs = format!("{}/minirootfs", app_dir);
299 downloader::download_alpine_rootfs(&minirootfs, arch)?;
300
301 run_command(
302 "sh",
303 &[
304 "-c",
305 &format!(
306 "echo 'nameserver {}' >> {}/etc/resolv.conf",
307 BRIDGE_IP, minirootfs
308 ),
309 ],
310 true,
311 )?;
312
313 run_command_with_stdout_inherit(
314 "chroot",
315 &[
316 &minirootfs,
317 "sh",
318 "-c",
319 "type curl || (apk update && apk add curl)",
320 ],
321 true,
322 )?;
323
324 if !run_command("chroot", &[&minirootfs, "which", "sshd"], true)
325 .map(|output| output.status.success())
326 .unwrap_or(false)
327 {
328 run_command_with_stdout_inherit("chroot", &[&minirootfs, "apk", "update"], true)?;
329 run_command_with_stdout_inherit(
330 "chroot",
331 &[
332 &minirootfs,
333 "apk",
334 "add",
335 "alpine-base",
336 "util-linux",
337 "linux-virt",
338 "haveged",
339 "openssh",
340 ],
341 true,
342 )?;
343 }
344
345 run_command_with_stdout_inherit(
346 "chroot",
347 &[&minirootfs, "rc-update", "add", "haveged"],
348 true,
349 )?;
350 run_command(
351 "chroot",
352 &[
353 &minirootfs,
354 "sh",
355 "-c",
356 "for svc in devfs procfs sysfs; do ln -fs /etc/init.d/$svc /etc/runlevels/boot; done",
357 ],
358 true,
359 )?;
360 if !run_command(
361 "chroot",
362 &[
363 &minirootfs,
364 "ln",
365 "-s",
366 "agetty",
367 "/etc/init.d/agetty.ttyS0",
368 ],
369 true,
370 )
371 .map(|output| output.status.success())
372 .unwrap_or(false)
373 {
374 println!("[!] Failed to create symlink for agetty.ttyS0, please check manually.");
375 }
376 run_command_with_stdout_inherit(
377 "chroot",
378 &[&minirootfs, "sh", "-c", "echo ttyS0 > /etc/securetty"],
379 true,
380 )?;
381 run_command(
382 "chroot",
383 &[&minirootfs, "rc-update", "add", "agetty.ttyS0", "default"],
384 true,
385 )?;
386
387 run_command("chroot", &[&minirootfs, "rc-update", "add", "sshd"], true)?;
388 run_command(
389 "chroot",
390 &[&minirootfs, "rc-update", "add", "networking", "boot"],
391 true,
392 )?;
393 run_command(
394 "chroot",
395 &[&minirootfs, "mkdir", "-p", "/root/.ssh", "/etc/network"],
396 true,
397 )?;
398
399 run_command(
400 "chroot",
401 &[
402 &minirootfs,
403 "sh",
404 "-c",
405 "echo 'auto eth0\niface eth0 inet dhcp' > /etc/network/interfaces",
406 ],
407 true,
408 )?;
409
410 let img_file = format!("{}/alpine-rootfs.img", app_dir);
411 if ssh_keys_changed(
412 &ssh_keys,
413 &format!("{}/root/.ssh/authorized_keys", minirootfs),
414 )? {
415 println!("[+] SSH keys have changed, removing existing image to regenerate.");
416 run_command("rm", &["-f", &img_file], true)?;
417 }
418
419 let ssh_key_name = "id_rsa";
420 match ssh_keys {
421 Some(ref keys) => ssh::copy_ssh_keys(keys, &minirootfs)?,
422 None => ssh::generate_and_copy_ssh_key(&ssh_key_name, &minirootfs)?,
423 }
424
425 rootfs::create_squashfs(&minirootfs, &img_file)?;
426
427 let ssh_key_file = match ssh_keys {
428 Some(_) => None,
429 None => Some(format!("{}/{}", app_dir, ssh_key_name)),
430 };
431
432 Ok((kernel_file, img_file, ssh_key_file))
433 }
434}
435
436impl RootfsPreparer for UbuntuPreparer {
437 fn name(&self) -> &'static str {
438 "Ubuntu"
439 }
440
441 fn prepare(
442 &self,
443 arch: &str,
444 app_dir: &str,
445 kernel_file: Option<String>,
446 ssh_keys: Option<Vec<String>>,
447 ) -> Result<(String, String, Option<String>)> {
448 println!(
449 "[+] Preparing {} rootfs for {}...",
450 self.name(),
451 arch.bright_green()
452 );
453 let (vmlinuz_file, ubuntu_file, _ubuntu_version) = downloader::download_files(arch)?;
454
455 let kernel_file = match kernel_file {
456 Some(k) => fs::canonicalize(k)?.to_str().unwrap().to_string(),
457 None => vmlinuz_file,
458 };
459
460 let squashfs_root_dir = format!("{}/squashfs_root", app_dir);
461 rootfs::extract_squashfs(&ubuntu_file, &squashfs_root_dir)?;
462
463 run_command(
464 "cp",
465 &["-r", "/etc/ssl", &format!("{}/etc/", squashfs_root_dir)],
466 true,
467 )?;
468 run_command(
469 "cp",
470 &[
471 "-r",
472 "/etc/ca-certificates",
473 &format!("{}/etc/", squashfs_root_dir),
474 ],
475 true,
476 )?;
477
478 run_command(
479 "chroot",
480 &[
481 &squashfs_root_dir,
482 "systemctl",
483 "enable",
484 "systemd-networkd",
485 ],
486 true,
487 )?;
488
489 const RESOLVED_CONF: &str = include_str!("./config/resolved.conf");
490 run_command(
491 "chroot",
492 &[
493 &squashfs_root_dir,
494 "sh",
495 "-c",
496 &format!("echo '{}' > /etc/systemd/resolved.conf", RESOLVED_CONF),
497 ],
498 true,
499 )?;
500
501 let img_file = format!("{}/ubuntu-rootfs.img", app_dir);
502 if ssh_keys_changed(
503 &ssh_keys,
504 &format!("{}/root/.ssh/authorized_keys", squashfs_root_dir),
505 )? {
506 println!("[+] SSH keys have changed, removing existing image to regenerate.");
507 run_command("rm", &["-f", &img_file], true)?;
508 }
509
510 let ssh_key_name = "id_rsa";
511 match ssh_keys {
512 Some(ref keys) => ssh::copy_ssh_keys(keys, &squashfs_root_dir)?,
513 None => ssh::generate_and_copy_ssh_key(&ssh_key_name, &squashfs_root_dir)?,
514 }
515
516 rootfs::create_overlay_dirs(&squashfs_root_dir)?;
517 rootfs::add_overlay_init(&squashfs_root_dir)?;
518 rootfs::create_squashfs(&squashfs_root_dir, &img_file)?;
519
520 let ssh_key_file = match ssh_keys {
521 Some(_) => None,
522 None => Some(format!("{}/{}", app_dir, ssh_key_name)),
523 };
524
525 Ok((kernel_file, img_file, ssh_key_file))
526 }
527}
528
529impl RootfsPreparer for NixOSPreparer {
530 fn name(&self) -> &'static str {
531 "NixOS"
532 }
533
534 fn prepare(
535 &self,
536 arch: &str,
537 app_dir: &str,
538 kernel_file: Option<String>,
539 ssh_keys: Option<Vec<String>>,
540 ) -> Result<(String, String, Option<String>)> {
541 println!(
542 "[+] Preparing {} rootfs for {}...",
543 self.name(),
544 arch.bright_green()
545 );
546 let kernel_file = match kernel_file {
547 Some(k) => fs::canonicalize(k)?.to_str().unwrap().to_string(),
548 None => downloader::download_kernel(&get_kernel_version(), arch)?,
549 };
550 let nixos_rootfs = format!("{}/nixos-rootfs", app_dir);
551 let squashfs_file = format!("{}/nixos-rootfs.squashfs", app_dir);
552
553 downloader::download_nixos_rootfs(arch)?;
554 rootfs::extract_squashfs(&squashfs_file, &nixos_rootfs)?;
555
556 let img_file = format!("{}/nixos-rootfs.img", app_dir);
557 if ssh_keys_changed(
558 &ssh_keys,
559 &format!("{}/root/.ssh/authorized_keys", nixos_rootfs),
560 )? {
561 println!("[+] SSH keys have changed, removing existing image to regenerate.");
562 run_command("rm", &["-f", &img_file], true)?;
563 }
564
565 let ssh_key_name = "id_rsa";
566 match ssh_keys {
567 Some(ref keys) => ssh::copy_ssh_keys(keys, &nixos_rootfs)?,
568 None => ssh::generate_and_copy_ssh_key(&ssh_key_name, &nixos_rootfs)?,
569 }
570
571 rootfs::create_squashfs(&nixos_rootfs, &img_file)?;
572
573 let ssh_key_file = match ssh_keys {
574 Some(_) => None,
575 None => Some(format!("{}/{}", app_dir, ssh_key_name)),
576 };
577
578 println!(
579 "[+] {} rootfs prepared at: {}",
580 self.name(),
581 nixos_rootfs.bright_green()
582 );
583
584 Ok((kernel_file, img_file, ssh_key_file))
585 }
586}
587
588impl RootfsPreparer for FedoraPreparer {
589 fn name(&self) -> &'static str {
590 "Fedora"
591 }
592
593 fn prepare(
594 &self,
595 arch: &str,
596 app_dir: &str,
597 kernel_file: Option<String>,
598 ssh_keys: Option<Vec<String>>,
599 ) -> Result<(String, String, Option<String>)> {
600 println!(
601 "[+] Preparing {} rootfs for {}...",
602 self.name(),
603 arch.bright_green()
604 );
605
606 let kernel_file = match kernel_file {
607 Some(k) => fs::canonicalize(k)?.to_str().unwrap().to_string(),
608 None => downloader::download_kernel(&get_kernel_version(), arch)?,
609 };
610 let fedora_rootfs = format!("{}/fedora-rootfs", app_dir);
611 let squashfs_file = format!("{}/fedora-rootfs.squashfs", app_dir);
612
613 downloader::download_fedora_rootfs(arch)?;
614 rootfs::extract_squashfs(&squashfs_file, &fedora_rootfs)?;
615
616 let img_file = format!("{}/fedora-rootfs.img", app_dir);
617 if ssh_keys_changed(
618 &ssh_keys,
619 &format!("{}/root/.ssh/authorized_keys", fedora_rootfs),
620 )? {
621 println!("[+] SSH keys have changed, removing existing image to regenerate.");
622 run_command("rm", &["-f", &img_file], true)?;
623 }
624
625 run_command(
626 "chroot",
627 &[&fedora_rootfs, "systemctl", "enable", "sshd"],
628 true,
629 )?;
630
631 let ssh_key_name = "id_rsa";
632 match ssh_keys {
633 Some(ref keys) => ssh::copy_ssh_keys(keys, &fedora_rootfs)?,
634 None => ssh::generate_and_copy_ssh_key(&ssh_key_name, &fedora_rootfs)?,
635 }
636
637 rootfs::create_squashfs(&fedora_rootfs, &img_file)?;
638
639 let ssh_key_file = match ssh_keys {
640 Some(_) => None,
641 None => Some(format!("{}/{}", app_dir, ssh_key_name)),
642 };
643
644 println!(
645 "[+] {} rootfs prepared at: {}",
646 self.name(),
647 fedora_rootfs.bright_green()
648 );
649
650 Ok((kernel_file, img_file, ssh_key_file))
651 }
652}
653
654impl RootfsPreparer for GentooPreparer {
655 fn name(&self) -> &'static str {
656 "Gentoo"
657 }
658
659 fn prepare(
660 &self,
661 arch: &str,
662 app_dir: &str,
663 kernel_file: Option<String>,
664 ssh_keys: Option<Vec<String>>,
665 ) -> Result<(String, String, Option<String>)> {
666 println!(
667 "[+] Preparing {} rootfs for {}...",
668 self.name(),
669 arch.bright_green()
670 );
671
672 let kernel_file = match kernel_file {
673 Some(k) => fs::canonicalize(k)?.to_str().unwrap().to_string(),
674 None => downloader::download_kernel(&get_kernel_version(), arch)?,
675 };
676
677 let gentoo_rootfs = format!("{}/gentoo-rootfs", app_dir);
678 let squashfs_file = format!("{}/gentoo-rootfs.squashfs", app_dir);
679
680 downloader::download_gentoo_rootfs(arch)?;
681 rootfs::extract_squashfs(&squashfs_file, &gentoo_rootfs)?;
682
683 // Enable sshd service
684 run_command(
685 "chroot",
686 &[&gentoo_rootfs, "systemctl", "enable", "sshd"],
687 true,
688 )?;
689
690 let img_file = format!("{}/gentoo-rootfs.img", app_dir);
691 if ssh_keys_changed(
692 &ssh_keys,
693 &format!("{}/root/.ssh/authorized_keys", gentoo_rootfs),
694 )? {
695 println!("[+] SSH keys have changed, removing existing image to regenerate.");
696 run_command("rm", &["-f", &img_file], true)?;
697 }
698
699 let ssh_key_name = "id_rsa";
700 match ssh_keys {
701 Some(ref keys) => ssh::copy_ssh_keys(keys, &gentoo_rootfs)?,
702 None => ssh::generate_and_copy_ssh_key(&ssh_key_name, &gentoo_rootfs)?,
703 }
704
705 rootfs::create_squashfs(&gentoo_rootfs, &img_file)?;
706
707 let ssh_key_file = match ssh_keys {
708 Some(_) => None,
709 None => Some(format!("{}/{}", app_dir, ssh_key_name)),
710 };
711
712 Ok((kernel_file, img_file, ssh_key_file))
713 }
714}
715
716impl RootfsPreparer for SlackwarePreparer {
717 fn name(&self) -> &'static str {
718 "Slackware"
719 }
720
721 fn prepare(
722 &self,
723 arch: &str,
724 app_dir: &str,
725 kernel_file: Option<String>,
726 ssh_keys: Option<Vec<String>>,
727 ) -> Result<(String, String, Option<String>)> {
728 println!(
729 "[+] Preparing {} rootfs for {}...",
730 self.name(),
731 arch.bright_green()
732 );
733
734 let kernel_file = match kernel_file {
735 Some(k) => fs::canonicalize(k)?.to_str().unwrap().to_string(),
736 None => downloader::download_kernel(&get_kernel_version(), arch)?,
737 };
738
739 let slackware_rootfs = format!("{}/slackware-rootfs", app_dir);
740 let squashfs_file = format!("{}/slackware-rootfs.squashfs", app_dir);
741
742 downloader::download_slackware_rootfs(arch)?;
743 rootfs::extract_squashfs(&squashfs_file, &slackware_rootfs)?;
744
745 let img_file = format!("{}/slackware-rootfs.img", app_dir);
746 if ssh_keys_changed(
747 &ssh_keys,
748 &format!("{}/root/.ssh/authorized_keys", slackware_rootfs),
749 )? {
750 println!("[+] SSH keys have changed, removing existing image to regenerate.");
751 run_command("rm", &["-f", &img_file], true)?;
752 }
753
754 run_command(
755 "chroot",
756 &[
757 &slackware_rootfs,
758 "ln",
759 "-sf",
760 "/etc/rc.d/rc.sshd",
761 "/etc/rc.d/rc3.d/S50sshd",
762 ],
763 true,
764 )?;
765
766 let ssh_key_name = "id_rsa";
767 match ssh_keys {
768 Some(ref keys) => ssh::copy_ssh_keys(keys, &slackware_rootfs)?,
769 None => ssh::generate_and_copy_ssh_key(&ssh_key_name, &slackware_rootfs)?,
770 }
771
772 rootfs::create_squashfs(&slackware_rootfs, &img_file)?;
773
774 let ssh_key_file = match ssh_keys {
775 Some(_) => None,
776 None => Some(format!("{}/{}", app_dir, ssh_key_name)),
777 };
778
779 Ok((kernel_file, img_file, ssh_key_file))
780 }
781}
782
783impl RootfsPreparer for OpensusePreparer {
784 fn name(&self) -> &'static str {
785 "OpenSUSE (Leap)"
786 }
787
788 fn prepare(
789 &self,
790 arch: &str,
791 app_dir: &str,
792 kernel_file: Option<String>,
793 ssh_keys: Option<Vec<String>>,
794 ) -> Result<(String, String, Option<String>)> {
795 println!(
796 "[+] Preparing {} rootfs for {}...",
797 self.name(),
798 arch.bright_green()
799 );
800
801 let kernel_file = match kernel_file {
802 Some(k) => fs::canonicalize(k)?.to_str().unwrap().to_string(),
803 None => downloader::download_kernel(&get_kernel_version(), arch)?,
804 };
805
806 let opensuse_rootfs = format!("{}/opensuse-rootfs", app_dir);
807 let squashfs_file = format!("{}/opensuse-rootfs.squashfs", app_dir);
808
809 downloader::download_opensuse_rootfs(arch)?;
810 rootfs::extract_squashfs(&squashfs_file, &opensuse_rootfs)?;
811
812 let img_file = format!("{}/opensuse-rootfs.img", app_dir);
813 if ssh_keys_changed(
814 &ssh_keys,
815 &format!("{}/root/.ssh/authorized_keys", opensuse_rootfs),
816 )? {
817 println!("[+] SSH keys have changed, removing existing image to regenerate.");
818 run_command("rm", &["-f", &img_file], true)?;
819 }
820
821 run_command(
822 "chroot",
823 &[&opensuse_rootfs, "systemctl", "enable", "sshd"],
824 true,
825 )?;
826
827 let ssh_key_name = "id_rsa";
828 match ssh_keys {
829 Some(ref keys) => ssh::copy_ssh_keys(keys, &opensuse_rootfs)?,
830 None => ssh::generate_and_copy_ssh_key(&ssh_key_name, &opensuse_rootfs)?,
831 }
832
833 rootfs::create_squashfs(&opensuse_rootfs, &img_file)?;
834
835 let ssh_key_file = match ssh_keys {
836 Some(_) => None,
837 None => Some(format!("{}/{}", app_dir, ssh_key_name)),
838 };
839
840 Ok((kernel_file, img_file, ssh_key_file))
841 }
842}
843
844impl RootfsPreparer for AlmalinuxPreparer {
845 fn name(&self) -> &'static str {
846 "AlmaLinux"
847 }
848
849 fn prepare(
850 &self,
851 arch: &str,
852 app_dir: &str,
853 kernel_file: Option<String>,
854 ssh_keys: Option<Vec<String>>,
855 ) -> Result<(String, String, Option<String>)> {
856 println!(
857 "[+] Preparing {} rootfs for {}...",
858 self.name(),
859 arch.bright_green()
860 );
861
862 let kernel_file = match kernel_file {
863 Some(k) => fs::canonicalize(k)?.to_str().unwrap().to_string(),
864 None => downloader::download_kernel(&get_kernel_version(), arch)?,
865 };
866
867 let almalinux_rootfs = format!("{}/almalinux-rootfs", app_dir);
868 let squashfs_file = format!("{}/almalinux-rootfs.squashfs", app_dir);
869
870 downloader::download_almalinux_rootfs(arch)?;
871 rootfs::extract_squashfs(&squashfs_file, &almalinux_rootfs)?;
872
873 let img_file = format!("{}/almalinux-rootfs.img", app_dir);
874 if ssh_keys_changed(
875 &ssh_keys,
876 &format!("{}/root/.ssh/authorized_keys", almalinux_rootfs),
877 )? {
878 println!("[+] SSH keys have changed, removing existing image to regenerate.");
879 run_command("rm", &["-f", &img_file], true)?;
880 }
881
882 let ssh_key_name = "id_rsa";
883 match ssh_keys {
884 Some(ref keys) => ssh::copy_ssh_keys(keys, &almalinux_rootfs)?,
885 None => ssh::generate_and_copy_ssh_key(&ssh_key_name, &almalinux_rootfs)?,
886 }
887
888 rootfs::create_squashfs(&almalinux_rootfs, &img_file)?;
889
890 let ssh_key_file = match ssh_keys {
891 Some(_) => None,
892 None => Some(format!("{}/{}", app_dir, ssh_key_name)),
893 };
894
895 Ok((kernel_file, img_file, ssh_key_file))
896 }
897}
898
899impl RootfsPreparer for RockyLinuxPreparer {
900 fn name(&self) -> &'static str {
901 "RockyLinux"
902 }
903
904 fn prepare(
905 &self,
906 arch: &str,
907 app_dir: &str,
908 kernel_file: Option<String>,
909 ssh_keys: Option<Vec<String>>,
910 ) -> Result<(String, String, Option<String>)> {
911 println!(
912 "[+] Preparing {} rootfs for {}...",
913 self.name(),
914 arch.bright_green()
915 );
916
917 let kernel_file = match kernel_file {
918 Some(k) => fs::canonicalize(k)?.to_str().unwrap().to_string(),
919 None => downloader::download_kernel(&get_kernel_version(), arch)?,
920 };
921
922 let rockylinux_rootfs = format!("{}/rockylinux-rootfs", app_dir);
923 let squashfs_file = format!("{}/rockylinux-rootfs.squashfs", app_dir);
924
925 downloader::download_rockylinux_rootfs(arch)?;
926 rootfs::extract_squashfs(&squashfs_file, &rockylinux_rootfs)?;
927
928 let img_file = format!("{}/rockylinux-rootfs.img", app_dir);
929 if ssh_keys_changed(
930 &ssh_keys,
931 &format!("{}/root/.ssh/authorized_keys", rockylinux_rootfs),
932 )? {
933 println!("[+] SSH keys have changed, removing existing image to regenerate.");
934 run_command("rm", &["-f", &img_file], true)?;
935 }
936
937 let ssh_key_name = "id_rsa";
938 match ssh_keys {
939 Some(ref keys) => ssh::copy_ssh_keys(keys, &rockylinux_rootfs)?,
940 None => ssh::generate_and_copy_ssh_key(&ssh_key_name, &rockylinux_rootfs)?,
941 }
942 rootfs::create_squashfs(&rockylinux_rootfs, &img_file)?;
943
944 let ssh_key_file = match ssh_keys {
945 Some(_) => None,
946 None => Some(format!("{}/{}", app_dir, ssh_key_name)),
947 };
948
949 Ok((kernel_file, img_file, ssh_key_file))
950 }
951}
952
953impl RootfsPreparer for ArchlinuxPreparer {
954 fn name(&self) -> &'static str {
955 "ArchLinux"
956 }
957
958 fn prepare(
959 &self,
960 arch: &str,
961 app_dir: &str,
962 kernel_file: Option<String>,
963 ssh_keys: Option<Vec<String>>,
964 ) -> Result<(String, String, Option<String>)> {
965 println!(
966 "[+] Preparing {} rootfs for {}...",
967 self.name(),
968 arch.bright_green()
969 );
970
971 let kernel_file = match kernel_file {
972 Some(k) => fs::canonicalize(k)?.to_str().unwrap().to_string(),
973 None => downloader::download_kernel(&get_kernel_version(), arch)?,
974 };
975 let archlinux_rootfs = format!("{}/archlinux-rootfs", app_dir);
976 let squashfs_file = format!("{}/archlinux-rootfs.squashfs", app_dir);
977
978 downloader::download_archlinux_rootfs(arch)?;
979 rootfs::extract_squashfs(&squashfs_file, &archlinux_rootfs)?;
980
981 let img_file = format!("{}/archlinux-rootfs.img", app_dir);
982 if ssh_keys_changed(
983 &ssh_keys,
984 &format!("{}/root/.ssh/authorized_keys", archlinux_rootfs),
985 )? {
986 println!("[+] SSH keys have changed, removing existing image to regenerate.");
987 run_command("rm", &["-f", &img_file], true)?;
988 }
989
990 run_command(
991 "chroot",
992 &[&archlinux_rootfs, "systemctl", "enable", "sshd"],
993 true,
994 )?;
995 run_command(
996 "chroot",
997 &[&archlinux_rootfs, "systemctl", "mask", "systemd-firstboot"],
998 true,
999 )?;
1000
1001 let ssh_key_name = "id_rsa";
1002 match ssh_keys {
1003 Some(ref keys) => ssh::copy_ssh_keys(keys, &archlinux_rootfs)?,
1004 None => ssh::generate_and_copy_ssh_key(&ssh_key_name, &archlinux_rootfs)?,
1005 }
1006
1007 rootfs::create_squashfs(&archlinux_rootfs, &img_file)?;
1008
1009 let ssh_key_file = match ssh_keys {
1010 Some(_) => None,
1011 None => Some(format!("{}/{}", app_dir, ssh_key_name)),
1012 };
1013
1014 Ok((kernel_file, img_file, ssh_key_file))
1015 }
1016}
1017
1018impl RootfsPreparer for OpensuseTumbleweedPreparer {
1019 fn name(&self) -> &'static str {
1020 "OpenSUSE (Tumbleweed)"
1021 }
1022
1023 fn prepare(
1024 &self,
1025 arch: &str,
1026 app_dir: &str,
1027 kernel_file: Option<String>,
1028 ssh_keys: Option<Vec<String>>,
1029 ) -> Result<(String, String, Option<String>)> {
1030 println!(
1031 "[+] Preparing {} rootfs for {}...",
1032 self.name(),
1033 arch.bright_green()
1034 );
1035
1036 let kernel_file = match kernel_file {
1037 Some(k) => fs::canonicalize(k)?.to_str().unwrap().to_string(),
1038 None => downloader::download_kernel(&get_kernel_version(), arch)?,
1039 };
1040
1041 let opensuse_rootfs = format!("{}/opensuse-tumbleweed-rootfs", app_dir);
1042 let squashfs_file = format!("{}/opensuse-tumbleweed-rootfs.squashfs", app_dir);
1043
1044 downloader::download_opensuse_tumbleweed_rootfs(arch)?;
1045 rootfs::extract_squashfs(&squashfs_file, &opensuse_rootfs)?;
1046
1047 let img_file = format!("{}/opensuse-tumbleweed-rootfs.img", app_dir);
1048 if ssh_keys_changed(
1049 &ssh_keys,
1050 &format!("{}/root/.ssh/authorized_keys", opensuse_rootfs),
1051 )? {
1052 println!("[+] SSH keys have changed, removing existing image to regenerate.");
1053 run_command("rm", &["-f", &img_file], true)?;
1054 }
1055
1056 run_command(
1057 "chroot",
1058 &[&opensuse_rootfs, "systemctl", "enable", "sshd"],
1059 true,
1060 )?;
1061
1062 let ssh_key_name = "id_rsa";
1063
1064 match ssh_keys {
1065 Some(ref keys) => ssh::copy_ssh_keys(keys, &opensuse_rootfs)?,
1066 None => ssh::generate_and_copy_ssh_key(&ssh_key_name, &opensuse_rootfs)?,
1067 }
1068
1069 rootfs::create_squashfs(&opensuse_rootfs, &img_file)?;
1070
1071 let ssh_key_file = match ssh_keys {
1072 Some(_) => None,
1073 None => Some(format!("{}/{}", app_dir, ssh_key_name)),
1074 };
1075
1076 Ok((kernel_file, img_file, ssh_key_file))
1077 }
1078}
1079
1080fn ssh_keys_changed(ssh_keys: &Option<Vec<String>>, authorized_keys_path: &str) -> Result<bool> {
1081 if ssh_keys.is_none() {
1082 return Ok(false);
1083 }
1084 let ssh_keys = ssh_keys.as_ref().unwrap();
1085 let mut hasher = Sha256::new();
1086 let ssh_keys_str = ssh_keys.join("\n");
1087
1088 let ssh_keys_str = match ssh_keys_str.ends_with('\n') {
1089 true => ssh_keys_str,
1090 false => format!("{}\n", ssh_keys_str),
1091 };
1092
1093 hasher.update(ssh_keys_str.as_bytes());
1094 let ssh_keys_hash = hasher.finalize();
1095
1096 if !run_command("test", &["-e", authorized_keys_path], true).is_ok() {
1097 return Ok(true);
1098 }
1099
1100 let output = run_command("cat", &[authorized_keys_path], true)?;
1101 let authorized_keys_content = String::from_utf8_lossy(&output.stdout);
1102 let mut hasher = Sha256::new();
1103 hasher.update(authorized_keys_content.as_bytes());
1104 let authorized_keys_hash = hasher.finalize();
1105
1106 Ok(ssh_keys_hash != authorized_keys_hash)
1107}