Prepare, configure, and manage Firecracker microVMs in seconds!
virtualization linux microvm firecracker

Merge pull request #5 from tsirysndr/feat/overlayfs

feat: add overlay initialization script for boot process

authored by tsiry-sandratraina.com and committed by

GitHub 61b8a7da 1450e006

+1112 -254
+2 -1
.gitignore
··· 1 1 target/ 2 - fire.toml 2 + fire.toml 3 + .vscode/
+1 -1
README.md
··· 17 17 18 18 ## Prerequisites 19 19 - [CoreDNS](https://coredns.io/) (for DNS resolution) 20 - - [NextDHCP](https://github.com/nextdhcp/nextdhcp) (for DHCP services) 20 + - [Kea DHCP](https://kea.readthedocs.io/en/latest/) (for DHCP services) 21 21 - [Mosquitto](https://mosquitto.org/) (MQTT Server) 22 22 23 23 ## Installation
+11
crates/fire-config/src/lib.rs
··· 5 5 use owo_colors::OwoColorize; 6 6 use serde::{Deserialize, Serialize}; 7 7 8 + #[derive(Debug, Clone, Serialize, Deserialize)] 9 + pub struct EtcdConfig { 10 + pub endpoints: Option<Vec<String>>, 11 + pub user: Option<String>, 12 + pub password: Option<String>, 13 + pub cacert: Option<String>, 14 + pub cert: Option<String>, 15 + } 16 + 8 17 #[derive(Debug, Serialize, Deserialize)] 9 18 pub struct Vm { 10 19 pub vcpu: Option<u16>, ··· 22 31 pub struct FireConfig { 23 32 pub distro: Distro, 24 33 pub vm: Vm, 34 + pub etcd: Option<EtcdConfig>, 25 35 } 26 36 27 37 impl Default for FireConfig { ··· 39 49 api_socket: None, 40 50 mac: None, 41 51 }, 52 + etcd: None, 42 53 } 43 54 } 44 55 }
+39
crates/firecracker-prepare/src/config/resolved.conf
··· 1 + # This file is part of systemd. 2 + # 3 + # systemd is free software; you can redistribute it and/or modify it under the 4 + # terms of the GNU Lesser General Public License as published by the Free 5 + # Software Foundation; either version 2.1 of the License, or (at your option) 6 + # any later version. 7 + # 8 + # Entries in this file show the compile time defaults. Local configuration 9 + # should be created by either modifying this file (or a copy of it placed in 10 + # /etc/ if the original file is shipped in /usr/), or by creating "drop-ins" in 11 + # the /etc/systemd/resolved.conf.d/ directory. The latter is generally 12 + # recommended. Defaults can be restored by simply deleting the main 13 + # configuration file and all drop-ins located in /etc/. 14 + # 15 + # Use 'systemd-analyze cat-config systemd/resolved.conf' to display the full config. 16 + # 17 + # See resolved.conf(5) for details. 18 + 19 + [Resolve] 20 + # Some examples of DNS servers which may be used for DNS= and FallbackDNS=: 21 + # Cloudflare: 1.1.1.1#cloudflare-dns.com 1.0.0.1#cloudflare-dns.com 2606:4700:4700::1111#cloudflare-dns.com 2606:4700:4700::1001#cloudflare-dns.com 22 + # Google: 8.8.8.8#dns.google 8.8.4.4#dns.google 2001:4860:4860::8888#dns.google 2001:4860:4860::8844#dns.google 23 + # Quad9: 9.9.9.9#dns.quad9.net 149.112.112.112#dns.quad9.net 2620:fe::fe#dns.quad9.net 2620:fe::9#dns.quad9.net 24 + #DNS= 25 + #FallbackDNS= 26 + DNS=1.1.1.1 8.8.8.8 8.8.4.4 172.16.0.1 27 + FallbackDNS=100.100.100.100 28 + #Domains= 29 + #DNSSEC=no 30 + #DNSOverTLS=no 31 + #MulticastDNS=no 32 + #LLMNR=no 33 + #Cache=no-negative 34 + #CacheFromLocalhost=no 35 + DNSStubListener=no 36 + #DNSStubListenerExtra= 37 + #ReadEtcHosts=yes 38 + #ResolveUnicastSingleLabel=no 39 + #StaleRetentionSec=0
+60 -1
crates/firecracker-prepare/src/downloader.rs
··· 174 174 pub fn download_nixos_rootfs(_arch: &str) -> Result<()> { 175 175 let app_dir = crate::config::get_config_dir()?; 176 176 let output = format!("{}/nixos-rootfs.squashfs", app_dir); 177 - download_file("https://public.rocksky.app/nixos-rootfs.squashfs", &output)?; 177 + download_file("https://public.rocksky.app/nixos-rootfs.img", &output)?; 178 + Ok(()) 179 + } 180 + 181 + pub fn download_fedora_rootfs(_arch: &str) -> Result<()> { 182 + let app_dir = crate::config::get_config_dir()?; 183 + let output = format!("{}/fedora-rootfs.squashfs", app_dir); 184 + download_file("https://public.rocksky.app/fedora-rootfs.img", &output)?; 185 + Ok(()) 186 + } 187 + 188 + pub fn download_gentoo_rootfs(_arch: &str) -> Result<()> { 189 + let app_dir = crate::config::get_config_dir()?; 190 + let output = format!("{}/gentoo-rootfs.squashfs", app_dir); 191 + download_file("https://public.rocksky.app/gentoo-rootfs.img", &output)?; 192 + Ok(()) 193 + } 194 + 195 + pub fn download_slackware_rootfs(_arch: &str) -> Result<()> { 196 + let app_dir = crate::config::get_config_dir()?; 197 + let output = format!("{}/slackware-rootfs.squashfs", app_dir); 198 + download_file("https://public.rocksky.app/slackware-rootfs.img", &output)?; 199 + Ok(()) 200 + } 201 + 202 + pub fn download_opensuse_rootfs(_arch: &str) -> Result<()> { 203 + let app_dir = crate::config::get_config_dir()?; 204 + let output = format!("{}/opensuse-rootfs.squashfs", app_dir); 205 + download_file("https://public.rocksky.app/opensuse-rootfs.img", &output)?; 206 + Ok(()) 207 + } 208 + 209 + pub fn download_opensuse_tumbleweed_rootfs(_arch: &str) -> Result<()> { 210 + let app_dir = crate::config::get_config_dir()?; 211 + let output = format!("{}/opensuse-tumbleweed-rootfs.squashfs", app_dir); 212 + download_file( 213 + "https://public.rocksky.app/opensuse-tumbleweed-rootfs.img", 214 + &output, 215 + )?; 216 + Ok(()) 217 + } 218 + 219 + pub fn download_almalinux_rootfs(_arch: &str) -> Result<()> { 220 + let app_dir = crate::config::get_config_dir()?; 221 + let output = format!("{}/almalinux-rootfs.squashfs", app_dir); 222 + download_file("https://public.rocksky.app/almalinux-rootfs.img", &output)?; 223 + Ok(()) 224 + } 225 + 226 + pub fn download_rockylinux_rootfs(_arch: &str) -> Result<()> { 227 + let app_dir = crate::config::get_config_dir()?; 228 + let output = format!("{}/rockylinux-rootfs.squashfs", app_dir); 229 + download_file("https://public.rocksky.app/rockylinux-rootfs.img", &output)?; 230 + Ok(()) 231 + } 232 + 233 + pub fn download_archlinux_rootfs(_arch: &str) -> Result<()> { 234 + let app_dir = crate::config::get_config_dir()?; 235 + let output = format!("{}/archlinux-rootfs.squashfs", app_dir); 236 + download_file("https://public.rocksky.app/archlinux-rootfs.img", &output)?; 178 237 Ok(()) 179 238 }
+421 -30
crates/firecracker-prepare/src/lib.rs
··· 20 20 Alpine, 21 21 Ubuntu, 22 22 NixOS, 23 + Fedora, 24 + Gentoo, 25 + Slackware, 26 + Opensuse, 27 + OpensuseTumbleweed, 28 + Almalinux, 29 + RockyLinux, 30 + Archlinux, 31 + } 32 + 33 + impl ToString for Distro { 34 + fn to_string(&self) -> String { 35 + match self { 36 + Distro::Debian => "debian".to_string(), 37 + Distro::Alpine => "alpine".to_string(), 38 + Distro::Ubuntu => "ubuntu".to_string(), 39 + Distro::NixOS => "nixos".to_string(), 40 + Distro::Fedora => "fedora".to_string(), 41 + Distro::Gentoo => "gentoo".to_string(), 42 + Distro::Slackware => "slackware".to_string(), 43 + Distro::Opensuse => "opensuse".to_string(), 44 + Distro::OpensuseTumbleweed => "opensuse-tumbleweed".to_string(), 45 + Distro::Almalinux => "almalinux".to_string(), 46 + Distro::RockyLinux => "rockylinux".to_string(), 47 + Distro::Archlinux => "archlinux".to_string(), 48 + } 49 + } 23 50 } 24 51 25 52 pub trait RootfsPreparer { ··· 31 58 pub struct AlpinePreparer; 32 59 pub struct UbuntuPreparer; 33 60 pub struct NixOSPreparer; 61 + pub struct FedoraPreparer; 62 + pub struct GentooPreparer; 63 + pub struct SlackwarePreparer; 64 + pub struct OpensusePreparer; 65 + pub struct OpensuseTumbleweedPreparer; 66 + pub struct AlmalinuxPreparer; 67 + pub struct RockyLinuxPreparer; 68 + pub struct ArchlinuxPreparer; 34 69 35 70 impl RootfsPreparer for DebianPreparer { 36 71 fn name(&self) -> &'static str { ··· 44 79 arch.bright_green() 45 80 ); 46 81 let kernel_file = downloader::download_kernel(arch)?; 47 - let debootstrap_dir = format!("{}/debootstrap", app_dir); 82 + let debootstrap_dir = format!("{}/debian-rootfs", app_dir); 48 83 49 84 let arch = match arch { 50 85 "x86_64" => "amd64", ··· 66 101 )?; 67 102 } 68 103 104 + run_command( 105 + "chroot", 106 + &[ 107 + &debootstrap_dir, 108 + "sh", 109 + "-c", 110 + "apt-get install -y systemd-resolved", 111 + ], 112 + true, 113 + )?; 114 + run_command( 115 + "chroot", 116 + &[ 117 + &debootstrap_dir, 118 + "systemctl", 119 + "enable", 120 + "systemd-networkd", 121 + "systemd-resolved", 122 + ], 123 + true, 124 + )?; 125 + 126 + const RESOLVED_CONF: &str = include_str!("./config/resolved.conf"); 127 + run_command( 128 + "chroot", 129 + &[ 130 + &debootstrap_dir, 131 + "sh", 132 + "-c", 133 + &format!("echo '{}' > /etc/systemd/resolved.conf", RESOLVED_CONF), 134 + ], 135 + true, 136 + )?; 137 + 69 138 let ssh_key_name = "id_rsa"; 70 139 run_command( 71 140 "mkdir", ··· 101 170 )?; 102 171 } 103 172 104 - let ext4_file = format!("{}/debian-{}.ext4", app_dir, arch); 105 - if !std::path::Path::new(&ext4_file).exists() { 106 - rootfs::create_ext4_filesystem(&debootstrap_dir, &ext4_file, 600)?; 107 - } 173 + let img_file = format!("{}/debian-rootfs.img", app_dir); 174 + rootfs::create_overlay_dirs(&debootstrap_dir)?; 175 + rootfs::add_overlay_init(&debootstrap_dir)?; 176 + rootfs::create_squashfs(&debootstrap_dir, &img_file)?; 108 177 109 178 let ssh_key_file = format!("{}/{}", app_dir, ssh_key_name); 110 179 111 - Ok((kernel_file, ext4_file, ssh_key_file)) 180 + Ok((kernel_file, img_file, ssh_key_file)) 112 181 } 113 182 } 114 183 ··· 227 296 let ssh_key_name = "id_rsa"; 228 297 ssh::generate_and_copy_ssh_key(&ssh_key_name, &minirootfs)?; 229 298 230 - let ext4_file = format!("{}/alpine-{}.ext4", app_dir, arch); 231 - if !std::path::Path::new(&ext4_file).exists() { 232 - rootfs::create_ext4_filesystem(&minirootfs, &ext4_file, 500)?; 233 - } 299 + let img_file = format!("{}/alpine-rootfs.img", app_dir); 300 + rootfs::create_squashfs(&minirootfs, &img_file)?; 234 301 235 302 let ssh_key_file = format!("{}/{}", app_dir, ssh_key_name); 236 303 237 - Ok((kernel_file, ext4_file, ssh_key_file)) 304 + Ok((kernel_file, img_file, ssh_key_file)) 238 305 } 239 306 } 240 307 ··· 249 316 self.name(), 250 317 arch.bright_green() 251 318 ); 252 - let (kernel_file, ubuntu_file, ubuntu_version) = downloader::download_files(arch)?; 319 + let (kernel_file, ubuntu_file, _ubuntu_version) = downloader::download_files(arch)?; 253 320 254 321 let squashfs_root_dir = format!("{}/squashfs_root", app_dir); 255 322 rootfs::extract_squashfs(&ubuntu_file, &squashfs_root_dir)?; 256 323 324 + run_command( 325 + "chroot", 326 + &[ 327 + &squashfs_root_dir, 328 + "systemctl", 329 + "enable", 330 + "systemd-networkd", 331 + ], 332 + true, 333 + )?; 334 + 335 + const RESOLVED_CONF: &str = include_str!("./config/resolved.conf"); 336 + run_command( 337 + "chroot", 338 + &[ 339 + &squashfs_root_dir, 340 + "sh", 341 + "-c", 342 + &format!("echo '{}' > /etc/systemd/resolved.conf", RESOLVED_CONF), 343 + ], 344 + true, 345 + )?; 346 + 257 347 let ssh_key_name = "id_rsa"; 258 348 ssh::generate_and_copy_ssh_key(&ssh_key_name, &squashfs_root_dir)?; 259 349 260 - let ext4_file = format!("{}/ubuntu-{}.ext4", app_dir, ubuntu_version); 261 - if !std::path::Path::new(&ext4_file).exists() { 262 - rootfs::create_ext4_filesystem(&squashfs_root_dir, &ext4_file, 400)?; 263 - } else { 264 - println!( 265 - "[!] {} already exists, skipping ext4 creation.", 266 - ext4_file.bright_yellow() 267 - ); 268 - } 350 + let img_file = format!("{}/ubuntu-rootfs.img", app_dir); 351 + rootfs::create_overlay_dirs(&squashfs_root_dir)?; 352 + rootfs::add_overlay_init(&squashfs_root_dir)?; 353 + rootfs::create_squashfs(&squashfs_root_dir, &img_file)?; 269 354 270 355 let ssh_key_file = format!("{}/{}", app_dir, ssh_key_name); 271 356 272 - Ok((kernel_file, ext4_file, ssh_key_file)) 357 + Ok((kernel_file, img_file, ssh_key_file)) 273 358 } 274 359 } 275 360 ··· 285 370 arch.bright_green() 286 371 ); 287 372 let kernel_file = downloader::download_kernel(arch)?; 288 - let nixos_rootfs = format!("{}/nixosrootfs", app_dir); 373 + let nixos_rootfs = format!("{}/nixos-rootfs", app_dir); 289 374 let squashfs_file = format!("{}/nixos-rootfs.squashfs", app_dir); 290 375 291 376 downloader::download_nixos_rootfs(arch)?; ··· 294 379 let ssh_key_name = "id_rsa"; 295 380 ssh::generate_and_copy_ssh_key_nixos(&ssh_key_name, &nixos_rootfs)?; 296 381 297 - let ext4_file = format!("{}/nixos-rootfs.ext4", app_dir); 298 - if !std::path::Path::new(&ext4_file).exists() { 299 - rootfs::create_ext4_filesystem(&nixos_rootfs, &ext4_file, 5120)?; 300 - } 382 + let img_file = format!("{}/nixos-rootfs.img", app_dir); 383 + rootfs::create_squashfs(&nixos_rootfs, &img_file)?; 301 384 302 385 let ssh_key_file = format!("{}/{}", app_dir, ssh_key_name); 303 386 ··· 307 390 nixos_rootfs.bright_green() 308 391 ); 309 392 310 - Ok((kernel_file, ext4_file, ssh_key_file)) 393 + Ok((kernel_file, img_file, ssh_key_file)) 394 + } 395 + } 396 + 397 + impl RootfsPreparer for FedoraPreparer { 398 + fn name(&self) -> &'static str { 399 + "Fedora" 400 + } 401 + 402 + fn prepare(&self, arch: &str, app_dir: &str) -> Result<(String, String, String)> { 403 + println!( 404 + "[+] Preparing {} rootfs for {}...", 405 + self.name(), 406 + arch.bright_green() 407 + ); 408 + 409 + let kernel_file = downloader::download_kernel(arch)?; 410 + let fedora_rootfs = format!("{}/fedora-rootfs", app_dir); 411 + let squashfs_file = format!("{}/fedora-rootfs.squashfs", app_dir); 412 + 413 + downloader::download_fedora_rootfs(arch)?; 414 + rootfs::extract_squashfs(&squashfs_file, &fedora_rootfs)?; 415 + 416 + run_command( 417 + "chroot", 418 + &[&fedora_rootfs, "systemctl", "enable", "sshd"], 419 + true, 420 + )?; 421 + 422 + let ssh_key_name = "id_rsa"; 423 + ssh::generate_and_copy_ssh_key(&ssh_key_name, &fedora_rootfs)?; 424 + 425 + let img_file = format!("{}/fedora-rootfs.img", app_dir); 426 + rootfs::create_squashfs(&fedora_rootfs, &img_file)?; 427 + 428 + let ssh_key_file = format!("{}/{}", app_dir, ssh_key_name); 429 + 430 + println!( 431 + "[+] {} rootfs prepared at: {}", 432 + self.name(), 433 + fedora_rootfs.bright_green() 434 + ); 435 + 436 + Ok((kernel_file, img_file, ssh_key_file)) 437 + } 438 + } 439 + 440 + impl RootfsPreparer for GentooPreparer { 441 + fn name(&self) -> &'static str { 442 + "Gentoo" 443 + } 444 + 445 + fn prepare(&self, arch: &str, app_dir: &str) -> Result<(String, String, String)> { 446 + println!( 447 + "[+] Preparing {} rootfs for {}...", 448 + self.name(), 449 + arch.bright_green() 450 + ); 451 + 452 + let kernel_file = downloader::download_kernel(arch)?; 453 + let gentoo_rootfs = format!("{}/gentoo-rootfs", app_dir); 454 + let squashfs_file = format!("{}/gentoo-rootfs.squashfs", app_dir); 455 + 456 + downloader::download_gentoo_rootfs(arch)?; 457 + rootfs::extract_squashfs(&squashfs_file, &gentoo_rootfs)?; 458 + 459 + // Enable sshd service 460 + run_command( 461 + "chroot", 462 + &[&gentoo_rootfs, "systemctl", "enable", "sshd"], 463 + true, 464 + )?; 465 + 466 + let ssh_key_name = "id_rsa"; 467 + ssh::generate_and_copy_ssh_key(&ssh_key_name, &gentoo_rootfs)?; 468 + 469 + let img_file = format!("{}/gentoo-rootfs.img", app_dir); 470 + rootfs::create_squashfs(&gentoo_rootfs, &img_file)?; 471 + 472 + let ssh_key_file = format!("{}/{}", app_dir, ssh_key_name); 473 + 474 + Ok((kernel_file, img_file, ssh_key_file)) 475 + } 476 + } 477 + 478 + impl RootfsPreparer for SlackwarePreparer { 479 + fn name(&self) -> &'static str { 480 + "Slackware" 481 + } 482 + 483 + fn prepare(&self, arch: &str, app_dir: &str) -> Result<(String, String, String)> { 484 + println!( 485 + "[+] Preparing {} rootfs for {}...", 486 + self.name(), 487 + arch.bright_green() 488 + ); 489 + 490 + let kernel_file = downloader::download_kernel(arch)?; 491 + let slackware_rootfs = format!("{}/slackware-rootfs", app_dir); 492 + let squashfs_file = format!("{}/slackware-rootfs.squashfs", app_dir); 493 + 494 + downloader::download_slackware_rootfs(arch)?; 495 + rootfs::extract_squashfs(&squashfs_file, &slackware_rootfs)?; 496 + 497 + run_command( 498 + "chroot", 499 + &[ 500 + &slackware_rootfs, 501 + "ln", 502 + "-sf", 503 + "/etc/rc.d/rc.sshd", 504 + "/etc/rc.d/rc3.d/S50sshd", 505 + ], 506 + true, 507 + )?; 508 + 509 + let ssh_key_name = "id_rsa"; 510 + ssh::generate_and_copy_ssh_key(&ssh_key_name, &slackware_rootfs)?; 511 + 512 + let img_file = format!("{}/slackware-rootfs.img", app_dir); 513 + rootfs::create_squashfs(&slackware_rootfs, &img_file)?; 514 + 515 + let ssh_key_file = format!("{}/{}", app_dir, ssh_key_name); 516 + 517 + Ok((kernel_file, img_file, ssh_key_file)) 518 + } 519 + } 520 + 521 + impl RootfsPreparer for OpensusePreparer { 522 + fn name(&self) -> &'static str { 523 + "OpenSUSE (Leap)" 524 + } 525 + 526 + fn prepare(&self, arch: &str, app_dir: &str) -> Result<(String, String, String)> { 527 + println!( 528 + "[+] Preparing {} rootfs for {}...", 529 + self.name(), 530 + arch.bright_green() 531 + ); 532 + 533 + let kernel_file = downloader::download_kernel(arch)?; 534 + let opensuse_rootfs = format!("{}/opensuse-rootfs", app_dir); 535 + let squashfs_file = format!("{}/opensuse-rootfs.squashfs", app_dir); 536 + 537 + downloader::download_opensuse_rootfs(arch)?; 538 + rootfs::extract_squashfs(&squashfs_file, &opensuse_rootfs)?; 539 + 540 + run_command( 541 + "chroot", 542 + &[&opensuse_rootfs, "systemctl", "enable", "sshd"], 543 + true, 544 + )?; 545 + 546 + let ssh_key_name = "id_rsa"; 547 + ssh::generate_and_copy_ssh_key(&ssh_key_name, &opensuse_rootfs)?; 548 + 549 + let img_file = format!("{}/opensuse-rootfs.img", app_dir); 550 + rootfs::create_squashfs(&opensuse_rootfs, &img_file)?; 551 + 552 + let ssh_key_file = format!("{}/{}", app_dir, ssh_key_name); 553 + Ok((kernel_file, img_file, ssh_key_file)) 554 + } 555 + } 556 + 557 + impl RootfsPreparer for AlmalinuxPreparer { 558 + fn name(&self) -> &'static str { 559 + "AlmaLinux" 560 + } 561 + 562 + fn prepare(&self, arch: &str, app_dir: &str) -> Result<(String, String, String)> { 563 + println!( 564 + "[+] Preparing {} rootfs for {}...", 565 + self.name(), 566 + arch.bright_green() 567 + ); 568 + 569 + let kernel_file = downloader::download_kernel(arch)?; 570 + let almalinux_rootfs = format!("{}/almalinux-rootfs", app_dir); 571 + let squashfs_file = format!("{}/almalinux-rootfs.squashfs", app_dir); 572 + 573 + downloader::download_almalinux_rootfs(arch)?; 574 + rootfs::extract_squashfs(&squashfs_file, &almalinux_rootfs)?; 575 + 576 + let ssh_key_name = "id_rsa"; 577 + ssh::generate_and_copy_ssh_key(&ssh_key_name, &almalinux_rootfs)?; 578 + 579 + let img_file = format!("{}/almalinux-rootfs.img", app_dir); 580 + rootfs::create_squashfs(&almalinux_rootfs, &img_file)?; 581 + 582 + let ssh_key_file = format!("{}/{}", app_dir, ssh_key_name); 583 + 584 + Ok((kernel_file, img_file, ssh_key_file)) 585 + } 586 + } 587 + 588 + impl RootfsPreparer for RockyLinuxPreparer { 589 + fn name(&self) -> &'static str { 590 + "RockyLinux" 591 + } 592 + 593 + fn prepare(&self, arch: &str, app_dir: &str) -> Result<(String, String, String)> { 594 + println!( 595 + "[+] Preparing {} rootfs for {}...", 596 + self.name(), 597 + arch.bright_green() 598 + ); 599 + 600 + let kernel_file = downloader::download_kernel(arch)?; 601 + let rockylinux_rootfs = format!("{}/rockylinux-rootfs", app_dir); 602 + let squashfs_file = format!("{}/rockylinux-rootfs.squashfs", app_dir); 603 + 604 + downloader::download_rockylinux_rootfs(arch)?; 605 + rootfs::extract_squashfs(&squashfs_file, &rockylinux_rootfs)?; 606 + 607 + let ssh_key_name = "id_rsa"; 608 + ssh::generate_and_copy_ssh_key(&ssh_key_name, &rockylinux_rootfs)?; 609 + 610 + let img_file = format!("{}/rockylinux-rootfs.img", app_dir); 611 + rootfs::create_squashfs(&rockylinux_rootfs, &img_file)?; 612 + 613 + let ssh_key_file = format!("{}/{}", app_dir, ssh_key_name); 614 + 615 + Ok((kernel_file, img_file, ssh_key_file)) 616 + } 617 + } 618 + 619 + impl RootfsPreparer for ArchlinuxPreparer { 620 + fn name(&self) -> &'static str { 621 + "ArchLinux" 622 + } 623 + 624 + fn prepare(&self, arch: &str, app_dir: &str) -> Result<(String, String, String)> { 625 + println!( 626 + "[+] Preparing {} rootfs for {}...", 627 + self.name(), 628 + arch.bright_green() 629 + ); 630 + 631 + let kernel_file = downloader::download_kernel(arch)?; 632 + let archlinux_rootfs = format!("{}/archlinux-rootfs", app_dir); 633 + let squashfs_file = format!("{}/archlinux-rootfs.squashfs", app_dir); 634 + 635 + downloader::download_archlinux_rootfs(arch)?; 636 + rootfs::extract_squashfs(&squashfs_file, &archlinux_rootfs)?; 637 + 638 + run_command( 639 + "chroot", 640 + &[&archlinux_rootfs, "systemctl", "enable", "sshd"], 641 + true, 642 + )?; 643 + run_command( 644 + "chroot", 645 + &[&archlinux_rootfs, "systemctl", "mask", "systemd-firstboot"], 646 + true, 647 + )?; 648 + 649 + let ssh_key_name = "id_rsa"; 650 + ssh::generate_and_copy_ssh_key(&ssh_key_name, &archlinux_rootfs)?; 651 + 652 + let img_file = format!("{}/archlinux-rootfs.img", app_dir); 653 + rootfs::create_squashfs(&archlinux_rootfs, &img_file)?; 654 + 655 + let ssh_key_file = format!("{}/{}", app_dir, ssh_key_name); 656 + 657 + Ok((kernel_file, img_file, ssh_key_file)) 658 + } 659 + } 660 + 661 + impl RootfsPreparer for OpensuseTumbleweedPreparer { 662 + fn name(&self) -> &'static str { 663 + "OpenSUSE (Tumbleweed)" 664 + } 665 + 666 + fn prepare(&self, arch: &str, app_dir: &str) -> Result<(String, String, String)> { 667 + println!( 668 + "[+] Preparing {} rootfs for {}...", 669 + self.name(), 670 + arch.bright_green() 671 + ); 672 + 673 + let kernel_file = downloader::download_kernel(arch)?; 674 + let opensuse_rootfs = format!("{}/opensuse-tumbleweed-rootfs", app_dir); 675 + let squashfs_file = format!("{}/opensuse-tumbleweed-rootfs.squashfs", app_dir); 676 + 677 + downloader::download_opensuse_tumbleweed_rootfs(arch)?; 678 + rootfs::extract_squashfs(&squashfs_file, &opensuse_rootfs)?; 679 + 680 + run_command( 681 + "chroot", 682 + &[&opensuse_rootfs, "systemctl", "enable", "sshd"], 683 + true, 684 + )?; 685 + 686 + let ssh_key_name = "id_rsa"; 687 + ssh::generate_and_copy_ssh_key(&ssh_key_name, &opensuse_rootfs)?; 688 + 689 + let img_file = format!("{}/opensuse-tumbleweed-rootfs.img", app_dir); 690 + rootfs::create_squashfs(&opensuse_rootfs, &img_file)?; 691 + 692 + let ssh_key_file = format!("{}/{}", app_dir, ssh_key_name); 693 + Ok((kernel_file, img_file, ssh_key_file)) 311 694 } 312 695 } 313 696 ··· 322 705 Distro::Alpine => Box::new(AlpinePreparer), 323 706 Distro::Ubuntu => Box::new(UbuntuPreparer), 324 707 Distro::NixOS => Box::new(NixOSPreparer), 708 + Distro::Fedora => Box::new(FedoraPreparer), 709 + Distro::Gentoo => Box::new(GentooPreparer), 710 + Distro::Slackware => Box::new(SlackwarePreparer), 711 + Distro::Opensuse => Box::new(OpensusePreparer), 712 + Distro::OpensuseTumbleweed => Box::new(OpensuseTumbleweedPreparer), 713 + Distro::Almalinux => Box::new(AlmalinuxPreparer), 714 + Distro::RockyLinux => Box::new(RockyLinuxPreparer), 715 + Distro::Archlinux => Box::new(ArchlinuxPreparer), 325 716 }; 326 717 327 - let (kernel_file, ext4_file, ssh_key_file) = preparer.prepare(&arch, &app_dir)?; 718 + let (kernel_file, img_file, ssh_key_file) = preparer.prepare(&arch, &app_dir)?; 328 719 329 720 println!("[✓] Kernel: {}", kernel_file.bright_green()); 330 - println!("[✓] Rootfs: {}", ext4_file.bright_green()); 721 + println!("[✓] Rootfs: {}", img_file.bright_green()); 331 722 println!("[✓] SSH Key: {}", ssh_key_file.bright_green()); 332 723 333 724 Ok(())
+47 -1
crates/firecracker-prepare/src/rootfs.rs
··· 1 1 use anyhow::Result; 2 2 3 - use crate::command::run_command; 3 + use crate::command::{run_command, run_command_with_stdout_inherit}; 4 4 5 5 pub fn extract_squashfs(squashfs_file: &str, output_dir: &str) -> Result<()> { 6 6 if std::path::Path::new(output_dir).exists() { ··· 26 26 run_command("mkfs.ext4", &["-d", squashfs_dir, "-F", output_file], true)?; 27 27 Ok(()) 28 28 } 29 + 30 + pub fn create_squashfs(squashfs_dir: &str, output_file: &str) -> Result<()> { 31 + if std::path::Path::new(output_file).exists() { 32 + println!( 33 + "[!] Warning: {} already exists, skipping. Delete it and try again if you want to recreate it.", 34 + output_file 35 + ); 36 + return Ok(()); 37 + } 38 + run_command_with_stdout_inherit("mksquashfs", &[squashfs_dir, output_file], true)?; 39 + Ok(()) 40 + } 41 + 42 + pub fn create_overlay_dirs(rootfs_dir: &str) -> Result<()> { 43 + run_command( 44 + "mkdir", 45 + &[ 46 + "-p", 47 + &format!("{}/overlay/work", rootfs_dir), 48 + &format!("{}/overlay/root", rootfs_dir), 49 + &format!("{}/rom", rootfs_dir), 50 + ], 51 + true, 52 + )?; 53 + Ok(()) 54 + } 55 + 56 + pub fn add_overlay_init(rootfs_dir: &str) -> Result<()> { 57 + const OVERLAY_INIT: &str = include_str!("./scripts/overlay-init.sh"); 58 + // add overlay-init script to rootfs/sbin/overlay-init 59 + println!("Adding overlay-init script..."); 60 + std::fs::write("/tmp/overlay-init", OVERLAY_INIT)?; 61 + run_command("mkdir", &["-p", &format!("{}/sbin", rootfs_dir)], true)?; 62 + run_command( 63 + "mv", 64 + &["/tmp/overlay-init", &format!("{}/sbin", rootfs_dir)], 65 + true, 66 + )?; 67 + println!("Making overlay-init executable..."); 68 + run_command( 69 + "chmod", 70 + &["+x", &format!("{}/sbin/overlay-init", rootfs_dir)], 71 + true, 72 + )?; 73 + Ok(()) 74 + }
+62
crates/firecracker-prepare/src/scripts/overlay-init.sh
··· 1 + #!/bin/sh 2 + # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 + # 4 + # Licensed under the Apache License, Version 2.0 (the "License"). You may 5 + # not use this file except in compliance with the License. A copy of the 6 + # License is located at 7 + # 8 + # http://aws.amazon.com/apache2.0/ 9 + # 10 + # or in the "license" file accompanying this file. This file is distributed 11 + # on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 12 + # express or implied. See the License for the specific language governing 13 + # permissions and limitations under the License. 14 + 15 + # Parameters: 16 + # 1. rw_root -- path where the read/write root is mounted 17 + # 2. work_dir -- path to the overlay workdir (must be on same filesystem as rw_root) 18 + # Overlay will be set up on /mnt, original root on /mnt/rom 19 + pivot() { 20 + local rw_root work_dir 21 + rw_root="$1" 22 + work_dir="$2" 23 + /bin/mount \ 24 + -o noatime,lowerdir=/,upperdir=${rw_root},workdir=${work_dir} \ 25 + -t overlay "overlayfs:${rw_root}" /mnt 26 + pivot_root /mnt /mnt/rom 27 + } 28 + 29 + # Overlay is configured under /overlay 30 + # Global variable $overlay_root is expected to be set to either: 31 + # "ram", which configures a tmpfs as the rw overlay layer (this is 32 + # the default, if the variable is unset) 33 + # - or - 34 + # A block device name, relative to /dev, in which case it is assumed 35 + # to contain an ext4 filesystem suitable for use as a rw overlay 36 + # layer. e.g. "vdb" 37 + do_overlay() { 38 + local overlay_dir="/overlay" 39 + if [ "$overlay_root" = ram ] || 40 + [ -z "$overlay_root" ]; then 41 + /bin/mount -t tmpfs -o noatime,mode=0755 tmpfs /overlay 42 + else 43 + /bin/mount -t ext4 "/dev/$overlay_root" /overlay 44 + fi 45 + mkdir -p /overlay/root /overlay/work 46 + pivot /overlay/root /overlay/work 47 + } 48 + 49 + # If we're given an overlay, ensure that it really exists. Panic if not. 50 + if [ -n "$overlay_root" ] && 51 + [ "$overlay_root" != ram ] && 52 + [ ! -b "/dev/$overlay_root" ]; then 53 + echo -n "FATAL: " 54 + echo "Overlay root given as $overlay_root but /dev/$overlay_root does not exist" 55 + exit 1 56 + fi 57 + 58 + do_overlay 59 + 60 + # invoke the actual system init program and proceed with the boot 61 + # process. 62 + exec /sbin/init $@
+5
crates/firecracker-prepare/src/ssh.rs
··· 12 12 ); 13 13 let pub_key_path = format!("{}/{}.pub", app_dir, key_name); 14 14 let auth_keys_path = format!("{}/root/.ssh/authorized_keys", squashfs_root_dir); 15 + run_command( 16 + "mkdir", 17 + &["-p", &format!("{}/root/.ssh", squashfs_root_dir)], 18 + true, 19 + )?; 15 20 run_command("cp", &[&pub_key_path, &auth_keys_path], true)?; 16 21 return Ok(()); 17 22 }
+13
crates/firecracker-up/src/cmd/start.rs
··· 7 7 use crate::cmd::up::up; 8 8 9 9 pub async fn start(name: &str) -> Result<(), Error> { 10 + let etcd = match fire_config::read_config() { 11 + Ok(config) => config.etcd, 12 + Err(_) => None, 13 + }; 10 14 let pool = firecracker_state::create_connection_pool().await?; 11 15 let vm = repo::virtual_machine::find(&pool, name).await?; 12 16 if vm.is_none() { ··· 21 25 alpine: Some(vm.distro == "alpine"), 22 26 ubuntu: Some(vm.distro == "ubuntu"), 23 27 nixos: Some(vm.distro == "nixos"), 28 + fedora: Some(vm.distro == "fedora"), 29 + gentoo: Some(vm.distro == "gentoo"), 30 + slackware: Some(vm.distro == "slackware"), 31 + opensuse: Some(vm.distro == "opensuse"), 32 + opensuse_tumbleweed: Some(vm.distro == "opensuse-tumbleweed"), 33 + almalinux: Some(vm.distro == "almalinux"), 34 + rockylinux: Some(vm.distro == "rockylinux"), 35 + archlinux: Some(vm.distro == "archlinux"), 24 36 vcpu: vm.vcpu, 25 37 memory: vm.memory, 26 38 vmlinux: vm.vmlinux, ··· 30 42 tap: vm.tap, 31 43 api_socket: vm.api_socket, 32 44 mac_address: vm.mac_address, 45 + etcd, 33 46 }) 34 47 .await?; 35 48
+72
crates/firecracker-up/src/main.rs
··· 59 59 .arg(arg!(--debian "Prepare Debian MicroVM").default_value("false")) 60 60 .arg(arg!(--alpine "Prepare Alpine MicroVM").default_value("false")) 61 61 .arg(arg!(--nixos "Prepare NixOS MicroVM").default_value("false")) 62 + .arg(arg!(--fedora "Prepare Fedora MicroVM").default_value("false")) 63 + .arg(arg!(--gentoo "Prepare Gentoo MicroVM").default_value("false")) 64 + .arg(arg!(--slackware "Prepare Slackware MicroVM").default_value("false")) 65 + .arg(arg!(--opensuse "Prepare OpenSUSE MicroVM").default_value("false")) 66 + .arg( 67 + Arg::new("opensuse-tumbleweed") 68 + .help("Prepare OpenSUSE Tumbleweed MicroVM") 69 + .action(clap::ArgAction::SetTrue), 70 + ) 71 + .arg(arg!(--almalinux "Prepare AlmaLinux MicroVM").default_value("false")) 72 + .arg(arg!(--rockylinux "Prepare RockyLinux MicroVM").default_value("false")) 73 + .arg(arg!(--archlinux "Prepare ArchLinux MicroVM").default_value("false")) 62 74 .arg(arg!(--ubuntu "Prepare Ubuntu MicroVM").default_value("true")) 63 75 .arg(arg!(--vcpu <n> "Number of vCPUs")) 64 76 .arg(arg!(--memory <m> "Memory size in MiB")) ··· 115 127 .arg(arg!(--debian "Prepare Debian MicroVM").default_value("false")) 116 128 .arg(arg!(--alpine "Prepare Alpine MicroVM").default_value("false")) 117 129 .arg(arg!(--nixos "Prepare NixOS MicroVM").default_value("false")) 130 + .arg(arg!(--fedora "Prepare Fedora MicroVM").default_value("false")) 131 + .arg(arg!(--gentoo "Prepare Gentoo MicroVM").default_value("false")) 132 + .arg(arg!(--slackware "Prepare Slackware MicroVM").default_value("false")) 133 + .arg(arg!(--opensuse "Prepare OpenSUSE MicroVM").default_value("false")) 134 + .arg( 135 + Arg::new("opensuse-tumbleweed") 136 + .long("opensuse-tumbleweed") 137 + .help("Prepare OpenSUSE Tumbleweed MicroVM") 138 + .action(clap::ArgAction::SetTrue), 139 + ) 140 + .arg(arg!(--almalinux "Prepare AlmaLinux MicroVM").default_value("false")) 141 + .arg(arg!(--rockylinux "Prepare RockyLinux MicroVM").default_value("false")) 142 + .arg(arg!(--archlinux "Prepare ArchLinux MicroVM").default_value("false")) 118 143 .arg(arg!(--ubuntu "Prepare Ubuntu MicroVM").default_value("true")) 119 144 .arg(arg!(--vcpu <n> "Number of vCPUs")) 120 145 .arg(arg!(--memory <m> "Memory size in MiB")) ··· 197 222 alpine: args.get_one::<bool>("alpine").copied(), 198 223 ubuntu: args.get_one::<bool>("ubuntu").copied(), 199 224 nixos: args.get_one::<bool>("nixos").copied(), 225 + fedora: args.get_one::<bool>("fedora").copied(), 226 + gentoo: args.get_one::<bool>("gentoo").copied(), 227 + slackware: args.get_one::<bool>("slackware").copied(), 228 + opensuse: args.get_one::<bool>("opensuse").copied(), 229 + opensuse_tumbleweed: args.get_one::<bool>("opensuse-tumbleweed").copied(), 230 + almalinux: args.get_one::<bool>("almalinux").copied(), 231 + rockylinux: args.get_one::<bool>("rockylinux").copied(), 232 + archlinux: args.get_one::<bool>("archlinux").copied(), 200 233 vcpu, 201 234 memory, 202 235 vmlinux, ··· 206 239 tap, 207 240 api_socket, 208 241 mac_address, 242 + etcd: None, 209 243 }; 210 244 up(options).await? 211 245 } ··· 236 270 let alpine = matches.get_one::<bool>("alpine").copied().unwrap_or(false); 237 271 let nixos = matches.get_one::<bool>("nixos").copied().unwrap_or(false); 238 272 let ubuntu = matches.get_one::<bool>("ubuntu").copied().unwrap_or(false); 273 + let fedora = matches.get_one::<bool>("fedora").copied().unwrap_or(false); 274 + let gentoo = matches.get_one::<bool>("gentoo").copied().unwrap_or(false); 275 + let slackware = matches 276 + .get_one::<bool>("slackware") 277 + .copied() 278 + .unwrap_or(false); 279 + let opensuse = matches 280 + .get_one::<bool>("opensuse") 281 + .copied() 282 + .unwrap_or(false); 283 + let opensuse_tumbleweed = matches 284 + .get_one::<bool>("opensuse-tumbleweed") 285 + .copied() 286 + .unwrap_or(false); 287 + let almalinux = matches 288 + .get_one::<bool>("almalinux") 289 + .copied() 290 + .unwrap_or(false); 291 + let rockylinux = matches 292 + .get_one::<bool>("rockylinux") 293 + .copied() 294 + .unwrap_or(false); 295 + let archlinux = matches 296 + .get_one::<bool>("archlinux") 297 + .copied() 298 + .unwrap_or(false); 299 + 239 300 let vcpu = matches 240 301 .get_one::<String>("vcpu") 241 302 .map(|s| s.parse::<u16>().unwrap()) 242 303 .unwrap_or(num_cpus::get() as u16); 304 + 243 305 let memory = matches 244 306 .get_one::<String>("memory") 245 307 .map(|s| s.parse::<u16>().unwrap()) 246 308 .unwrap_or(if nixos { 2048 } else { 512 }); 309 + 247 310 let vmlinux = matches.get_one::<String>("vmlinux").cloned(); 248 311 let rootfs = matches.get_one::<String>("rootfs").cloned(); 249 312 let bootargs = matches.get_one::<String>("boot-args").cloned(); ··· 263 326 alpine: Some(alpine), 264 327 ubuntu: Some(ubuntu), 265 328 nixos: Some(nixos), 329 + fedora: Some(fedora), 330 + gentoo: Some(gentoo), 331 + slackware: Some(slackware), 332 + opensuse: Some(opensuse), 333 + opensuse_tumbleweed: Some(opensuse_tumbleweed), 334 + almalinux: Some(almalinux), 335 + rockylinux: Some(rockylinux), 336 + archlinux: Some(archlinux), 266 337 vcpu, 267 338 memory, 268 339 vmlinux, ··· 272 343 tap, 273 344 api_socket, 274 345 mac_address, 346 + etcd: None, 275 347 }; 276 348 up(options).await? 277 349 }
+51
crates/firecracker-vm/src/apparmor/usr.sbin.kea-dhcp4
··· 1 + abi <abi/3.0>, 2 + 3 + include <tunables/global> 4 + 5 + profile kea-dhcp4 /usr/sbin/kea-dhcp4 { 6 + include <abstractions/base> 7 + include <abstractions/nameservice> 8 + 9 + # for MySQL access, localhost 10 + include <abstractions/mysql> 11 + include <abstractions/openssl> 12 + 13 + capability net_bind_service, 14 + capability net_raw, 15 + 16 + network inet dgram, 17 + network inet stream, 18 + network netlink raw, 19 + network packet raw, 20 + 21 + /etc/gss/mech.d/ r, 22 + /etc/gss/mech.d/* r, 23 + 24 + /etc/kea/ r, 25 + /etc/kea/** r, 26 + /usr/sbin/kea-dhcp4 mr, 27 + /usr/sbin/kea-lfc Px, 28 + 29 + owner /run/kea/kea-dhcp4.kea-dhcp4.pid rw, 30 + owner /run/lock/kea/logger_lockfile rwk, 31 + 32 + # Control sockets 33 + # Before LP: #1863100, these were in /tmp. For compatibility, let's keep both 34 + # locations 35 + owner /{tmp,run/kea}/kea4-ctrl-socket w, 36 + owner /{tmp,run/kea}/kea4-ctrl-socket.lock rwk, 37 + 38 + # this includes .completed, .output, .pid, .[0-9] 39 + owner /var/lib/kea/kea-leases4.csv* rw, 40 + 41 + owner /var/log/kea/kea-dhcp4.log rw, 42 + owner /var/log/kea/kea-dhcp4.log.[0-9]* rw, 43 + owner /var/log/kea/kea-dhcp4.log.lock rwk, 44 + 45 + # Site-specific additions and overrides. See local/README for details. 46 + #include <local/usr.sbin.kea-dhcp4> 47 + 48 + /usr/local/bin/kea-mqtt-hook.sh ux, 49 + /usr/bin/mosquitto_pub ixr, 50 + /usr/lib/** rm, 51 + }
+76 -77
crates/firecracker-vm/src/coredns.rs
··· 1 1 use std::{process, thread}; 2 2 3 3 use anyhow::{Context, Error}; 4 - use firecracker_state::repo; 5 4 6 5 use crate::{command::run_command, mqttc, types::VmOptions}; 7 6 ··· 10 9 11 10 pub fn setup_coredns(config: &VmOptions) -> Result<(), Error> { 12 11 let api_socket = config.api_socket.clone(); 12 + if !coredns_is_installed()? { 13 + println!("[✗] CoreDNS is not installed. Please install it first to /usr/sbin."); 14 + process::exit(1); 15 + } 16 + 17 + if !etcd_is_installed()? { 18 + println!("[+] Installing etcd..."); 19 + run_command( 20 + "apt-get", 21 + &["install", "-y", "etcd-server", "etcd-client"], 22 + true, 23 + )?; 24 + } 25 + 26 + run_command( 27 + "sh", 28 + &[ 29 + "-c", 30 + &format!( 31 + "echo '{}' > {}", 32 + include_str!("./coredns/Corefile"), 33 + COREDNS_CONFIG_PATH 34 + ), 35 + ], 36 + true, 37 + )?; 38 + 39 + run_command( 40 + "sh", 41 + &[ 42 + "-c", 43 + &format!( 44 + "echo '{}' > /etc/systemd/system/coredns.service", 45 + COREDNS_SERVICE_TEMPLATE 46 + ), 47 + ], 48 + true, 49 + )?; 50 + restart_coredns()?; 51 + 52 + let etcd_args = match config.etcd.clone() { 53 + Some(etcd) => { 54 + let mut args = vec![]; 55 + if let Some(endpoints) = &etcd.endpoints { 56 + args.push(format!("--endpoints={}", endpoints.join(","))); 57 + } 58 + if let Some(user) = &etcd.user { 59 + args.push(format!("--user={}", user)); 60 + } 61 + if let Some(password) = &etcd.password { 62 + args.push(format!("--password={}", password)); 63 + } 64 + if let Some(cacert) = &etcd.cacert { 65 + args.push(format!("--cacert={}", cacert)); 66 + } 67 + if let Some(cert) = &etcd.cert { 68 + args.push(format!("--cert={}", cert)); 69 + } 70 + args 71 + } 72 + None => vec![], 73 + }; 74 + 13 75 thread::spawn(move || { 14 76 let runtime = tokio::runtime::Runtime::new().unwrap(); 15 77 match runtime.block_on(async { 16 - println!("[+] Checking if CoreDNS is installed..."); 17 - if !coredns_is_installed()? { 18 - // TODO: install it automatically 19 - println!("[✗] CoreDNS is not installed. Please install it first to /usr/sbin."); 20 - process::exit(1); 21 - } 22 - 23 78 let message = mqttc::wait_for_mqtt_message("REQUEST").await?; 24 79 let ip_addr = message 25 80 .split_whitespace() ··· 36 91 std::fs::write(format!("/tmp/firecracker-{}.ip", name), ip_addr) 37 92 .with_context(|| "Failed to write IP address to file")?; 38 93 39 - let pool = firecracker_state::create_connection_pool().await?; 40 - let vms = repo::virtual_machine::all(&pool).await?; 41 - let mut hosts = vms 42 - .into_iter() 43 - .filter(|vm| vm.ip_address.is_some() && vm.name != name) 44 - .map(|vm| format!("{} {}.firecracker", vm.ip_address.unwrap(), vm.name)) 45 - .collect::<Vec<String>>(); 46 - 47 - hosts.extend(vec![format!("{} {}.firecracker", ip_addr, name)]); 48 - 49 - let hosts = hosts.join("\n "); 50 - 51 - let coredns_config: &str = &format!( 52 - r#" 53 - firecracker:53 {{ 54 - hosts {{ 55 - 172.16.0.1 br.firecracker 56 - {} 57 - fallthrough 58 - }} 59 - 60 - loadbalance 61 - }} 62 - 63 - ts.net:53 {{ 64 - # Forward non-internal queries (e.g., to Tailscale DNS) 65 - forward . 100.100.100.100 66 - # Log and errors for debugging 67 - log 68 - errors 69 - health 70 - }} 71 - 72 - .:53 {{ 73 - # Forward non-internal queries (e.g., to Google DNS) 74 - forward . 8.8.8.8 8.8.4.4 1.1.1.1 1.0.0.1 {{ 75 - max_fails 3 76 - expire 10s 77 - health_check 5s 78 - policy round_robin 79 - except ts.net 80 - }} 81 - # Log and errors for debugging 82 - log 83 - errors 84 - health 85 - }} 86 - "#, 87 - hosts 94 + println!( 95 + "[+] Assigning DNS entry: {}.firecracker -> {}", 96 + name, ip_addr 88 97 ); 89 98 90 - run_command( 91 - "sh", 92 - &[ 93 - "-c", 94 - &format!("echo '{}' > {}", coredns_config, COREDNS_CONFIG_PATH), 95 - ], 96 - true, 97 - )?; 99 + let etcd_key = format!("/skydns/firecracker/{}", name); 100 + let etcd_value = format!("{{\"host\":\"{}\"}}", ip_addr); 101 + let mut args = vec!["put", &etcd_key, &etcd_value]; 102 + args.extend(etcd_args.iter().map(String::as_str)); 98 103 99 - run_command( 100 - "sh", 101 - &[ 102 - "-c", 103 - &format!( 104 - "echo '{}' > /etc/systemd/system/coredns.service", 105 - COREDNS_SERVICE_TEMPLATE 106 - ), 107 - ], 108 - true, 109 - )?; 110 - restart_coredns()?; 104 + run_command("etcdctl", &args, false)?; 111 105 112 106 Ok::<(), Error>(()) 113 107 }) { ··· 136 130 let output = run_command("which", &["coredns"], false)?; 137 131 Ok(output.status.success()) 138 132 } 133 + 134 + pub fn etcd_is_installed() -> Result<bool, Error> { 135 + let output = run_command("ls", &["/usr/bin/etcd"], false)?; 136 + Ok(output.status.success()) 137 + }
+35
crates/firecracker-vm/src/coredns/Corefile
··· 1 + firecracker:53 { 2 + etcd firecracker { 3 + path /skydns 4 + endpoint http://127.0.0.1:2379 5 + fallthrough 6 + } 7 + 8 + cache 30 9 + loadbalance 10 + log 11 + } 12 + 13 + ts.net:53 { 14 + # Forward non-internal queries (e.g., to Tailscale DNS) 15 + forward . 100.100.100.100 16 + # Log and errors for debugging 17 + log 18 + errors 19 + health 20 + } 21 + 22 + .:53 { 23 + # Forward non-internal queries (e.g., to Google DNS) 24 + forward . 8.8.8.8 8.8.4.4 1.1.1.1 1.0.0.1 { 25 + max_fails 3 26 + expire 10s 27 + health_check 5s 28 + policy round_robin 29 + except ts.net 30 + } 31 + # Log and errors for debugging 32 + log 33 + errors 34 + health 35 + }
+128
crates/firecracker-vm/src/dhcpd.rs
··· 1 + use anyhow::Error; 2 + 3 + use crate::{command::run_command, constants::BRIDGE_DEV, types::VmOptions}; 4 + 5 + pub const DHCPD_CONFIG_PATH: &str = "/etc/kea/kea-dhcp4.conf"; 6 + 7 + pub fn setup_kea_dhcp(_config: &VmOptions) -> Result<(), Error> { 8 + println!("[+] Checking if isc-kea-dhcp-server is installed..."); 9 + if is_kea_dhcp_installed().is_err() { 10 + run_command( 11 + "apt-get", 12 + &[ 13 + "install", 14 + "-y", 15 + "kea-dhcp4-server", 16 + "kea-admin", 17 + "kea-common", 18 + "etcd-client", 19 + "etcd-server", 20 + ], 21 + true, 22 + )?; 23 + } 24 + 25 + const KEA_MQTT_HOOK_SH: &str = include_str!("./scripts/kea-mqtt-hook.sh"); 26 + println!("[+] Installing kea-mqtt-hook.sh script..."); 27 + std::fs::write("/tmp/kea-mqtt-hook.sh", KEA_MQTT_HOOK_SH)?; 28 + run_command("cp", &["/tmp/kea-mqtt-hook.sh", "/usr/local/bin"], true)?; 29 + run_command("chmod", &["a+x", "/usr/local/bin/kea-mqtt-hook.sh"], true)?; 30 + run_command("rm", &["/tmp/kea-mqtt-hook.sh"], false)?; 31 + 32 + println!("[+] Setting up AppArmor for kea-dhcp4..."); 33 + std::fs::write( 34 + "/tmp/usr.sbin.kea-dhcp4", 35 + include_str!("./apparmor/usr.sbin.kea-dhcp4"), 36 + )?; 37 + run_command("cp", &["/tmp/usr.sbin.kea-dhcp4", "/etc/apparmor.d/"], true)?; 38 + run_command("rm", &["/tmp/usr.sbin.kea-dhcp4"], false)?; 39 + run_command( 40 + "apparmor_parser", 41 + &["-r", "/etc/apparmor.d/usr.sbin.kea-dhcp4"], 42 + true, 43 + )?; 44 + 45 + let kea_dhcp_config: &str = &format!( 46 + r#" 47 + {{ 48 + "Dhcp4": {{ 49 + "valid-lifetime": 4000, 50 + "renew-timer": 1000, 51 + "rebind-timer": 2000, 52 + "interfaces-config": {{ 53 + "interfaces": [ "{}" ] 54 + }}, 55 + "lease-database": {{ 56 + "type": "memfile", 57 + "lfc-interval": 3600 58 + }}, 59 + "subnet4": [ 60 + {{ 61 + "subnet": "172.16.0.0/24", 62 + "pools": [ {{ "pool": "172.16.0.2 - 172.16.0.150" }} ], 63 + "option-data": [ 64 + {{ "name": "routers", "data": "172.16.0.1" }}, 65 + {{ "name": "domain-name-servers", "data": "172.16.0.1" }} 66 + ] 67 + }} 68 + ], 69 + "hooks-libraries": [ 70 + {{ 71 + "library": "/usr/lib/x86_64-linux-gnu/kea/hooks/libdhcp_run_script.so", 72 + "parameters": {{ 73 + "name": "/usr/local/bin/kea-mqtt-hook.sh" 74 + }} 75 + }} 76 + ], 77 + "loggers": [ 78 + {{ 79 + "name": "kea-dhcp4", 80 + "severity": "DEBUG", 81 + "debuglevel": 99 82 + }} 83 + ] 84 + }} 85 + }} 86 + "#, 87 + BRIDGE_DEV 88 + ); 89 + 90 + run_command( 91 + "sh", 92 + &[ 93 + "-c", 94 + &format!("echo '{}' > {}", kea_dhcp_config, DHCPD_CONFIG_PATH), 95 + ], 96 + true, 97 + )?; 98 + 99 + restart_kea_dhcp()?; 100 + 101 + Ok(()) 102 + } 103 + 104 + pub fn restart_kea_dhcp() -> Result<(), Error> { 105 + println!("[+] Starting kea-dhcp4-server..."); 106 + 107 + let dummy_is_up = run_command("ip", &["link", "show", "dummy0"], false) 108 + .map(|output| output.status.success()) 109 + .unwrap_or(false); 110 + if !dummy_is_up { 111 + println!("[+] Creating dummy0 interface..."); 112 + run_command("ip", &["link", "add", "dummy0", "type", "dummy"], true)?; 113 + run_command("ip", &["link", "set", "dummy0", "up"], true)?; 114 + run_command("ip", &["link", "set", "dummy0", "master", BRIDGE_DEV], true)?; 115 + } 116 + 117 + run_command("systemctl", &["enable", "kea-dhcp4-server"], true)?; 118 + run_command("systemctl", &["daemon-reload"], true)?; 119 + run_command("systemctl", &["stop", "kea-dhcp4-server"], true)?; 120 + run_command("systemctl", &["start", "kea-dhcp4-server"], true)?; 121 + println!("[✓] kea-dhcp4-server started successfully."); 122 + Ok(()) 123 + } 124 + 125 + pub fn is_kea_dhcp_installed() -> Result<bool, Error> { 126 + let output = run_command("which", &["kea-dhcp4"], false)?; 127 + Ok(output.status.success()) 128 + }
+6 -17
crates/firecracker-vm/src/firecracker.rs
··· 1 1 use crate::types::VmOptions; 2 2 use anyhow::Result; 3 - use firecracker_prepare::Distro; 4 3 use serde_json::json; 5 4 use std::thread::sleep; 6 5 use std::time::Duration; 7 6 8 7 use crate::command::run_command; 9 - 10 - const NIXOS_BOOT_ARGS: &str = "init=/nix/store/w1yqjd8sswh8zj9sz2v76dpw3llzkg5k-nixos-system-nixos-firecracker-25.05.802216.55d1f923c480/init root=/dev/vda ro console=ttyS0 reboot=k panic=1 ip=dhcp"; 11 8 12 9 pub fn configure( 13 10 logfile: &str, ··· 15 12 rootfs: &str, 16 13 arch: &str, 17 14 options: &VmOptions, 18 - distro: Distro, 19 15 ) -> Result<()> { 20 16 configure_logger(logfile, options)?; 21 - setup_boot_source(kernel, arch, distro == Distro::NixOS, &options)?; 17 + setup_boot_source(kernel, arch, &options)?; 22 18 setup_rootfs(rootfs, options)?; 23 19 setup_network_interface(options)?; 24 20 setup_vcpu_and_memory(options.vcpu, options.memory, &options.api_socket)?; ··· 58 54 Ok(()) 59 55 } 60 56 61 - fn setup_boot_source( 62 - kernel: &str, 63 - arch: &str, 64 - is_nixos: bool, 65 - options: &VmOptions, 66 - ) -> Result<String> { 57 + fn setup_boot_source(kernel: &str, arch: &str, options: &VmOptions) -> Result<String> { 67 58 println!("[+] Setting boot source..."); 68 - let mut boot_args = "console=ttyS0 reboot=k panic=1 pci=off ip=dhcp".to_string(); 59 + let mut boot_args = 60 + "console=ttyS0 reboot=k panic=1 pci=off ip=dhcp selinux=0 enforcing=0 init=/sbin/overlay-init overlay_root=ram" 61 + .to_string(); 69 62 if arch == "aarch64" { 70 63 boot_args = format!("keep_bootcon {}", boot_args); 71 - } 72 - 73 - if is_nixos { 74 - boot_args = NIXOS_BOOT_ARGS.into(); 75 64 } 76 65 77 66 if let Some(args) = &options.bootargs { ··· 109 98 "drive_id": "rootfs", 110 99 "path_on_host": rootfs, 111 100 "is_root_device": true, 112 - "is_read_only": false 101 + "is_read_only": true 113 102 }); 114 103 run_command( 115 104 "curl",
+21 -28
crates/firecracker-vm/src/lib.rs
··· 10 10 mod config; 11 11 pub mod constants; 12 12 mod coredns; 13 + mod dhcpd; 13 14 mod firecracker; 14 15 mod guest; 15 16 pub mod mac; 16 17 mod mosquitto; 17 18 mod mqttc; 18 19 mod network; 19 - mod nextdhcp; 20 20 pub mod types; 21 21 22 22 pub async fn setup(options: &VmOptions, pid: u32, vm_id: Option<String>) -> Result<()> { ··· 58 58 .display() 59 59 .to_string(); 60 60 61 - let ext4_file = match distro { 62 - Distro::Debian => format!("{}/debian*.ext4", app_dir), 63 - Distro::Alpine => format!("{}/alpine*.ext4", app_dir), 64 - Distro::NixOS => format!("{}/nixos*.ext4", app_dir), 65 - Distro::Ubuntu => format!("{}/ubuntu*.ext4", app_dir), 61 + // readonly rootfs (squashfs) 62 + let img_file = match distro { 63 + Distro::Debian => format!("{}/debian-rootfs.img", app_dir), 64 + Distro::Alpine => format!("{}/alpine-rootfs.img", app_dir), 65 + Distro::NixOS => format!("{}/nixos-rootfs.img", app_dir), 66 + Distro::Ubuntu => format!("{}/ubuntu-rootfs.img", app_dir), 67 + Distro::Fedora => format!("{}/fedora-rootfs.img", app_dir), 68 + Distro::Gentoo => format!("{}/gentoo-rootfs.img", app_dir), 69 + Distro::Slackware => format!("{}/slackware-rootfs.img", app_dir), 70 + Distro::Opensuse => format!("{}/opensuse-rootfs.img", app_dir), 71 + Distro::OpensuseTumbleweed => format!("{}/opensuse-tumbleweed-rootfs.img", app_dir), 72 + Distro::Almalinux => format!("{}/almalinux-rootfs.img", app_dir), 73 + Distro::RockyLinux => format!("{}/rockylinux-rootfs.img", app_dir), 74 + Distro::Archlinux => format!("{}/archlinux-rootfs.img", app_dir), 66 75 }; 67 76 68 - let rootfs = glob::glob(&ext4_file) 69 - .with_context(|| "Failed to glob rootfs files")? 70 - .last() 71 - .ok_or_else(|| anyhow!("No rootfs file found"))? 72 - .with_context(|| "Failed to get rootfs path")?; 73 - let rootfs = fs::canonicalize(&rootfs) 74 - .with_context(|| { 75 - format!( 76 - "Failed to resolve absolute path for rootfs: {}", 77 - rootfs.display() 78 - ) 79 - })? 77 + let rootfs = fs::canonicalize(&img_file) 78 + .with_context(|| format!("Failed to resolve absolute path for rootfs: {}", img_file))? 80 79 .display() 81 80 .to_string(); 82 81 ··· 99 98 network::setup_network(options)?; 100 99 mosquitto::setup_mosquitto(options)?; 101 100 coredns::setup_coredns(options)?; 102 - nextdhcp::setup_nextdhcp(options)?; 101 + dhcpd::setup_kea_dhcp(options)?; 103 102 104 - firecracker::configure(&logfile, &kernel, &rootfs, &arch, &options, distro)?; 103 + firecracker::configure(&logfile, &kernel, &rootfs, &arch, &options)?; 105 104 106 105 if distro != Distro::NixOS { 107 106 let guest_ip = format!("{}.firecracker", name); 108 107 guest::configure_guest_network(&key_name, &guest_ip)?; 109 108 } 110 109 let pool = firecracker_state::create_connection_pool().await?; 111 - let distro = match distro { 112 - Distro::Debian => "debian".into(), 113 - Distro::Alpine => "alpine".into(), 114 - Distro::NixOS => "nixos".into(), 115 - Distro::Ubuntu => "ubuntu".into(), 116 - }; 117 110 118 111 let ip_file = format!("/tmp/firecracker-{}.ip", name); 119 112 ··· 165 158 mac_address: options.mac_address.clone(), 166 159 name: name.clone(), 167 160 pid: Some(pid), 168 - distro, 161 + distro: distro.to_string(), 169 162 ip_address: Some(ip_addr.clone()), 170 163 status: "RUNNING".into(), 171 164 project_dir, ··· 189 182 mac_address: options.mac_address.clone(), 190 183 name: name.clone(), 191 184 pid: Some(pid), 192 - distro, 185 + distro: distro.to_string(), 193 186 ip_address: Some(ip_addr.clone()), 194 187 status: "RUNNING".into(), 195 188 project_dir,
-79
crates/firecracker-vm/src/nextdhcp.rs
··· 1 - use std::process; 2 - 3 - use anyhow::Error; 4 - 5 - use crate::{command::run_command, types::VmOptions}; 6 - 7 - pub const NEXTDHCP_CONFIG_PATH: &str = "/etc/nextdhcp/Dhcpfile"; 8 - pub const NEXTDHCP_SERVICE_TEMPLATE: &str = include_str!("./systemd/nextdhcp.service"); 9 - 10 - pub fn setup_nextdhcp(_config: &VmOptions) -> Result<(), Error> { 11 - println!("[+] Checking if NextDHCP is installed..."); 12 - if !nextdhcp_is_installed()? { 13 - // TODO: install it automatically 14 - println!("[✗] NextDHCP is not installed. Please install it first to /usr/sbin."); 15 - process::exit(1); 16 - } 17 - 18 - let nextdhcp_config: &str = r#" 19 - 172.16.0.1/24 { 20 - lease 5m 21 - 22 - range 172.16.0.2 172.16.0.150 23 - 24 - mqtt { 25 - name default 26 - broker tcp://localhost:1883 27 - 28 - topic /dhcp/hwaddr/{hwaddr} 29 - payload "{msgtype} {hwaddr} {requestedip} {state}" 30 - qos 1 31 - } 32 - 33 - option { 34 - router 172.16.0.1 35 - nameserver 172.16.0.1 36 - } 37 - } 38 - "#; 39 - 40 - run_command( 41 - "sh", 42 - &[ 43 - "-c", 44 - &format!("echo '{}' > {}", nextdhcp_config, NEXTDHCP_CONFIG_PATH), 45 - ], 46 - true, 47 - )?; 48 - 49 - run_command( 50 - "sh", 51 - &[ 52 - "-c", 53 - &format!( 54 - "echo '{}' > /etc/systemd/system/nextdhcp.service", 55 - NEXTDHCP_SERVICE_TEMPLATE 56 - ), 57 - ], 58 - true, 59 - )?; 60 - restart_nextdhcp()?; 61 - 62 - Ok(()) 63 - } 64 - 65 - pub fn restart_nextdhcp() -> Result<(), Error> { 66 - println!("[+] Starting nextdhcp..."); 67 - 68 - run_command("systemctl", &["enable", "nextdhcp"], true)?; 69 - run_command("systemctl", &["daemon-reload"], true)?; 70 - run_command("systemctl", &["stop", "nextdhcp"], true)?; 71 - run_command("systemctl", &["start", "nextdhcp"], true)?; 72 - println!("[✓] Nextdhcp started successfully."); 73 - Ok(()) 74 - } 75 - 76 - pub fn nextdhcp_is_installed() -> Result<bool, Error> { 77 - let output = run_command("which", &["nextdhcp"], false)?; 78 - Ok(output.status.success()) 79 - }
+27
crates/firecracker-vm/src/scripts/kea-mqtt-hook.sh
··· 1 + #!/usr/bin/env bash 2 + set -euo pipefail 3 + 4 + export BROKER="localhost" 5 + export PORT="1883" 6 + export TOPIC="/dhcp/hwaddr" 7 + 8 + echo "Starting Kea DHCP hook script..." 9 + echo $1 $QUERY4_HWADDR 10 + 11 + if [ "$1" = "leases4_committed" ]; then 12 + if [ -z "${QUERY4_HWADDR:-}" ]; then 13 + echo "QUERY4_HWADDR is not set. Exiting." 14 + exit 0 15 + fi 16 + 17 + echo ">> Lease committed event detected." >> /tmp/kea-lease-hook.log 18 + env >> /tmp/kea-lease-hook.log 19 + # Log to a file 20 + echo "$(date): New lease assigned - IP: $LEASES4_AT0_ADDRESS, MAC: $QUERY4_HWADDR" >> /tmp/kea-lease-hook.log 21 + 22 + echo "New lease assigned - IP: $LEASES4_AT0_ADDRESS, MAC: $QUERY4_HWADDR" 23 + 24 + mosquitto_pub -h "$BROKER" -p "$PORT" -t "$TOPIC" -m "REQUEST $QUERY4_HWADDR $LEASES4_AT0_ADDRESS binding" 25 + fi 26 + 27 + exit 0
-18
crates/firecracker-vm/src/systemd/nextdhcp.service
··· 1 - [Unit] 2 - Description=NextDHCP Service 3 - After=network.target systemd-tmpfiles-setup.service 4 - 5 - [Service] 6 - ExecStart=/usr/sbin/nextdhcp -conf /etc/nextdhcp/Dhcpfile 7 - Restart=on-failure 8 - RestartSec=5 9 - LimitNOFILE=65535 10 - LimitNPROC=512 11 - ProtectSystem=strict 12 - ReadWritePaths=/etc/nextdhcp 13 - NoNewPrivileges=true 14 - AmbientCapabilities=CAP_NET_ADMIN CAP_NET_RAW CAP_NET_BIND_SERVICE 15 - WorkingDirectory=/etc/nextdhcp 16 - 17 - [Install] 18 - WantedBy=multi-user.target
+35 -1
crates/firecracker-vm/src/types.rs
··· 1 - use fire_config::FireConfig; 1 + use fire_config::{EtcdConfig, FireConfig}; 2 2 use firecracker_prepare::Distro; 3 3 4 4 use crate::constants::{BRIDGE_DEV, FC_MAC, FIRECRACKER_SOCKET}; ··· 9 9 pub alpine: Option<bool>, 10 10 pub ubuntu: Option<bool>, 11 11 pub nixos: Option<bool>, 12 + pub fedora: Option<bool>, 13 + pub gentoo: Option<bool>, 14 + pub slackware: Option<bool>, 15 + pub opensuse: Option<bool>, 16 + pub opensuse_tumbleweed: Option<bool>, 17 + pub almalinux: Option<bool>, 18 + pub rockylinux: Option<bool>, 19 + pub archlinux: Option<bool>, 12 20 pub vcpu: u16, 13 21 pub memory: u16, 14 22 pub vmlinux: Option<String>, ··· 18 26 pub tap: String, 19 27 pub api_socket: String, 20 28 pub mac_address: String, 29 + pub etcd: Option<EtcdConfig>, 21 30 } 22 31 23 32 impl From<FireConfig> for VmOptions { ··· 28 37 alpine: Some(config.distro == Distro::Alpine), 29 38 ubuntu: Some(config.distro == Distro::Ubuntu), 30 39 nixos: Some(config.distro == Distro::NixOS), 40 + fedora: Some(config.distro == Distro::Fedora), 41 + gentoo: Some(config.distro == Distro::Gentoo), 42 + slackware: Some(config.distro == Distro::Slackware), 43 + opensuse: Some(config.distro == Distro::Opensuse), 44 + opensuse_tumbleweed: Some(config.distro == Distro::OpensuseTumbleweed), 45 + almalinux: Some(config.distro == Distro::Almalinux), 46 + rockylinux: Some(config.distro == Distro::RockyLinux), 47 + archlinux: Some(config.distro == Distro::Archlinux), 31 48 vcpu: vm.vcpu.unwrap_or(num_cpus::get() as u16), 32 49 memory: vm.memory.unwrap_or(512), 33 50 vmlinux: vm.vmlinux, ··· 37 54 tap: vm.tap.unwrap_or("".into()), 38 55 api_socket: vm.api_socket.unwrap_or(FIRECRACKER_SOCKET.into()), 39 56 mac_address: vm.mac.unwrap_or(FC_MAC.into()), 57 + etcd: config.etcd.clone(), 40 58 } 41 59 } 42 60 } ··· 49 67 Distro::Alpine 50 68 } else if self.nixos.unwrap_or(false) { 51 69 Distro::NixOS 70 + } else if self.fedora.unwrap_or(false) { 71 + Distro::Fedora 72 + } else if self.gentoo.unwrap_or(false) { 73 + Distro::Gentoo 74 + } else if self.slackware.unwrap_or(false) { 75 + Distro::Slackware 76 + } else if self.opensuse.unwrap_or(false) { 77 + Distro::Opensuse 78 + } else if self.opensuse_tumbleweed.unwrap_or(false) { 79 + Distro::OpensuseTumbleweed 80 + } else if self.almalinux.unwrap_or(false) { 81 + Distro::Almalinux 82 + } else if self.rockylinux.unwrap_or(false) { 83 + Distro::RockyLinux 84 + } else if self.archlinux.unwrap_or(false) { 85 + Distro::Archlinux 52 86 } else if self.ubuntu.unwrap_or(true) { 53 87 Distro::Ubuntu 54 88 } else {