diff --git a/.gitignore b/.gitignore index 32779c7..fa4ee51 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ target/ -fire.toml \ No newline at end of file +fire.toml +.vscode/ \ No newline at end of file diff --git a/README.md b/README.md index fd596f0..b710ccd 100644 --- a/README.md +++ b/README.md @@ -17,7 +17,7 @@ ## Prerequisites - [CoreDNS](https://coredns.io/) (for DNS resolution) -- [NextDHCP](https://github.com/nextdhcp/nextdhcp) (for DHCP services) +- [Kea DHCP](https://kea.readthedocs.io/en/latest/) (for DHCP services) - [Mosquitto](https://mosquitto.org/) (MQTT Server) ## Installation diff --git a/crates/firecracker-prepare/src/lib.rs b/crates/firecracker-prepare/src/lib.rs index de7d021..020fea0 100644 --- a/crates/firecracker-prepare/src/lib.rs +++ b/crates/firecracker-prepare/src/lib.rs @@ -79,7 +79,7 @@ impl RootfsPreparer for DebianPreparer { arch.bright_green() ); let kernel_file = downloader::download_kernel(arch)?; - let debootstrap_dir = format!("{}/debootstrap", app_dir); + let debootstrap_dir = format!("{}/debian-rootfs", app_dir); let arch = match arch { "x86_64" => "amd64", diff --git a/crates/firecracker-vm/src/apparmor/usr.sbin.kea-dhcp4 b/crates/firecracker-vm/src/apparmor/usr.sbin.kea-dhcp4 new file mode 100644 index 0000000..dde4db0 --- /dev/null +++ b/crates/firecracker-vm/src/apparmor/usr.sbin.kea-dhcp4 @@ -0,0 +1,51 @@ +abi , + +include + +profile kea-dhcp4 /usr/sbin/kea-dhcp4 { + include + include + + # for MySQL access, localhost + include + include + + capability net_bind_service, + capability net_raw, + + network inet dgram, + network inet stream, + network netlink raw, + network packet raw, + + /etc/gss/mech.d/ r, + /etc/gss/mech.d/* r, + + /etc/kea/ r, + /etc/kea/** r, + /usr/sbin/kea-dhcp4 mr, + /usr/sbin/kea-lfc Px, + + owner /run/kea/kea-dhcp4.kea-dhcp4.pid rw, + owner /run/lock/kea/logger_lockfile rwk, + + # Control sockets + # Before LP: #1863100, these were in /tmp. For compatibility, let's keep both + # locations + owner /{tmp,run/kea}/kea4-ctrl-socket w, + owner /{tmp,run/kea}/kea4-ctrl-socket.lock rwk, + + # this includes .completed, .output, .pid, .[0-9] + owner /var/lib/kea/kea-leases4.csv* rw, + + owner /var/log/kea/kea-dhcp4.log rw, + owner /var/log/kea/kea-dhcp4.log.[0-9]* rw, + owner /var/log/kea/kea-dhcp4.log.lock rwk, + + # Site-specific additions and overrides. See local/README for details. + #include + + /usr/local/bin/kea-mqtt-hook.sh ux, + /usr/bin/mosquitto_pub ixr, + /usr/lib/** rm, +} diff --git a/crates/firecracker-vm/src/coredns.rs b/crates/firecracker-vm/src/coredns.rs index aacbf9a..17a6321 100644 --- a/crates/firecracker-vm/src/coredns.rs +++ b/crates/firecracker-vm/src/coredns.rs @@ -1,7 +1,6 @@ use std::{process, thread}; use anyhow::{Context, Error}; -use firecracker_state::repo; use crate::{command::run_command, mqttc, types::VmOptions}; @@ -10,16 +9,49 @@ pub const COREDNS_SERVICE_TEMPLATE: &str = include_str!("./systemd/coredns.servi pub fn setup_coredns(config: &VmOptions) -> Result<(), Error> { let api_socket = config.api_socket.clone(); + if !coredns_is_installed()? { + println!("[✗] CoreDNS is not installed. Please install it first to /usr/sbin."); + process::exit(1); + } + + if !etcd_is_installed()? { + println!("[+] Installing etcd..."); + run_command( + "apt-get", + &["install", "-y", "etcd-server", "etcd-client"], + true, + )?; + } + + run_command( + "sh", + &[ + "-c", + &format!( + "echo '{}' > {}", + include_str!("./coredns/Corefile"), + COREDNS_CONFIG_PATH + ), + ], + true, + )?; + + run_command( + "sh", + &[ + "-c", + &format!( + "echo '{}' > /etc/systemd/system/coredns.service", + COREDNS_SERVICE_TEMPLATE + ), + ], + true, + )?; + restart_coredns()?; + thread::spawn(move || { let runtime = tokio::runtime::Runtime::new().unwrap(); match runtime.block_on(async { - println!("[+] Checking if CoreDNS is installed..."); - if !coredns_is_installed()? { - // TODO: install it automatically - println!("[✗] CoreDNS is not installed. Please install it first to /usr/sbin."); - process::exit(1); - } - let message = mqttc::wait_for_mqtt_message("REQUEST").await?; let ip_addr = message .split_whitespace() @@ -36,79 +68,19 @@ pub fn setup_coredns(config: &VmOptions) -> Result<(), Error> { std::fs::write(format!("/tmp/firecracker-{}.ip", name), ip_addr) .with_context(|| "Failed to write IP address to file")?; - let pool = firecracker_state::create_connection_pool().await?; - let vms = repo::virtual_machine::all(&pool).await?; - let mut hosts = vms - .into_iter() - .filter(|vm| vm.ip_address.is_some() && vm.name != name) - .map(|vm| format!("{} {}.firecracker", vm.ip_address.unwrap(), vm.name)) - .collect::>(); - - hosts.extend(vec![format!("{} {}.firecracker", ip_addr, name)]); - - let hosts = hosts.join("\n "); - - let coredns_config: &str = &format!( - r#" - firecracker:53 {{ - hosts {{ - 172.16.0.1 br.firecracker - {} - fallthrough - }} - - loadbalance - }} - - ts.net:53 {{ - # Forward non-internal queries (e.g., to Tailscale DNS) - forward . 100.100.100.100 - # Log and errors for debugging - log - errors - health - }} - - .:53 {{ - # Forward non-internal queries (e.g., to Google DNS) - forward . 8.8.8.8 8.8.4.4 1.1.1.1 1.0.0.1 {{ - max_fails 3 - expire 10s - health_check 5s - policy round_robin - except ts.net - }} - # Log and errors for debugging - log - errors - health - }} - "#, - hosts + println!( + "[+] Assigning DNS entry: {}.firecracker -> {}", + name, ip_addr ); + let etcd_key = format!("/skydns/firecracker/{}", name); + let etcd_value = format!("{{\"host\":\"{}\"}}", ip_addr); run_command( - "sh", - &[ - "-c", - &format!("echo '{}' > {}", coredns_config, COREDNS_CONFIG_PATH), - ], + "etcdctl", + &["put", &etcd_key, &etcd_value], true, )?; - run_command( - "sh", - &[ - "-c", - &format!( - "echo '{}' > /etc/systemd/system/coredns.service", - COREDNS_SERVICE_TEMPLATE - ), - ], - true, - )?; - restart_coredns()?; - Ok::<(), Error>(()) }) { Ok(_) => {} @@ -136,3 +108,8 @@ pub fn coredns_is_installed() -> Result { let output = run_command("which", &["coredns"], false)?; Ok(output.status.success()) } + +pub fn etcd_is_installed() -> Result { + let output = run_command("ls", &["/usr/bin/etcd"], false)?; + Ok(output.status.success()) +} diff --git a/crates/firecracker-vm/src/coredns/Corefile b/crates/firecracker-vm/src/coredns/Corefile new file mode 100644 index 0000000..6519046 --- /dev/null +++ b/crates/firecracker-vm/src/coredns/Corefile @@ -0,0 +1,35 @@ +firecracker:53 { + etcd firecracker { + path /skydns + endpoint http://127.0.0.1:2379 + fallthrough + } + + cache 30 + loadbalance + log +} + +ts.net:53 { + # Forward non-internal queries (e.g., to Tailscale DNS) + forward . 100.100.100.100 + # Log and errors for debugging + log + errors + health +} + +.:53 { + # Forward non-internal queries (e.g., to Google DNS) + forward . 8.8.8.8 8.8.4.4 1.1.1.1 1.0.0.1 { + max_fails 3 + expire 10s + health_check 5s + policy round_robin + except ts.net + } + # Log and errors for debugging + log + errors + health +} diff --git a/crates/firecracker-vm/src/dhcpd.rs b/crates/firecracker-vm/src/dhcpd.rs new file mode 100644 index 0000000..28b8992 --- /dev/null +++ b/crates/firecracker-vm/src/dhcpd.rs @@ -0,0 +1,128 @@ +use anyhow::Error; + +use crate::{command::run_command, constants::BRIDGE_DEV, types::VmOptions}; + +pub const DHCPD_CONFIG_PATH: &str = "/etc/kea/kea-dhcp4.conf"; + +pub fn setup_kea_dhcp(_config: &VmOptions) -> Result<(), Error> { + println!("[+] Checking if isc-kea-dhcp-server is installed..."); + if is_kea_dhcp_installed().is_err() { + run_command( + "apt-get", + &[ + "install", + "-y", + "kea-dhcp4-server", + "kea-admin", + "kea-common", + "etcd-client", + "etcd-server", + ], + true, + )?; + } + + const KEA_MQTT_HOOK_SH: &str = include_str!("./scripts/kea-mqtt-hook.sh"); + println!("[+] Installing kea-mqtt-hook.sh script..."); + std::fs::write("/tmp/kea-mqtt-hook.sh", KEA_MQTT_HOOK_SH)?; + run_command("cp", &["/tmp/kea-mqtt-hook.sh", "/usr/local/bin"], true)?; + run_command("chmod", &["a+x", "/usr/local/bin/kea-mqtt-hook.sh"], true)?; + run_command("rm", &["/tmp/kea-mqtt-hook.sh"], false)?; + + println!("[+] Setting up AppArmor for kea-dhcp4..."); + std::fs::write( + "/tmp/usr.sbin.kea-dhcp4", + include_str!("./apparmor/usr.sbin.kea-dhcp4"), + )?; + run_command("cp", &["/tmp/usr.sbin.kea-dhcp4", "/etc/apparmor.d/"], true)?; + run_command("rm", &["/tmp/usr.sbin.kea-dhcp4"], false)?; + run_command( + "apparmor_parser", + &["-r", "/etc/apparmor.d/usr.sbin.kea-dhcp4"], + true, + )?; + + let kea_dhcp_config: &str = &format!( + r#" +{{ + "Dhcp4": {{ + "valid-lifetime": 4000, + "renew-timer": 1000, + "rebind-timer": 2000, + "interfaces-config": {{ + "interfaces": [ "{}" ] + }}, + "lease-database": {{ + "type": "memfile", + "lfc-interval": 3600 + }}, + "subnet4": [ + {{ + "subnet": "172.16.0.0/24", + "pools": [ {{ "pool": "172.16.0.2 - 172.16.0.150" }} ], + "option-data": [ + {{ "name": "routers", "data": "172.16.0.1" }}, + {{ "name": "domain-name-servers", "data": "172.16.0.1" }} + ] + }} + ], + "hooks-libraries": [ + {{ + "library": "/usr/lib/x86_64-linux-gnu/kea/hooks/libdhcp_run_script.so", + "parameters": {{ + "name": "/usr/local/bin/kea-mqtt-hook.sh" + }} + }} + ], + "loggers": [ + {{ + "name": "kea-dhcp4", + "severity": "DEBUG", + "debuglevel": 99 + }} + ] + }} +}} +"#, + BRIDGE_DEV + ); + + run_command( + "sh", + &[ + "-c", + &format!("echo '{}' > {}", kea_dhcp_config, DHCPD_CONFIG_PATH), + ], + true, + )?; + + restart_kea_dhcp()?; + + Ok(()) +} + +pub fn restart_kea_dhcp() -> Result<(), Error> { + println!("[+] Starting kea-dhcp4-server..."); + + let dummy_is_up = run_command("ip", &["link", "show", "dummy0"], false) + .map(|output| output.status.success()) + .unwrap_or(false); + if !dummy_is_up { + println!("[+] Creating dummy0 interface..."); + run_command("ip", &["link", "add", "dummy0", "type", "dummy"], true)?; + run_command("ip", &["link", "set", "dummy0", "up"], true)?; + run_command("ip", &["link", "set", "dummy0", "master", BRIDGE_DEV], true)?; + } + + run_command("systemctl", &["enable", "kea-dhcp4-server"], true)?; + run_command("systemctl", &["daemon-reload"], true)?; + run_command("systemctl", &["stop", "kea-dhcp4-server"], true)?; + run_command("systemctl", &["start", "kea-dhcp4-server"], true)?; + println!("[✓] kea-dhcp4-server started successfully."); + Ok(()) +} + +pub fn is_kea_dhcp_installed() -> Result { + let output = run_command("which", &["kea-dhcp4"], false)?; + Ok(output.status.success()) +} diff --git a/crates/firecracker-vm/src/lib.rs b/crates/firecracker-vm/src/lib.rs index 8cddb9a..88b6d2f 100644 --- a/crates/firecracker-vm/src/lib.rs +++ b/crates/firecracker-vm/src/lib.rs @@ -10,13 +10,13 @@ mod command; mod config; pub mod constants; mod coredns; +mod dhcpd; mod firecracker; mod guest; pub mod mac; mod mosquitto; mod mqttc; mod network; -mod nextdhcp; pub mod types; pub async fn setup(options: &VmOptions, pid: u32, vm_id: Option) -> Result<()> { @@ -98,7 +98,7 @@ pub async fn setup(options: &VmOptions, pid: u32, vm_id: Option) -> Resu network::setup_network(options)?; mosquitto::setup_mosquitto(options)?; coredns::setup_coredns(options)?; - nextdhcp::setup_nextdhcp(options)?; + dhcpd::setup_kea_dhcp(options)?; firecracker::configure(&logfile, &kernel, &rootfs, &arch, &options)?; diff --git a/crates/firecracker-vm/src/nextdhcp.rs b/crates/firecracker-vm/src/nextdhcp.rs deleted file mode 100644 index 6a89128..0000000 --- a/crates/firecracker-vm/src/nextdhcp.rs +++ /dev/null @@ -1,79 +0,0 @@ -use std::process; - -use anyhow::Error; - -use crate::{command::run_command, types::VmOptions}; - -pub const NEXTDHCP_CONFIG_PATH: &str = "/etc/nextdhcp/Dhcpfile"; -pub const NEXTDHCP_SERVICE_TEMPLATE: &str = include_str!("./systemd/nextdhcp.service"); - -pub fn setup_nextdhcp(_config: &VmOptions) -> Result<(), Error> { - println!("[+] Checking if NextDHCP is installed..."); - if !nextdhcp_is_installed()? { - // TODO: install it automatically - println!("[✗] NextDHCP is not installed. Please install it first to /usr/sbin."); - process::exit(1); - } - - let nextdhcp_config: &str = r#" -172.16.0.1/24 { - lease 30m - - range 172.16.0.2 172.16.0.150 - - mqtt { - name default - broker tcp://localhost:1883 - - topic /dhcp/hwaddr/{hwaddr} - payload "{msgtype} {hwaddr} {requestedip} {state}" - qos 1 - } - - option { - router 172.16.0.1 - nameserver 172.16.0.1 - } - } -"#; - - run_command( - "sh", - &[ - "-c", - &format!("echo '{}' > {}", nextdhcp_config, NEXTDHCP_CONFIG_PATH), - ], - true, - )?; - - run_command( - "sh", - &[ - "-c", - &format!( - "echo '{}' > /etc/systemd/system/nextdhcp.service", - NEXTDHCP_SERVICE_TEMPLATE - ), - ], - true, - )?; - restart_nextdhcp()?; - - Ok(()) -} - -pub fn restart_nextdhcp() -> Result<(), Error> { - println!("[+] Starting nextdhcp..."); - - run_command("systemctl", &["enable", "nextdhcp"], true)?; - run_command("systemctl", &["daemon-reload"], true)?; - run_command("systemctl", &["stop", "nextdhcp"], true)?; - run_command("systemctl", &["start", "nextdhcp"], true)?; - println!("[✓] Nextdhcp started successfully."); - Ok(()) -} - -pub fn nextdhcp_is_installed() -> Result { - let output = run_command("which", &["nextdhcp"], false)?; - Ok(output.status.success()) -} diff --git a/crates/firecracker-vm/src/scripts/kea-mqtt-hook.sh b/crates/firecracker-vm/src/scripts/kea-mqtt-hook.sh new file mode 100755 index 0000000..e575843 --- /dev/null +++ b/crates/firecracker-vm/src/scripts/kea-mqtt-hook.sh @@ -0,0 +1,27 @@ +#!/usr/bin/env bash +set -euo pipefail + +export BROKER="localhost" +export PORT="1883" +export TOPIC="/dhcp/hwaddr" + +echo "Starting Kea DHCP hook script..." +echo $1 $QUERY4_HWADDR + +if [ "$1" = "leases4_committed" ]; then + if [ -z "${QUERY4_HWADDR:-}" ]; then + echo "QUERY4_HWADDR is not set. Exiting." + exit 0 + fi + + echo ">> Lease committed event detected." >> /tmp/kea-lease-hook.log + env >> /tmp/kea-lease-hook.log + # Log to a file + echo "$(date): New lease assigned - IP: $LEASES4_AT0_ADDRESS, MAC: $QUERY4_HWADDR" >> /tmp/kea-lease-hook.log + + echo "New lease assigned - IP: $LEASES4_AT0_ADDRESS, MAC: $QUERY4_HWADDR" + + mosquitto_pub -h "$BROKER" -p "$PORT" -t "$TOPIC" -m "REQUEST $QUERY4_HWADDR $LEASES4_AT0_ADDRESS binding" +fi + +exit 0 diff --git a/crates/firecracker-vm/src/systemd/nextdhcp.service b/crates/firecracker-vm/src/systemd/nextdhcp.service deleted file mode 100644 index 09d7de2..0000000 --- a/crates/firecracker-vm/src/systemd/nextdhcp.service +++ /dev/null @@ -1,18 +0,0 @@ -[Unit] -Description=NextDHCP Service -After=network.target systemd-tmpfiles-setup.service - -[Service] -ExecStart=/usr/sbin/nextdhcp -conf /etc/nextdhcp/Dhcpfile -Restart=on-failure -RestartSec=5 -LimitNOFILE=65535 -LimitNPROC=512 -ProtectSystem=strict -ReadWritePaths=/etc/nextdhcp -NoNewPrivileges=true -AmbientCapabilities=CAP_NET_ADMIN CAP_NET_RAW CAP_NET_BIND_SERVICE -WorkingDirectory=/etc/nextdhcp - -[Install] -WantedBy=multi-user.target