Volt VMM (Neutron Stardust): source-available under AGPSL v5.0
KVM-based microVMM for the Volt platform: - Sub-second VM boot times - Minimal memory footprint - Landlock LSM + seccomp security - Virtio device support - Custom kernel management Copyright (c) Armored Gates LLC. All rights reserved. Licensed under AGPSL v5.0
This commit is contained in:
158
rootfs/volt-init/src/main.rs
Normal file
158
rootfs/volt-init/src/main.rs
Normal file
@@ -0,0 +1,158 @@
|
||||
// volt-init: Minimal PID 1 for Volt VMs
|
||||
// No BusyBox, no Alpine, no external binaries. Pure Rust.
|
||||
|
||||
mod mount;
|
||||
mod net;
|
||||
mod shell;
|
||||
mod sys;
|
||||
|
||||
use std::ffi::CString;
|
||||
use std::io::Write;
|
||||
|
||||
/// Write a message to /dev/kmsg (kernel log buffer)
|
||||
/// This works even when stdout isn't connected.
|
||||
#[allow(dead_code)]
|
||||
fn klog(msg: &str) {
|
||||
let path = CString::new("/dev/kmsg").unwrap();
|
||||
let fd = unsafe { libc::open(path.as_ptr(), libc::O_WRONLY) };
|
||||
if fd >= 0 {
|
||||
let formatted = format!("<6>volt-init: {}\n", msg);
|
||||
let bytes = formatted.as_bytes();
|
||||
unsafe {
|
||||
libc::write(fd, bytes.as_ptr() as *const libc::c_void, bytes.len());
|
||||
libc::close(fd);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Direct write to a file descriptor (bypass Rust's I/O layer)
|
||||
#[allow(dead_code)]
|
||||
fn write_fd(fd: i32, msg: &str) {
|
||||
let bytes = msg.as_bytes();
|
||||
unsafe {
|
||||
libc::write(fd, bytes.as_ptr() as *const libc::c_void, bytes.len());
|
||||
}
|
||||
}
|
||||
|
||||
fn main() {
|
||||
// === PHASE 1: Mount filesystems (no I/O possible yet) ===
|
||||
mount::mount_essentials();
|
||||
|
||||
// === PHASE 2: Set up console I/O ===
|
||||
sys::setup_console();
|
||||
|
||||
// === PHASE 3: Signal handlers ===
|
||||
sys::install_signal_handlers();
|
||||
|
||||
// === PHASE 4: System configuration ===
|
||||
let cmdline = sys::read_kernel_cmdline();
|
||||
let hostname = sys::parse_cmdline_value(&cmdline, "hostname")
|
||||
.unwrap_or_else(|| "volt-vmm".to_string());
|
||||
sys::set_hostname(&hostname);
|
||||
|
||||
// === PHASE 5: Boot banner ===
|
||||
print_banner(&hostname);
|
||||
|
||||
// === PHASE 6: Networking ===
|
||||
let ip_config = sys::parse_cmdline_value(&cmdline, "ip");
|
||||
net::configure_network(ip_config.as_deref());
|
||||
|
||||
// === PHASE 7: Shell ===
|
||||
println!("\n[volt-init] Starting shell on console...");
|
||||
println!("Type 'help' for available commands.\n");
|
||||
shell::run_shell();
|
||||
|
||||
// === PHASE 8: Shutdown ===
|
||||
println!("[volt-init] Shutting down...");
|
||||
shutdown();
|
||||
}
|
||||
|
||||
fn print_banner(hostname: &str) {
|
||||
println!();
|
||||
println!("╔══════════════════════════════════════╗");
|
||||
println!("║ === VOLT VM READY === ║");
|
||||
println!("╚══════════════════════════════════════╝");
|
||||
println!();
|
||||
println!("[volt-init] Hostname: {}", hostname);
|
||||
|
||||
if let Ok(version) = std::fs::read_to_string("/proc/version") {
|
||||
let short = version
|
||||
.split_whitespace()
|
||||
.take(3)
|
||||
.collect::<Vec<_>>()
|
||||
.join(" ");
|
||||
println!("[volt-init] Kernel: {}", short);
|
||||
}
|
||||
|
||||
if let Ok(uptime) = std::fs::read_to_string("/proc/uptime") {
|
||||
if let Some(secs) = uptime.split_whitespace().next() {
|
||||
if let Ok(s) = secs.parse::<f64>() {
|
||||
println!("[volt-init] Uptime: {:.3}s", s);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let Ok(meminfo) = std::fs::read_to_string("/proc/meminfo") {
|
||||
let mut total = 0u64;
|
||||
let mut free = 0u64;
|
||||
let mut available = 0u64;
|
||||
for line in meminfo.lines() {
|
||||
if let Some(val) = extract_meminfo_kb(line, "MemTotal:") {
|
||||
total = val;
|
||||
} else if let Some(val) = extract_meminfo_kb(line, "MemFree:") {
|
||||
free = val;
|
||||
} else if let Some(val) = extract_meminfo_kb(line, "MemAvailable:") {
|
||||
available = val;
|
||||
}
|
||||
}
|
||||
println!(
|
||||
"[volt-init] Memory: {}MB total, {}MB available, {}MB free",
|
||||
total / 1024,
|
||||
available / 1024,
|
||||
free / 1024
|
||||
);
|
||||
}
|
||||
|
||||
if let Ok(cpuinfo) = std::fs::read_to_string("/proc/cpuinfo") {
|
||||
let mut model = None;
|
||||
let mut count = 0u32;
|
||||
for line in cpuinfo.lines() {
|
||||
if line.starts_with("processor") {
|
||||
count += 1;
|
||||
}
|
||||
if model.is_none() && line.starts_with("model name") {
|
||||
if let Some(val) = line.split(':').nth(1) {
|
||||
model = Some(val.trim().to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
if let Some(m) = model {
|
||||
println!("[volt-init] CPU: {} x {}", count, m);
|
||||
} else {
|
||||
println!("[volt-init] CPU: {} processor(s)", count);
|
||||
}
|
||||
}
|
||||
|
||||
let _ = std::io::stdout().flush();
|
||||
}
|
||||
|
||||
fn extract_meminfo_kb(line: &str, key: &str) -> Option<u64> {
|
||||
if line.starts_with(key) {
|
||||
line[key.len()..]
|
||||
.trim()
|
||||
.trim_end_matches("kB")
|
||||
.trim()
|
||||
.parse()
|
||||
.ok()
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn shutdown() {
|
||||
unsafe { libc::sync() };
|
||||
mount::umount_all();
|
||||
unsafe {
|
||||
libc::reboot(libc::RB_AUTOBOOT);
|
||||
}
|
||||
}
|
||||
93
rootfs/volt-init/src/mount.rs
Normal file
93
rootfs/volt-init/src/mount.rs
Normal file
@@ -0,0 +1,93 @@
|
||||
// Filesystem mounting for PID 1
|
||||
// ALL functions are panic-free — we cannot panic as PID 1.
|
||||
|
||||
use std::ffi::CString;
|
||||
use std::path::Path;
|
||||
|
||||
pub fn mount_essentials() {
|
||||
// Mount /proc first (needed for everything else)
|
||||
do_mount("proc", "/proc", "proc", libc::MS_NOSUID | libc::MS_NODEV | libc::MS_NOEXEC, None);
|
||||
|
||||
// Mount /sys
|
||||
do_mount("sysfs", "/sys", "sysfs", libc::MS_NOSUID | libc::MS_NODEV | libc::MS_NOEXEC, None);
|
||||
|
||||
// Mount /dev (devtmpfs)
|
||||
if !do_mount("devtmpfs", "/dev", "devtmpfs", libc::MS_NOSUID, Some("mode=0755")) {
|
||||
// Fallback: mount tmpfs on /dev and create device nodes manually
|
||||
do_mount("tmpfs", "/dev", "tmpfs", libc::MS_NOSUID, Some("mode=0755,size=4m"));
|
||||
create_dev_nodes();
|
||||
}
|
||||
|
||||
// Mount /tmp
|
||||
do_mount("tmpfs", "/tmp", "tmpfs", libc::MS_NOSUID | libc::MS_NODEV, Some("size=16m"));
|
||||
}
|
||||
|
||||
fn do_mount(source: &str, target: &str, fstype: &str, flags: libc::c_ulong, data: Option<&str>) -> bool {
|
||||
// Ensure mount target directory exists
|
||||
if !Path::new(target).exists() {
|
||||
let _ = std::fs::create_dir_all(target);
|
||||
}
|
||||
|
||||
let c_source = match CString::new(source) {
|
||||
Ok(s) => s,
|
||||
Err(_) => return false,
|
||||
};
|
||||
let c_target = match CString::new(target) {
|
||||
Ok(s) => s,
|
||||
Err(_) => return false,
|
||||
};
|
||||
let c_fstype = match CString::new(fstype) {
|
||||
Ok(s) => s,
|
||||
Err(_) => return false,
|
||||
};
|
||||
let c_data = data.map(|d| CString::new(d).ok()).flatten();
|
||||
|
||||
let data_ptr = c_data
|
||||
.as_ref()
|
||||
.map(|d| d.as_ptr() as *const libc::c_void)
|
||||
.unwrap_or(std::ptr::null());
|
||||
|
||||
let ret = unsafe {
|
||||
libc::mount(
|
||||
c_source.as_ptr(),
|
||||
c_target.as_ptr(),
|
||||
c_fstype.as_ptr(),
|
||||
flags,
|
||||
data_ptr,
|
||||
)
|
||||
};
|
||||
|
||||
ret == 0
|
||||
}
|
||||
|
||||
fn create_dev_nodes() {
|
||||
let devices: &[(&str, libc::mode_t, u32, u32)] = &[
|
||||
("/dev/null", libc::S_IFCHR | 0o666, 1, 3),
|
||||
("/dev/zero", libc::S_IFCHR | 0o666, 1, 5),
|
||||
("/dev/random", libc::S_IFCHR | 0o444, 1, 8),
|
||||
("/dev/urandom", libc::S_IFCHR | 0o444, 1, 9),
|
||||
("/dev/tty", libc::S_IFCHR | 0o666, 5, 0),
|
||||
("/dev/console", libc::S_IFCHR | 0o600, 5, 1),
|
||||
("/dev/ttyS0", libc::S_IFCHR | 0o660, 4, 64),
|
||||
];
|
||||
|
||||
for &(path, mode, major, minor) in devices {
|
||||
if let Ok(c_path) = CString::new(path) {
|
||||
let dev = libc::makedev(major, minor);
|
||||
unsafe {
|
||||
libc::mknod(c_path.as_ptr(), mode, dev);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn umount_all() {
|
||||
let targets = ["/tmp", "/dev", "/sys", "/proc"];
|
||||
for target in &targets {
|
||||
if let Ok(c_target) = CString::new(*target) {
|
||||
unsafe {
|
||||
libc::umount2(c_target.as_ptr(), libc::MNT_DETACH);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
336
rootfs/volt-init/src/net.rs
Normal file
336
rootfs/volt-init/src/net.rs
Normal file
@@ -0,0 +1,336 @@
|
||||
// Network configuration using raw socket ioctls
|
||||
// No `ip` command needed — we do it all ourselves.
|
||||
|
||||
use std::ffi::CString;
|
||||
use std::mem;
|
||||
use std::net::Ipv4Addr;
|
||||
|
||||
// ioctl request codes (libc::Ioctl = c_int on musl, c_ulong on glibc)
|
||||
const SIOCSIFADDR: libc::Ioctl = 0x8916;
|
||||
const SIOCSIFNETMASK: libc::Ioctl = 0x891C;
|
||||
const SIOCSIFFLAGS: libc::Ioctl = 0x8914;
|
||||
const SIOCGIFFLAGS: libc::Ioctl = 0x8913;
|
||||
const SIOCADDRT: libc::Ioctl = 0x890B;
|
||||
const SIOCSIFMTU: libc::Ioctl = 0x8922;
|
||||
|
||||
// Interface flags
|
||||
const IFF_UP: libc::c_short = libc::IFF_UP as libc::c_short;
|
||||
const IFF_RUNNING: libc::c_short = libc::IFF_RUNNING as libc::c_short;
|
||||
|
||||
#[repr(C)]
|
||||
struct Ifreq {
|
||||
ifr_name: [libc::c_char; libc::IFNAMSIZ],
|
||||
ifr_ifru: IfreqData,
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
union IfreqData {
|
||||
ifr_addr: libc::sockaddr,
|
||||
ifr_flags: libc::c_short,
|
||||
ifr_mtu: libc::c_int,
|
||||
_pad: [u8; 24],
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
struct Rtentry {
|
||||
rt_pad1: libc::c_ulong,
|
||||
rt_dst: libc::sockaddr,
|
||||
rt_gateway: libc::sockaddr,
|
||||
rt_genmask: libc::sockaddr,
|
||||
rt_flags: libc::c_ushort,
|
||||
rt_pad2: libc::c_short,
|
||||
rt_pad3: libc::c_ulong,
|
||||
rt_pad4: *mut libc::c_void,
|
||||
rt_metric: libc::c_short,
|
||||
rt_dev: *mut libc::c_char,
|
||||
rt_mtu: libc::c_ulong,
|
||||
rt_window: libc::c_ulong,
|
||||
rt_irtt: libc::c_ushort,
|
||||
}
|
||||
|
||||
pub fn configure_network(ip_config: Option<&str>) {
|
||||
// Detect network interfaces
|
||||
let interfaces = detect_interfaces();
|
||||
if interfaces.is_empty() {
|
||||
println!("[volt-init] No network interfaces detected");
|
||||
return;
|
||||
}
|
||||
|
||||
println!("[volt-init] Network interfaces: {:?}", interfaces);
|
||||
|
||||
// Bring up loopback
|
||||
if interfaces.contains(&"lo".to_string()) {
|
||||
configure_interface("lo", "127.0.0.1", "255.0.0.0");
|
||||
}
|
||||
|
||||
// Find the primary interface (eth0, ens*, enp*)
|
||||
let primary = interfaces
|
||||
.iter()
|
||||
.find(|i| i.starts_with("eth") || i.starts_with("ens") || i.starts_with("enp"))
|
||||
.cloned();
|
||||
|
||||
if let Some(iface) = primary {
|
||||
// Parse IP configuration
|
||||
let (ip, mask, gateway) = parse_ip_config(ip_config);
|
||||
println!(
|
||||
"[volt-init] Configuring {} with IP {}/{}",
|
||||
iface, ip, mask
|
||||
);
|
||||
configure_interface(&iface, &ip, &mask);
|
||||
set_mtu(&iface, 1500);
|
||||
|
||||
// Set default route
|
||||
if let Some(gw) = gateway {
|
||||
println!("[volt-init] Setting default route via {}", gw);
|
||||
add_default_route(&gw, &iface);
|
||||
}
|
||||
} else {
|
||||
println!("[volt-init] No primary network interface found");
|
||||
}
|
||||
}
|
||||
|
||||
fn detect_interfaces() -> Vec<String> {
|
||||
let mut interfaces = Vec::new();
|
||||
if let Ok(entries) = std::fs::read_dir("/sys/class/net") {
|
||||
for entry in entries.flatten() {
|
||||
if let Some(name) = entry.file_name().to_str() {
|
||||
interfaces.push(name.to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
interfaces.sort();
|
||||
interfaces
|
||||
}
|
||||
|
||||
fn parse_ip_config(config: Option<&str>) -> (String, String, Option<String>) {
|
||||
// Kernel cmdline ip= format: ip=<client-ip>:<server-ip>:<gw-ip>:<netmask>:<hostname>:<device>:<autoconf>
|
||||
// Or simple: ip=172.16.0.2/24 or ip=172.16.0.2::172.16.0.1:255.255.255.0
|
||||
if let Some(cfg) = config {
|
||||
// Simple CIDR: ip=172.16.0.2/24
|
||||
if cfg.contains('/') {
|
||||
let parts: Vec<&str> = cfg.split('/').collect();
|
||||
let ip = parts[0].to_string();
|
||||
let prefix: u32 = parts.get(1).and_then(|p| p.parse().ok()).unwrap_or(24);
|
||||
let mask = prefix_to_mask(prefix);
|
||||
// Default gateway: assume .1
|
||||
let gw = default_gateway_for(&ip);
|
||||
return (ip, mask, Some(gw));
|
||||
}
|
||||
|
||||
// Kernel format: ip=client:server:gw:mask:hostname:device:autoconf
|
||||
let parts: Vec<&str> = cfg.split(':').collect();
|
||||
if parts.len() >= 4 {
|
||||
let ip = parts[0].to_string();
|
||||
let gw = if !parts[2].is_empty() {
|
||||
Some(parts[2].to_string())
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let mask = if !parts[3].is_empty() {
|
||||
parts[3].to_string()
|
||||
} else {
|
||||
"255.255.255.0".to_string()
|
||||
};
|
||||
return (ip, mask, gw);
|
||||
}
|
||||
|
||||
// Bare IP
|
||||
return (
|
||||
cfg.to_string(),
|
||||
"255.255.255.0".to_string(),
|
||||
Some(default_gateway_for(cfg)),
|
||||
);
|
||||
}
|
||||
|
||||
// Defaults
|
||||
(
|
||||
"172.16.0.2".to_string(),
|
||||
"255.255.255.0".to_string(),
|
||||
Some("172.16.0.1".to_string()),
|
||||
)
|
||||
}
|
||||
|
||||
fn prefix_to_mask(prefix: u32) -> String {
|
||||
let mask: u32 = if prefix == 0 {
|
||||
0
|
||||
} else {
|
||||
!0u32 << (32 - prefix)
|
||||
};
|
||||
format!(
|
||||
"{}.{}.{}.{}",
|
||||
(mask >> 24) & 0xFF,
|
||||
(mask >> 16) & 0xFF,
|
||||
(mask >> 8) & 0xFF,
|
||||
mask & 0xFF
|
||||
)
|
||||
}
|
||||
|
||||
fn default_gateway_for(ip: &str) -> String {
|
||||
if let Ok(addr) = ip.parse::<Ipv4Addr>() {
|
||||
let octets = addr.octets();
|
||||
format!("{}.{}.{}.1", octets[0], octets[1], octets[2])
|
||||
} else {
|
||||
"172.16.0.1".to_string()
|
||||
}
|
||||
}
|
||||
|
||||
fn make_sockaddr_in(ip: &str) -> libc::sockaddr {
|
||||
let addr: Ipv4Addr = ip.parse().unwrap_or(Ipv4Addr::new(0, 0, 0, 0));
|
||||
let mut sa: libc::sockaddr_in = unsafe { mem::zeroed() };
|
||||
sa.sin_family = libc::AF_INET as libc::sa_family_t;
|
||||
sa.sin_addr.s_addr = u32::from_ne_bytes(addr.octets());
|
||||
unsafe { mem::transmute(sa) }
|
||||
}
|
||||
|
||||
fn configure_interface(name: &str, ip: &str, mask: &str) {
|
||||
let sock = unsafe { libc::socket(libc::AF_INET, libc::SOCK_DGRAM, 0) };
|
||||
if sock < 0 {
|
||||
eprintln!(
|
||||
"[volt-init] Failed to create socket: {}",
|
||||
std::io::Error::last_os_error()
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
let mut ifr: Ifreq = unsafe { mem::zeroed() };
|
||||
let name_bytes = name.as_bytes();
|
||||
let copy_len = name_bytes.len().min(libc::IFNAMSIZ - 1);
|
||||
for i in 0..copy_len {
|
||||
ifr.ifr_name[i] = name_bytes[i] as libc::c_char;
|
||||
}
|
||||
|
||||
// Set IP address
|
||||
ifr.ifr_ifru.ifr_addr = make_sockaddr_in(ip);
|
||||
let ret = unsafe { libc::ioctl(sock, SIOCSIFADDR, &ifr) };
|
||||
if ret < 0 {
|
||||
eprintln!(
|
||||
"[volt-init] Failed to set IP on {}: {}",
|
||||
name,
|
||||
std::io::Error::last_os_error()
|
||||
);
|
||||
}
|
||||
|
||||
// Set netmask
|
||||
ifr.ifr_ifru.ifr_addr = make_sockaddr_in(mask);
|
||||
let ret = unsafe { libc::ioctl(sock, SIOCSIFNETMASK, &ifr) };
|
||||
if ret < 0 {
|
||||
eprintln!(
|
||||
"[volt-init] Failed to set netmask on {}: {}",
|
||||
name,
|
||||
std::io::Error::last_os_error()
|
||||
);
|
||||
}
|
||||
|
||||
// Get current flags
|
||||
let ret = unsafe { libc::ioctl(sock, SIOCGIFFLAGS, &ifr) };
|
||||
if ret < 0 {
|
||||
eprintln!(
|
||||
"[volt-init] Failed to get flags for {}: {}",
|
||||
name,
|
||||
std::io::Error::last_os_error()
|
||||
);
|
||||
}
|
||||
|
||||
// Bring interface up
|
||||
unsafe {
|
||||
ifr.ifr_ifru.ifr_flags |= IFF_UP | IFF_RUNNING;
|
||||
}
|
||||
let ret = unsafe { libc::ioctl(sock, SIOCSIFFLAGS, &ifr) };
|
||||
if ret < 0 {
|
||||
eprintln!(
|
||||
"[volt-init] Failed to bring up {}: {}",
|
||||
name,
|
||||
std::io::Error::last_os_error()
|
||||
);
|
||||
} else {
|
||||
println!("[volt-init] Interface {} is UP with IP {}", name, ip);
|
||||
}
|
||||
|
||||
unsafe { libc::close(sock) };
|
||||
}
|
||||
|
||||
fn set_mtu(name: &str, mtu: i32) {
|
||||
let sock = unsafe { libc::socket(libc::AF_INET, libc::SOCK_DGRAM, 0) };
|
||||
if sock < 0 {
|
||||
return;
|
||||
}
|
||||
|
||||
let mut ifr: Ifreq = unsafe { mem::zeroed() };
|
||||
let name_bytes = name.as_bytes();
|
||||
let copy_len = name_bytes.len().min(libc::IFNAMSIZ - 1);
|
||||
for i in 0..copy_len {
|
||||
ifr.ifr_name[i] = name_bytes[i] as libc::c_char;
|
||||
}
|
||||
|
||||
ifr.ifr_ifru.ifr_mtu = mtu;
|
||||
let ret = unsafe { libc::ioctl(sock, SIOCSIFMTU, &ifr) };
|
||||
if ret < 0 {
|
||||
eprintln!(
|
||||
"[volt-init] Failed to set MTU on {}: {}",
|
||||
name,
|
||||
std::io::Error::last_os_error()
|
||||
);
|
||||
}
|
||||
|
||||
unsafe { libc::close(sock) };
|
||||
}
|
||||
|
||||
fn add_default_route(gateway: &str, _iface: &str) {
|
||||
let sock = unsafe { libc::socket(libc::AF_INET, libc::SOCK_DGRAM, 0) };
|
||||
if sock < 0 {
|
||||
eprintln!(
|
||||
"[volt-init] Failed to create socket for routing: {}",
|
||||
std::io::Error::last_os_error()
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
let mut rt: Rtentry = unsafe { mem::zeroed() };
|
||||
rt.rt_dst = make_sockaddr_in("0.0.0.0");
|
||||
rt.rt_gateway = make_sockaddr_in(gateway);
|
||||
rt.rt_genmask = make_sockaddr_in("0.0.0.0");
|
||||
rt.rt_flags = (libc::RTF_UP | libc::RTF_GATEWAY) as libc::c_ushort;
|
||||
rt.rt_metric = 100;
|
||||
|
||||
// Use interface name
|
||||
let iface_c = CString::new(_iface).unwrap();
|
||||
rt.rt_dev = iface_c.as_ptr() as *mut libc::c_char;
|
||||
|
||||
let ret = unsafe { libc::ioctl(sock, SIOCADDRT, &rt) };
|
||||
if ret < 0 {
|
||||
let err = std::io::Error::last_os_error();
|
||||
// EEXIST is fine — route might already exist
|
||||
if err.raw_os_error() != Some(libc::EEXIST) {
|
||||
eprintln!("[volt-init] Failed to add default route: {}", err);
|
||||
}
|
||||
} else {
|
||||
println!("[volt-init] Default route via {} set", gateway);
|
||||
}
|
||||
|
||||
unsafe { libc::close(sock) };
|
||||
}
|
||||
|
||||
/// Get interface IP address (for `ip` command display)
|
||||
pub fn get_interface_info() -> Vec<(String, String)> {
|
||||
let mut result = Vec::new();
|
||||
if let Ok(entries) = std::fs::read_dir("/sys/class/net") {
|
||||
for entry in entries.flatten() {
|
||||
let name = entry.file_name().to_string_lossy().to_string();
|
||||
// Read operstate
|
||||
let state_path = format!("/sys/class/net/{}/operstate", name);
|
||||
let state = std::fs::read_to_string(&state_path)
|
||||
.unwrap_or_default()
|
||||
.trim()
|
||||
.to_string();
|
||||
// Read address
|
||||
let addr_path = format!("/sys/class/net/{}/address", name);
|
||||
let mac = std::fs::read_to_string(&addr_path)
|
||||
.unwrap_or_default()
|
||||
.trim()
|
||||
.to_string();
|
||||
result.push((name, format!("state={} mac={}", state, mac)));
|
||||
}
|
||||
}
|
||||
result.sort();
|
||||
result
|
||||
}
|
||||
445
rootfs/volt-init/src/shell.rs
Normal file
445
rootfs/volt-init/src/shell.rs
Normal file
@@ -0,0 +1,445 @@
|
||||
// Built-in shell for Volt VMs
|
||||
// All commands are built-in — no external binaries needed.
|
||||
|
||||
use std::io::{self, BufRead, Write};
|
||||
use std::net::Ipv4Addr;
|
||||
use std::time::Duration;
|
||||
|
||||
use crate::net;
|
||||
|
||||
pub fn run_shell() {
|
||||
let stdin = io::stdin();
|
||||
let mut stdout = io::stdout();
|
||||
|
||||
loop {
|
||||
print!("volt-vmm# ");
|
||||
let _ = stdout.flush();
|
||||
|
||||
let mut line = String::new();
|
||||
match stdin.lock().read_line(&mut line) {
|
||||
Ok(0) => {
|
||||
// EOF
|
||||
println!();
|
||||
break;
|
||||
}
|
||||
Ok(_) => {}
|
||||
Err(e) => {
|
||||
eprintln!("Read error: {}", e);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
let line = line.trim();
|
||||
if line.is_empty() {
|
||||
continue;
|
||||
}
|
||||
|
||||
let parts: Vec<&str> = line.split_whitespace().collect();
|
||||
let cmd = parts[0];
|
||||
let args = &parts[1..];
|
||||
|
||||
match cmd {
|
||||
"help" => cmd_help(),
|
||||
"ip" => cmd_ip(),
|
||||
"ping" => cmd_ping(args),
|
||||
"cat" => cmd_cat(args),
|
||||
"ls" => cmd_ls(args),
|
||||
"echo" => cmd_echo(args),
|
||||
"uptime" => cmd_uptime(),
|
||||
"free" => cmd_free(),
|
||||
"hostname" => cmd_hostname(),
|
||||
"dmesg" => cmd_dmesg(args),
|
||||
"env" | "printenv" => cmd_env(),
|
||||
"uname" => cmd_uname(),
|
||||
"exit" | "poweroff" | "reboot" | "halt" => {
|
||||
println!("Shutting down...");
|
||||
break;
|
||||
}
|
||||
_ => {
|
||||
eprintln!("{}: command not found. Type 'help' for available commands.", cmd);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn cmd_help() {
|
||||
println!("Volt VM Built-in Shell");
|
||||
println!("===========================");
|
||||
println!(" help Show this help");
|
||||
println!(" ip Show network interfaces");
|
||||
println!(" ping <host> Ping a host (ICMP echo)");
|
||||
println!(" cat <file> Display file contents");
|
||||
println!(" ls [dir] List directory contents");
|
||||
println!(" echo [text] Print text");
|
||||
println!(" uptime Show system uptime");
|
||||
println!(" free Show memory usage");
|
||||
println!(" hostname Show hostname");
|
||||
println!(" uname Show system info");
|
||||
println!(" dmesg [N] Show kernel log (last N lines)");
|
||||
println!(" env Show environment variables");
|
||||
println!(" exit Shutdown VM");
|
||||
}
|
||||
|
||||
fn cmd_ip() {
|
||||
let interfaces = net::get_interface_info();
|
||||
if interfaces.is_empty() {
|
||||
println!("No network interfaces found");
|
||||
return;
|
||||
}
|
||||
for (name, info) in interfaces {
|
||||
println!(" {}: {}", name, info);
|
||||
}
|
||||
}
|
||||
|
||||
fn cmd_ping(args: &[&str]) {
|
||||
if args.is_empty() {
|
||||
eprintln!("Usage: ping <host>");
|
||||
return;
|
||||
}
|
||||
|
||||
let target = args[0];
|
||||
|
||||
// Parse as IPv4 address
|
||||
let addr: Ipv4Addr = match target.parse() {
|
||||
Ok(a) => a,
|
||||
Err(_) => {
|
||||
// No DNS resolver — only IP addresses
|
||||
eprintln!("ping: {} — only IP addresses supported (no DNS)", target);
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
// Create raw ICMP socket
|
||||
let sock = unsafe { libc::socket(libc::AF_INET, libc::SOCK_DGRAM, libc::IPPROTO_ICMP) };
|
||||
if sock < 0 {
|
||||
eprintln!(
|
||||
"ping: failed to create ICMP socket: {}",
|
||||
io::Error::last_os_error()
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// Set timeout
|
||||
let tv = libc::timeval {
|
||||
tv_sec: 2,
|
||||
tv_usec: 0,
|
||||
};
|
||||
unsafe {
|
||||
libc::setsockopt(
|
||||
sock,
|
||||
libc::SOL_SOCKET,
|
||||
libc::SO_RCVTIMEO,
|
||||
&tv as *const _ as *const libc::c_void,
|
||||
std::mem::size_of::<libc::timeval>() as libc::socklen_t,
|
||||
);
|
||||
}
|
||||
|
||||
println!("PING {} — 3 packets", addr);
|
||||
|
||||
let mut dest: libc::sockaddr_in = unsafe { std::mem::zeroed() };
|
||||
dest.sin_family = libc::AF_INET as libc::sa_family_t;
|
||||
dest.sin_addr.s_addr = u32::from_ne_bytes(addr.octets());
|
||||
|
||||
let mut sent = 0u32;
|
||||
let mut received = 0u32;
|
||||
|
||||
for seq in 0..3u16 {
|
||||
// ICMP echo request packet
|
||||
let mut packet = [0u8; 64];
|
||||
packet[0] = 8; // Type: Echo Request
|
||||
packet[1] = 0; // Code
|
||||
packet[2] = 0; // Checksum (will fill)
|
||||
packet[3] = 0;
|
||||
packet[4] = 0; // ID
|
||||
packet[5] = 1;
|
||||
packet[6] = (seq >> 8) as u8; // Sequence
|
||||
packet[7] = (seq & 0xff) as u8;
|
||||
|
||||
// Fill payload with pattern
|
||||
for i in 8..64 {
|
||||
packet[i] = (i as u8) & 0xff;
|
||||
}
|
||||
|
||||
// Compute checksum
|
||||
let cksum = icmp_checksum(&packet);
|
||||
packet[2] = (cksum >> 8) as u8;
|
||||
packet[3] = (cksum & 0xff) as u8;
|
||||
|
||||
let start = std::time::Instant::now();
|
||||
|
||||
let ret = unsafe {
|
||||
libc::sendto(
|
||||
sock,
|
||||
packet.as_ptr() as *const libc::c_void,
|
||||
packet.len(),
|
||||
0,
|
||||
&dest as *const libc::sockaddr_in as *const libc::sockaddr,
|
||||
std::mem::size_of::<libc::sockaddr_in>() as libc::socklen_t,
|
||||
)
|
||||
};
|
||||
|
||||
if ret < 0 {
|
||||
eprintln!("ping: send failed: {}", io::Error::last_os_error());
|
||||
sent += 1;
|
||||
continue;
|
||||
}
|
||||
sent += 1;
|
||||
|
||||
// Receive reply
|
||||
let mut buf = [0u8; 1024];
|
||||
let ret = unsafe {
|
||||
libc::recvfrom(
|
||||
sock,
|
||||
buf.as_mut_ptr() as *mut libc::c_void,
|
||||
buf.len(),
|
||||
0,
|
||||
std::ptr::null_mut(),
|
||||
std::ptr::null_mut(),
|
||||
)
|
||||
};
|
||||
|
||||
let elapsed = start.elapsed();
|
||||
|
||||
if ret > 0 {
|
||||
received += 1;
|
||||
println!(
|
||||
" {} bytes from {}: seq={} time={:.1}ms",
|
||||
ret,
|
||||
addr,
|
||||
seq,
|
||||
elapsed.as_secs_f64() * 1000.0
|
||||
);
|
||||
} else {
|
||||
println!(" Request timeout for seq={}", seq);
|
||||
}
|
||||
|
||||
if seq < 2 {
|
||||
std::thread::sleep(Duration::from_secs(1));
|
||||
}
|
||||
}
|
||||
|
||||
unsafe { libc::close(sock) };
|
||||
|
||||
let loss = if sent > 0 {
|
||||
((sent - received) as f64 / sent as f64) * 100.0
|
||||
} else {
|
||||
100.0
|
||||
};
|
||||
println!(
|
||||
"--- {} ping statistics ---\n{} transmitted, {} received, {:.0}% loss",
|
||||
addr, sent, received, loss
|
||||
);
|
||||
}
|
||||
|
||||
fn icmp_checksum(data: &[u8]) -> u16 {
|
||||
let mut sum: u32 = 0;
|
||||
let mut i = 0;
|
||||
while i + 1 < data.len() {
|
||||
sum += ((data[i] as u32) << 8) | (data[i + 1] as u32);
|
||||
i += 2;
|
||||
}
|
||||
if i < data.len() {
|
||||
sum += (data[i] as u32) << 8;
|
||||
}
|
||||
while (sum >> 16) != 0 {
|
||||
sum = (sum & 0xFFFF) + (sum >> 16);
|
||||
}
|
||||
!sum as u16
|
||||
}
|
||||
|
||||
fn cmd_cat(args: &[&str]) {
|
||||
if args.is_empty() {
|
||||
eprintln!("Usage: cat <file>");
|
||||
return;
|
||||
}
|
||||
for path in args {
|
||||
match std::fs::read_to_string(path) {
|
||||
Ok(contents) => print!("{}", contents),
|
||||
Err(e) => eprintln!("cat: {}: {}", path, e),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn cmd_ls(args: &[&str]) {
|
||||
let dir = if args.is_empty() { "." } else { args[0] };
|
||||
|
||||
match std::fs::read_dir(dir) {
|
||||
Ok(entries) => {
|
||||
let mut names: Vec<String> = entries
|
||||
.filter_map(|e| e.ok())
|
||||
.map(|e| {
|
||||
let name = e.file_name().to_string_lossy().to_string();
|
||||
let meta = e.metadata().ok();
|
||||
if let Some(m) = meta {
|
||||
if m.is_dir() {
|
||||
format!("{}/ ", name)
|
||||
} else {
|
||||
let size = m.len();
|
||||
format!("{} ({}) ", name, human_size(size))
|
||||
}
|
||||
} else {
|
||||
format!("{} ", name)
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
names.sort();
|
||||
for name in &names {
|
||||
println!(" {}", name);
|
||||
}
|
||||
}
|
||||
Err(e) => eprintln!("ls: {}: {}", dir, e),
|
||||
}
|
||||
}
|
||||
|
||||
fn human_size(bytes: u64) -> String {
|
||||
if bytes >= 1024 * 1024 * 1024 {
|
||||
format!("{:.1}G", bytes as f64 / (1024.0 * 1024.0 * 1024.0))
|
||||
} else if bytes >= 1024 * 1024 {
|
||||
format!("{:.1}M", bytes as f64 / (1024.0 * 1024.0))
|
||||
} else if bytes >= 1024 {
|
||||
format!("{:.1}K", bytes as f64 / 1024.0)
|
||||
} else {
|
||||
format!("{}B", bytes)
|
||||
}
|
||||
}
|
||||
|
||||
fn cmd_echo(args: &[&str]) {
|
||||
println!("{}", args.join(" "));
|
||||
}
|
||||
|
||||
fn cmd_uptime() {
|
||||
if let Ok(uptime) = std::fs::read_to_string("/proc/uptime") {
|
||||
if let Some(secs) = uptime.split_whitespace().next() {
|
||||
if let Ok(s) = secs.parse::<f64>() {
|
||||
let hours = (s / 3600.0) as u64;
|
||||
let mins = ((s % 3600.0) / 60.0) as u64;
|
||||
let secs_remaining = s % 60.0;
|
||||
if hours > 0 {
|
||||
println!("up {}h {}m {:.0}s", hours, mins, secs_remaining);
|
||||
} else if mins > 0 {
|
||||
println!("up {}m {:.0}s", mins, secs_remaining);
|
||||
} else {
|
||||
println!("up {:.2}s", s);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
eprintln!("uptime: cannot read /proc/uptime");
|
||||
}
|
||||
}
|
||||
|
||||
fn cmd_free() {
|
||||
if let Ok(meminfo) = std::fs::read_to_string("/proc/meminfo") {
|
||||
println!(
|
||||
"{:<16} {:>12} {:>12} {:>12}",
|
||||
"", "total", "used", "free"
|
||||
);
|
||||
|
||||
let mut total = 0u64;
|
||||
let mut free = 0u64;
|
||||
let mut available = 0u64;
|
||||
let mut buffers = 0u64;
|
||||
let mut cached = 0u64;
|
||||
let mut swap_total = 0u64;
|
||||
let mut swap_free = 0u64;
|
||||
|
||||
for line in meminfo.lines() {
|
||||
if let Some(v) = extract_kb(line, "MemTotal:") {
|
||||
total = v;
|
||||
} else if let Some(v) = extract_kb(line, "MemFree:") {
|
||||
free = v;
|
||||
} else if let Some(v) = extract_kb(line, "MemAvailable:") {
|
||||
available = v;
|
||||
} else if let Some(v) = extract_kb(line, "Buffers:") {
|
||||
buffers = v;
|
||||
} else if let Some(v) = extract_kb(line, "Cached:") {
|
||||
cached = v;
|
||||
} else if let Some(v) = extract_kb(line, "SwapTotal:") {
|
||||
swap_total = v;
|
||||
} else if let Some(v) = extract_kb(line, "SwapFree:") {
|
||||
swap_free = v;
|
||||
}
|
||||
}
|
||||
|
||||
let used = total.saturating_sub(free).saturating_sub(buffers).saturating_sub(cached);
|
||||
println!(
|
||||
"{:<16} {:>10}K {:>10}K {:>10}K",
|
||||
"Mem:", total, used, free
|
||||
);
|
||||
if available > 0 {
|
||||
println!("Available: {:>10}K", available);
|
||||
}
|
||||
if swap_total > 0 {
|
||||
println!(
|
||||
"{:<16} {:>10}K {:>10}K {:>10}K",
|
||||
"Swap:",
|
||||
swap_total,
|
||||
swap_total - swap_free,
|
||||
swap_free
|
||||
);
|
||||
}
|
||||
} else {
|
||||
eprintln!("free: cannot read /proc/meminfo");
|
||||
}
|
||||
}
|
||||
|
||||
fn extract_kb(line: &str, key: &str) -> Option<u64> {
|
||||
if line.starts_with(key) {
|
||||
line[key.len()..]
|
||||
.trim()
|
||||
.trim_end_matches("kB")
|
||||
.trim()
|
||||
.parse()
|
||||
.ok()
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn cmd_hostname() {
|
||||
if let Ok(name) = std::fs::read_to_string("/etc/hostname") {
|
||||
println!("{}", name.trim());
|
||||
} else {
|
||||
println!("volt-vmm");
|
||||
}
|
||||
}
|
||||
|
||||
fn cmd_dmesg(args: &[&str]) {
|
||||
let limit: usize = args
|
||||
.first()
|
||||
.and_then(|a| a.parse().ok())
|
||||
.unwrap_or(20);
|
||||
|
||||
match std::fs::read_to_string("/dev/kmsg") {
|
||||
Ok(content) => {
|
||||
let lines: Vec<&str> = content.lines().collect();
|
||||
let start = lines.len().saturating_sub(limit);
|
||||
for line in &lines[start..] {
|
||||
// kmsg format: priority,sequence,timestamp;message
|
||||
if let Some(msg) = line.split(';').nth(1) {
|
||||
println!("{}", msg);
|
||||
} else {
|
||||
println!("{}", line);
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(_) => {
|
||||
// Fall back to /proc/kmsg or printk buffer via syslog
|
||||
eprintln!("dmesg: kernel log not available");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn cmd_env() {
|
||||
for (key, value) in std::env::vars() {
|
||||
println!("{}={}", key, value);
|
||||
}
|
||||
}
|
||||
|
||||
fn cmd_uname() {
|
||||
if let Ok(version) = std::fs::read_to_string("/proc/version") {
|
||||
println!("{}", version.trim());
|
||||
} else {
|
||||
println!("Volt VM");
|
||||
}
|
||||
}
|
||||
109
rootfs/volt-init/src/sys.rs
Normal file
109
rootfs/volt-init/src/sys.rs
Normal file
@@ -0,0 +1,109 @@
|
||||
// System utilities: signal handling, hostname, kernel cmdline, console
|
||||
|
||||
use std::ffi::CString;
|
||||
|
||||
/// Set up console I/O by ensuring fd 0/1/2 point to /dev/console or /dev/ttyS0
|
||||
pub fn setup_console() {
|
||||
// Try /dev/console first, then /dev/ttyS0
|
||||
let consoles = ["/dev/console", "/dev/ttyS0"];
|
||||
|
||||
for console in &consoles {
|
||||
let c_path = CString::new(*console).unwrap();
|
||||
let fd = unsafe { libc::open(c_path.as_ptr(), libc::O_RDWR | libc::O_NOCTTY | libc::O_NONBLOCK) };
|
||||
if fd >= 0 {
|
||||
// Clear O_NONBLOCK now that the open succeeded
|
||||
unsafe {
|
||||
let flags = libc::fcntl(fd, libc::F_GETFL);
|
||||
if flags >= 0 {
|
||||
libc::fcntl(fd, libc::F_SETFL, flags & !libc::O_NONBLOCK);
|
||||
}
|
||||
}
|
||||
|
||||
// Close existing fds and dup console to 0, 1, 2
|
||||
if fd != 0 {
|
||||
unsafe {
|
||||
libc::close(0);
|
||||
libc::dup2(fd, 0);
|
||||
}
|
||||
}
|
||||
unsafe {
|
||||
libc::close(1);
|
||||
libc::dup2(fd, 1);
|
||||
libc::close(2);
|
||||
libc::dup2(fd, 2);
|
||||
}
|
||||
if fd > 2 {
|
||||
unsafe {
|
||||
libc::close(fd);
|
||||
}
|
||||
}
|
||||
|
||||
// Make this our controlling terminal
|
||||
unsafe {
|
||||
libc::ioctl(0, libc::TIOCSCTTY as libc::Ioctl, 1);
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
// If we get here, no console device available — output will be lost
|
||||
}
|
||||
|
||||
/// Install signal handlers for PID 1
|
||||
pub fn install_signal_handlers() {
|
||||
unsafe {
|
||||
// SIGCHLD: reap zombies
|
||||
libc::signal(
|
||||
libc::SIGCHLD,
|
||||
sigchld_handler as *const () as libc::sighandler_t,
|
||||
);
|
||||
|
||||
// SIGTERM: ignore (PID 1 handles shutdown via shell)
|
||||
libc::signal(libc::SIGTERM, libc::SIG_IGN);
|
||||
|
||||
// SIGINT: ignore (Ctrl+C shouldn't kill init)
|
||||
libc::signal(libc::SIGINT, libc::SIG_IGN);
|
||||
}
|
||||
}
|
||||
|
||||
extern "C" fn sigchld_handler(_sig: libc::c_int) {
|
||||
// Reap all zombie children
|
||||
unsafe {
|
||||
loop {
|
||||
let ret = libc::waitpid(-1, std::ptr::null_mut(), libc::WNOHANG);
|
||||
if ret <= 0 {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Read kernel command line
|
||||
pub fn read_kernel_cmdline() -> String {
|
||||
std::fs::read_to_string("/proc/cmdline")
|
||||
.unwrap_or_default()
|
||||
.trim()
|
||||
.to_string()
|
||||
}
|
||||
|
||||
/// Parse a key=value from kernel cmdline
|
||||
pub fn parse_cmdline_value(cmdline: &str, key: &str) -> Option<String> {
|
||||
let prefix = format!("{}=", key);
|
||||
for param in cmdline.split_whitespace() {
|
||||
if let Some(value) = param.strip_prefix(&prefix) {
|
||||
return Some(value.to_string());
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
/// Set system hostname
|
||||
pub fn set_hostname(name: &str) {
|
||||
let c_name = CString::new(name).unwrap();
|
||||
let ret = unsafe { libc::sethostname(c_name.as_ptr(), name.len()) };
|
||||
if ret != 0 {
|
||||
eprintln!(
|
||||
"[volt-init] Failed to set hostname: {}",
|
||||
std::io::Error::last_os_error()
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user