diff --git a/Cargo.lock b/Cargo.lock index 8c17b9a..4c96b7b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -924,6 +924,7 @@ dependencies = [ "actix-web", "anyhow", "env_logger", + "fire-config", "firecracker-prepare", "firecracker-process", "firecracker-state", diff --git a/crates/fire-config/src/lib.rs b/crates/fire-config/src/lib.rs index 8ccd997..d985747 100644 --- a/crates/fire-config/src/lib.rs +++ b/crates/fire-config/src/lib.rs @@ -14,6 +14,11 @@ pub struct EtcdConfig { pub cert: Option, } +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct TailscaleOptions { + pub auth_key: Option, +} + #[derive(Debug, Serialize, Deserialize)] pub struct Vm { pub vcpu: Option, @@ -26,6 +31,7 @@ pub struct Vm { pub api_socket: Option, pub mac: Option, pub ssh_keys: Option>, + pub tailscale: Option, } #[derive(Debug, Serialize, Deserialize)] @@ -50,6 +56,7 @@ impl Default for FireConfig { api_socket: None, mac: None, ssh_keys: None, + tailscale: None, }, etcd: None, } diff --git a/crates/fire-server/Cargo.toml b/crates/fire-server/Cargo.toml index 6bebc3f..11de9bb 100644 --- a/crates/fire-server/Cargo.toml +++ b/crates/fire-server/Cargo.toml @@ -22,6 +22,7 @@ firecracker-state = { path = "../firecracker-state" } firecracker-vm = { path = "../firecracker-vm" } firecracker-prepare = { path = "../firecracker-prepare" } firecracker-process = { path = "../firecracker-process" } +fire-config = { path = "../fire-config" } serde_json = "1.0.145" sqlx = { version = "0.8.6", features = [ "runtime-tokio", diff --git a/crates/fire-server/src/api/microvm.rs b/crates/fire-server/src/api/microvm.rs index 3523605..764da3e 100644 --- a/crates/fire-server/src/api/microvm.rs +++ b/crates/fire-server/src/api/microvm.rs @@ -46,6 +46,7 @@ async fn create_microvm( boot_args: None, ssh_keys: None, start: None, + tailscale_auth_key: None, }, false => serde_json::from_slice::(&body)?, }; @@ -141,11 +142,23 @@ async fn list_microvms( #[post("/{id}/start")] async fn start_microvm( id: web::Path, + mut payload: web::Payload, pool: web::Data>>, ) -> Result { let id = id.into_inner(); + let body = read_payload!(payload); + let tailscale_auth_key = match body.is_empty() { + true => None, + false => { + let params: serde_json::Value = serde_json::from_slice(&body)?; + params + .get("tailscale_auth_key") + .and_then(|v| v.as_str()) + .map(|s| s.to_string()) + } + }; let pool = pool.get_ref().clone(); - let vm = services::microvm::start_microvm(pool, &id) + let vm = services::microvm::start_microvm(pool, &id, tailscale_auth_key) .await .map_err(actix_web::error::ErrorInternalServerError)?; Ok(HttpResponse::Ok().json(vm)) diff --git a/crates/fire-server/src/services/microvm.rs b/crates/fire-server/src/services/microvm.rs index 79e8d3b..2b5261a 100644 --- a/crates/fire-server/src/services/microvm.rs +++ b/crates/fire-server/src/services/microvm.rs @@ -2,6 +2,7 @@ use std::{sync::Arc, thread}; use crate::types::microvm::CreateMicroVM; use anyhow::Error; +use fire_config::TailscaleOptions; use firecracker_state::{entity::virtual_machine::VirtualMachine, repo}; use firecracker_vm::{constants::BRIDGE_DEV, types::VmOptions}; use owo_colors::OwoColorize; @@ -41,7 +42,11 @@ pub async fn delete_microvm( Ok(Some(vm)) } -pub async fn start_microvm(pool: Arc>, id: &str) -> Result { +pub async fn start_microvm( + pool: Arc>, + id: &str, + tailscale_auth_key: Option, +) -> Result { let vm = repo::virtual_machine::find(&pool, id).await?; if vm.is_none() { println!("[!] No virtual machine found with the name: {}", id); @@ -79,6 +84,9 @@ pub async fn start_microvm(pool: Arc>, id: &str) -> Result, } +#[derive(Serialize, Deserialize, Clone, ToSchema)] +pub struct StartMicroVM { + pub tailscale_auth_key: Option, +} + #[derive(Serialize, Deserialize, Clone, ToSchema)] pub struct CreateMicroVM { pub name: Option, @@ -27,6 +33,7 @@ pub struct CreateMicroVM { pub boot_args: Option, pub ssh_keys: Option>, pub start: Option, + pub tailscale_auth_key: Option, } impl Into for CreateMicroVM { @@ -110,6 +117,9 @@ impl Into for CreateMicroVM { rootfs: self.rootfs, bootargs: self.boot_args, mac_address: generate_unique_mac(), + tailscale: self.tailscale_auth_key.map(|key| TailscaleOptions { + auth_key: Some(key), + }), ..Default::default() } } diff --git a/crates/firecracker-prepare/src/lib.rs b/crates/firecracker-prepare/src/lib.rs index 1f96762..5ae1ea0 100644 --- a/crates/firecracker-prepare/src/lib.rs +++ b/crates/firecracker-prepare/src/lib.rs @@ -184,7 +184,7 @@ impl RootfsPreparer for DebianPreparer { &debootstrap_dir, "sh", "-c", - "apt-get install -y systemd-resolved", + "apt-get install -y systemd-resolved ca-certificates curl", ], true, )?; @@ -448,6 +448,21 @@ impl RootfsPreparer for UbuntuPreparer { let squashfs_root_dir = format!("{}/squashfs_root", app_dir); rootfs::extract_squashfs(&ubuntu_file, &squashfs_root_dir)?; + run_command( + "cp", + &["-r", "/etc/ssl", &format!("{}/etc/", squashfs_root_dir)], + true, + )?; + run_command( + "cp", + &[ + "-r", + "/etc/ca-certificates", + &format!("{}/etc/", squashfs_root_dir), + ], + true, + )?; + run_command( "chroot", &[ diff --git a/crates/firecracker-up/src/cmd/start.rs b/crates/firecracker-up/src/cmd/start.rs index b16937a..76d35c0 100644 --- a/crates/firecracker-up/src/cmd/start.rs +++ b/crates/firecracker-up/src/cmd/start.rs @@ -1,12 +1,13 @@ use std::process; use anyhow::Error; +use fire_config::TailscaleOptions; use firecracker_state::repo; use firecracker_vm::types::VmOptions; use crate::cmd::up::up; -pub async fn start(name: &str) -> Result<(), Error> { +pub async fn start(name: &str, tailscale_auth_key: Option) -> Result<(), Error> { let etcd = match fire_config::read_config() { Ok(config) => config.etcd, Err(_) => None, @@ -46,6 +47,9 @@ pub async fn start(name: &str) -> Result<(), Error> { ssh_keys: vm .ssh_keys .map(|keys| keys.split(',').map(|s| s.to_string()).collect()), + tailscale: tailscale_auth_key.map(|key| TailscaleOptions { + auth_key: Some(key), + }), }) .await?; diff --git a/crates/firecracker-up/src/main.rs b/crates/firecracker-up/src/main.rs index 8050e10..99a5768 100644 --- a/crates/firecracker-up/src/main.rs +++ b/crates/firecracker-up/src/main.rs @@ -44,6 +44,12 @@ fn cli() -> Command { .subcommand( Command::new("start") .arg(arg!( "Name of the Firecracker MicroVM to start").required(true)) + .arg( + Arg::new("tailscale-auth-key") + .long("tailscale-auth-key") + .value_name("TAILSCALE_AUTH_KEY") + .help("Tailscale auth key to connect the VM to a Tailscale network"), + ) .about("Start Firecracker MicroVM"), ) .subcommand( @@ -54,6 +60,12 @@ fn cli() -> Command { .subcommand( Command::new("restart") .arg(arg!( "Name of the Firecracker MicroVM to restart").required(true)) + .arg( + Arg::new("tailscale-auth-key") + .long("tailscale-auth-key") + .value_name("TAILSCALE_AUTH_KEY") + .help("Tailscale auth key to connect the VM to a Tailscale network"), + ) .about("Restart Firecracker MicroVM"), ) .subcommand( @@ -104,6 +116,12 @@ fn cli() -> Command { .value_name("SSH_KEYS") .help("Comma-separated list of SSH public keys to add to the VM"), ) + .arg( + Arg::new("tailscale-auth-key") + .long("tailscale-auth-key") + .value_name("TAILSCALE_AUTH_KEY") + .help("Tailscale auth key to connect the VM to a Tailscale network"), + ) .about("Start a new Firecracker MicroVM"), ) .subcommand(Command::new("down").about("Stop Firecracker MicroVM")) @@ -195,6 +213,12 @@ fn cli() -> Command { .value_name("SSH_KEYS") .help("Comma-separated list of SSH public keys to add to the VM"), ) + .arg( + Arg::new("tailscale-auth-key") + .long("tailscale-auth-key") + .value_name("TAILSCALE_AUTH_KEY") + .help("Tailscale auth key to connect the VM to a Tailscale network"), + ) } #[tokio::main] @@ -218,12 +242,14 @@ async fn main() -> Result<()> { } Some(("start", args)) => { let name = args.get_one::("name").cloned().unwrap(); - start(&name).await?; + let tailscale_auth_key = args.get_one::("tailscale-auth-key").cloned(); + start(&name, tailscale_auth_key).await?; } Some(("restart", args)) => { let name = args.get_one::("name").cloned().unwrap(); + let tailscale_auth_key = args.get_one::("tailscale-auth-key").cloned(); stop(&name).await?; - start(&name).await?; + start(&name, tailscale_auth_key).await?; } Some(("up", args)) => { let vcpu = matches @@ -250,6 +276,7 @@ async fn main() -> Result<()> { let ssh_keys = args .get_one::("ssh-keys") .map(|s| s.split(',').map(|s| s.trim().to_string()).collect()); + let tailscale_auth_key = args.get_one::("tailscale-auth-key").cloned(); let options = VmOptions { debian: args.get_one::("debian").copied(), alpine: args.get_one::("alpine").copied(), @@ -274,6 +301,9 @@ async fn main() -> Result<()> { mac_address, etcd: None, ssh_keys, + tailscale: tailscale_auth_key.map(|key| fire_config::TailscaleOptions { + auth_key: Some(key), + }), }; up(options).await? } @@ -369,6 +399,7 @@ async fn main() -> Result<()> { let ssh_keys = matches .get_one::("ssh-keys") .map(|s| s.split(',').map(|s| s.trim().to_string()).collect()); + let tailscale_auth_key = matches.get_one::("tailscale-auth-key").cloned(); let options = VmOptions { debian: Some(debian), @@ -394,6 +425,9 @@ async fn main() -> Result<()> { mac_address, etcd: None, ssh_keys, + tailscale: tailscale_auth_key.map(|key| fire_config::TailscaleOptions { + auth_key: Some(key), + }), }; up(options).await? } diff --git a/crates/firecracker-vm/src/guest.rs b/crates/firecracker-vm/src/guest.rs index 023fcd7..0df4cb1 100644 --- a/crates/firecracker-vm/src/guest.rs +++ b/crates/firecracker-vm/src/guest.rs @@ -1,7 +1,7 @@ use crate::{command::run_command, constants::BRIDGE_IP}; use anyhow::Result; -pub fn configure_guest_network(key_name: &str, guest_ip: &str) -> Result<()> { +pub fn configure_guest_network(key_path: &str, guest_ip: &str) -> Result<()> { println!("[+] Configuring network in guest..."); const MAX_RETRIES: u32 = 20; let mut retries = 0; @@ -10,7 +10,7 @@ pub fn configure_guest_network(key_name: &str, guest_ip: &str) -> Result<()> { "ssh", &[ "-i", - key_name, + key_path, "-o", "StrictHostKeyChecking=no", &format!("root@{}", guest_ip), diff --git a/crates/firecracker-vm/src/lib.rs b/crates/firecracker-vm/src/lib.rs index 8a646fd..2572ad9 100644 --- a/crates/firecracker-vm/src/lib.rs +++ b/crates/firecracker-vm/src/lib.rs @@ -17,6 +17,7 @@ pub mod mac; mod mosquitto; mod mqttc; mod network; +mod tailscale; pub mod types; pub async fn setup( @@ -106,6 +107,9 @@ pub async fn setup( let guest_ip = format!("{}.firecracker", name); guest::configure_guest_network(&key_name, &guest_ip)?; } + + tailscale::setup_tailscale(&name, options)?; + let pool = firecracker_state::create_connection_pool().await?; let ip_file = format!("/tmp/firecracker-{}.ip", name); diff --git a/crates/firecracker-vm/src/tailscale.rs b/crates/firecracker-vm/src/tailscale.rs new file mode 100644 index 0000000..e857325 --- /dev/null +++ b/crates/firecracker-vm/src/tailscale.rs @@ -0,0 +1,89 @@ +use std::fs; + +use anyhow::anyhow; +use anyhow::Context; +use anyhow::Error; +use firecracker_prepare::command::run_command_with_stdout_inherit; + +use crate::types::VmOptions; + +pub fn setup_tailscale(name: &str, config: &VmOptions) -> Result<(), Error> { + if let Some(tailscale) = &config.tailscale { + if let Some(auth_key) = &tailscale.auth_key { + let len = auth_key.len(); + let display_key = if len > 16 { + format!("{}****{}", &auth_key[..16], &auth_key[len - 4..]) + } else { + return Err(anyhow!("Tailscale auth key is too short")); + }; + println!("[+] Setting up Tailscale with auth key: {}", display_key); + let key_path = + get_private_key_path().with_context(|| "Failed to get SSH private key path")?; + + let guest_ip = format!("{}.firecracker", name); + run_ssh_command(&key_path, &guest_ip, "rm -f /etc/security/namespace.init")?; + run_ssh_command( + &key_path, + &guest_ip, + "type tailscaled || curl -fsSL https://tailscale.com/install.sh | sh", + )?; + run_ssh_command( + &key_path, + &guest_ip, + &format!( + "tailscale up --auth-key {} --hostname {}", + auth_key, + guest_ip.split('.').next().unwrap() + ), + )?; + run_ssh_command( + &key_path, + &guest_ip, + "systemctl enable --now tailscaled || true", + )?; + run_ssh_command(&key_path, &guest_ip, "systemctl status tailscaled || true")?; + run_ssh_command(&key_path, &guest_ip, "tailscale status || true")?; + println!("[+] Tailscale setup completed."); + return Ok(()); + } + } + + println!("[+] Tailscale auth key not provided, skipping Tailscale setup."); + Ok(()) +} + +fn run_ssh_command(key_path: &str, guest_ip: &str, command: &str) -> Result<(), Error> { + run_command_with_stdout_inherit( + "ssh", + &[ + "-i", + key_path, + "-o", + "StrictHostKeyChecking=no", + &format!("root@{}", guest_ip), + command, + ], + false, + )?; + Ok(()) +} + +fn get_private_key_path() -> Result { + let home_dir = dirs::home_dir().ok_or_else(|| anyhow!("Failed to get home directory"))?; + let app_dir = format!("{}/.fireup", home_dir.display()); + let key_name = glob::glob(format!("{}/id_rsa", app_dir).as_str()) + .with_context(|| "Failed to glob ssh key files")? + .last() + .ok_or_else(|| anyhow!("No SSH key file found"))? + .with_context(|| "Failed to get SSH key path")?; + let key_name = fs::canonicalize(&key_name) + .with_context(|| { + format!( + "Failed to resolve absolute path for SSH key: {}", + key_name.display() + ) + })? + .display() + .to_string(); + Ok(key_name) +} diff --git a/crates/firecracker-vm/src/types.rs b/crates/firecracker-vm/src/types.rs index 020d45f..577c3b4 100644 --- a/crates/firecracker-vm/src/types.rs +++ b/crates/firecracker-vm/src/types.rs @@ -1,4 +1,4 @@ -use fire_config::{EtcdConfig, FireConfig}; +use fire_config::{EtcdConfig, FireConfig, TailscaleOptions}; use firecracker_prepare::Distro; use crate::constants::{BRIDGE_DEV, FC_MAC, FIRECRACKER_SOCKET}; @@ -28,6 +28,7 @@ pub struct VmOptions { pub mac_address: String, pub etcd: Option, pub ssh_keys: Option>, + pub tailscale: Option, } impl From for VmOptions { @@ -57,6 +58,7 @@ impl From for VmOptions { mac_address: vm.mac.unwrap_or(FC_MAC.into()), etcd: config.etcd.clone(), ssh_keys: vm.ssh_keys.clone(), + tailscale: vm.tailscale.clone(), } } }