Prepare, configure, and manage Firecracker microVMs in seconds!
virtualization linux microvm firecracker
at main 1107 lines 34 kB view raw
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}