This commit is contained in:
Maxime Van Hees
2025-09-01 16:12:50 +02:00
parent da3da0ae30
commit f4512b66cf
8 changed files with 991 additions and 238 deletions

View File

@@ -11,8 +11,10 @@ use std::hash::{Hash, Hasher};
use sal_os;
use sal_process;
use crate::qcow2;
use crate::cloudhv::net::{NetworkingProfileSpec, DefaultNatOptions};
pub mod builder;
pub mod net;
/// Error type for Cloud Hypervisor operations
#[derive(Debug)]
@@ -61,6 +63,9 @@ pub struct VmSpec {
pub cmdline: Option<String>,
/// Extra args (raw) if you need to extend; keep minimal for Phase 2
pub extra_args: Option<Vec<String>>,
/// Optional networking profile; when None, behavior follows explicit --net/--no-default-net or defaults
#[serde(default)]
pub net_profile: Option<NetworkingProfileSpec>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
@@ -394,72 +399,92 @@ pub fn vm_start(id: &str) -> Result<(), CloudHvError> {
.map(|v| v.iter().any(|tok| tok == "--net" || tok == "--no-default-net"))
.unwrap_or(false);
if !has_user_net {
// Networking prerequisites (bridge + NAT via nftables + dnsmasq DHCP)
// Defaults can be overridden via env:
// HERO_VIRT_BRIDGE_NAME, HERO_VIRT_BRIDGE_ADDR_CIDR, HERO_VIRT_SUBNET_CIDR, HERO_VIRT_DHCP_START, HERO_VIRT_DHCP_END
let bridge_name = std::env::var("HERO_VIRT_BRIDGE_NAME").unwrap_or_else(|_| "br-hero".into());
let bridge_addr_cidr =
std::env::var("HERO_VIRT_BRIDGE_ADDR_CIDR").unwrap_or_else(|_| "172.30.0.1/24".into());
let subnet_cidr =
std::env::var("HERO_VIRT_SUBNET_CIDR").unwrap_or_else(|_| "172.30.0.0/24".into());
let dhcp_start =
std::env::var("HERO_VIRT_DHCP_START").unwrap_or_else(|_| "172.30.0.50".into());
let dhcp_end =
std::env::var("HERO_VIRT_DHCP_END").unwrap_or_else(|_| "172.30.0.250".into());
// IPv6 over Mycelium: enabled by default.
// If explicitly disabled via HERO_VIRT_IPV6_ENABLE=false|0, we skip.
// If enabled but Mycelium is not detected, return an error.
let ipv6_env = std::env::var("HERO_VIRT_IPV6_ENABLE").unwrap_or_else(|_| "".into());
let ipv6_requested = match ipv6_env.to_lowercase().as_str() {
"" | "1" | "true" | "yes" => true,
"0" | "false" | "no" => false,
_ => true,
};
let mycelium_if_cfg = std::env::var("HERO_VIRT_MYCELIUM_IF").unwrap_or_else(|_| "mycelium".into());
let mut ipv6_bridge_cidr: Option<String> = None;
let mut mycelium_if_opt: Option<String> = None;
if ipv6_requested {
if let Ok(cidr) = std::env::var("HERO_VIRT_IPV6_BRIDGE_CIDR") {
// Explicit override for bridge IPv6 (e.g., "400:...::2/64") but still require mycelium iface presence.
// Validate mycelium interface and that it has IPv6 configured.
let _ = get_mycelium_ipv6_addr(&mycelium_if_cfg)?; // returns DependencyMissing on failure
ipv6_bridge_cidr = Some(cidr);
mycelium_if_opt = Some(mycelium_if_cfg.clone());
} else {
// Auto-derive from mycelium node address; error out if not detected.
println!("auto-deriving mycelium address...");
let (ifname, myc_addr) = get_mycelium_ipv6_addr(&mycelium_if_cfg)?;
println!("on if {ifname}, got myc addr: {myc_addr}");
let (_pfx, router_cidr) = derive_ipv6_prefix_from_mycelium(&myc_addr)?;
println!("derived pfx: {_pfx} and router cidr: {router_cidr}");
ipv6_bridge_cidr = Some(router_cidr);
mycelium_if_opt = Some(ifname);
// Track chosen bridge/lease for later discovery
let mut bridge_for_disc: Option<String> = None;
let mut lease_for_disc: Option<String> = None;
// Determine effective networking profile
let profile_effective = if let Some(p) = rec.spec.net_profile.clone() {
Some(p)
} else if has_user_net {
// User provided explicit --net or --no-default-net; do not provision
None
} else {
// Default behavior: NAT profile
Some(NetworkingProfileSpec::DefaultNat(DefaultNatOptions::default()))
};
if let Some(profile) = profile_effective {
match profile {
NetworkingProfileSpec::DefaultNat(mut nat) => {
// IPv6 handling (auto via Mycelium unless disabled)
let mut ipv6_bridge_cidr: Option<String> = None;
if nat.ipv6_enable {
if let Ok(cidr) = std::env::var("HERO_VIRT_IPV6_BRIDGE_CIDR") {
// Validate mycelium iface presence if specified or default
let if_hint = nat.mycelium_if.clone().unwrap_or_else(|| "mycelium".into());
let _ = net::mycelium_ipv6_addr(&if_hint)?;
ipv6_bridge_cidr = Some(cidr);
} else {
let if_hint = nat.mycelium_if.clone().unwrap_or_else(|| "mycelium".into());
println!("auto-deriving mycelium address...");
let (_ifname, myc_addr) = net::mycelium_ipv6_addr(&if_hint)?;
let (_pfx, router_cidr) = net::derive_ipv6_prefix_from_mycelium(&myc_addr)?;
println!("derived router cidr for bridge: {}", router_cidr);
ipv6_bridge_cidr = Some(router_cidr);
}
}
// Ensure bridge, NAT, and DHCP
net::ensure_bridge(&nat.bridge_name, &nat.bridge_addr_cidr, ipv6_bridge_cidr.as_deref())?;
net::ensure_nat(&nat.subnet_cidr)?;
let lease_used = net::ensure_dnsmasq(
&nat.bridge_name,
&nat.dhcp_start,
&nat.dhcp_end,
ipv6_bridge_cidr.as_deref(),
nat.lease_file.as_deref(),
)?;
bridge_for_disc = Some(nat.bridge_name.clone());
lease_for_disc = Some(lease_used.clone());
// TAP + NIC args
let tap_name = net::ensure_tap_for_vm(&nat.bridge_name, id)?;
println!("TAP device for vm called: {tap_name}");
let mac = net::stable_mac_from_id(id);
println!("MAC for vm: {mac}");
parts.push("--net".into());
parts.push(format!("tap={},mac={}", tap_name, mac));
}
NetworkingProfileSpec::BridgeOnly(opts) => {
let bridge_name = opts.bridge_name.clone();
// Use provided IPv4 if any, else env default
let bridge_addr_cidr = opts
.bridge_addr_cidr
.clone()
.unwrap_or_else(|| std::env::var("HERO_VIRT_BRIDGE_ADDR_CIDR").unwrap_or_else(|_| "172.30.0.1/24".into()));
// Ensure bridge (optional IPv6 from opts)
net::ensure_bridge(&bridge_name, &bridge_addr_cidr, opts.bridge_ipv6_cidr.as_deref())?;
// TAP + NIC only, no NAT/DHCP
let tap_name = net::ensure_tap_for_vm(&bridge_name, id)?;
println!("TAP device for vm called: {tap_name}");
let mac = net::stable_mac_from_id(id);
println!("MAC for vm: {mac}");
parts.push("--net".into());
parts.push(format!("tap={},mac={}", tap_name, mac));
// For discovery: we can attempt IPv6 neighbor; IPv4 lease not present
bridge_for_disc = Some(bridge_name);
lease_for_disc = None;
}
NetworkingProfileSpec::NoNet => {
// Do nothing
}
NetworkingProfileSpec::CustomCli(_args) => {
// Do not provision; user must add --net via extra_args
}
}
// Ensure host-side networking (requires root privileges / CAP_NET_ADMIN)
ensure_host_net_prereq_dnsmasq_nftables(
&bridge_name,
&bridge_addr_cidr,
&subnet_cidr,
&dhcp_start,
&dhcp_end,
ipv6_bridge_cidr.as_deref(),
mycelium_if_opt.as_deref(),
)?;
// Ensure a TAP device for this VM and attach to the bridge
let tap_name = ensure_tap_for_vm(&bridge_name, id)?;
println!("TAP device for vm called: {tap_name}");
// Stable locally-administered MAC derived from VM id
let mac = stable_mac_from_id(id);
println!("MAC for vm: {mac}");
parts.push("--net".into());
parts.push(format!("tap={},mac={}", tap_name, mac));
}
// Append any user-provided extra args, sans any '--disk' we already consolidated
@@ -540,73 +565,40 @@ pub fn vm_start(id: &str) -> Result<(), CloudHvError> {
println!("wrote JSON for VM");
// Best-effort: discover and print guest IPv4/IPv6 addresses (default-net path)
// Give DHCP/ND a moment
println!("waiting 5 secs for DHCP/ND");
thread::sleep(Duration::from_millis(5000));
let bridge_name = std::env::var("HERO_VIRT_BRIDGE_NAME").unwrap_or_else(|_| "br-hero".into());
let mac_lower = stable_mac_from_id(id).to_lowercase();
let mac_lower = net::stable_mac_from_id(id).to_lowercase();
// IPv4 from dnsmasq leases (pinned per-bridge leasefile)
// Path set in ensure_host_net_prereq_dnsmasq_nftables: /var/lib/misc/dnsmasq-hero-$BR.leases
let lease_path = std::env::var("HERO_VIRT_DHCP_LEASE_FILE")
.unwrap_or_else(|_| format!("/var/lib/misc/dnsmasq-hero-{}.leases", bridge_name));
// Parse dnsmasq leases directly to avoid shell quoting/pipelines
let ipv4 = (|| {
let deadline = std::time::Instant::now() + Duration::from_secs(12);
loop {
if let Ok(content) = fs::read_to_string(&lease_path) {
let mut last_ip: Option<String> = None;
for line in content.lines() {
let cols: Vec<&str> = line.split_whitespace().collect();
if cols.len() >= 3 && cols[1].eq_ignore_ascii_case(&mac_lower) {
last_ip = Some(cols[2].to_string());
}
}
if last_ip.is_some() {
return last_ip;
}
}
if std::time::Instant::now() >= deadline {
return None;
}
thread::sleep(Duration::from_millis(800));
}
})();
println!(
"Got IPv4 from dnsmasq lease ({}): {}",
lease_path,
ipv4.clone().unwrap_or("not found".to_string())
);
if let Some(bridge_name) = bridge_for_disc.clone() {
let lease_path = lease_for_disc.unwrap_or_else(|| {
std::env::var("HERO_VIRT_DHCP_LEASE_FILE")
.unwrap_or_else(|_| format!("/var/lib/misc/dnsmasq-hero-{}.leases", bridge_name))
});
let ipv4 = net::discover_ipv4_from_leases(&lease_path, &mac_lower, 12);
println!(
"Got IPv4 from dnsmasq lease ({}): {}",
lease_path,
ipv4.clone().unwrap_or("not found".to_string())
);
// IPv6 from neighbor table on the bridge (exclude link-local), parsed in Rust
let ipv6 = (|| {
let cmd = format!("ip -6 neigh show dev {}", bridge_name);
if let Ok(res) = sal_process::run(&cmd).silent(true).die(false).execute() {
if res.success {
let mac_pat = format!("lladdr {}", mac_lower);
for line in res.stdout.lines() {
let lt = line.trim();
if lt.to_lowercase().contains(&mac_pat) {
if let Some(addr) = lt.split_whitespace().next() {
if !addr.starts_with("fe80") && !addr.is_empty() {
return Some(addr.to_string());
}
}
}
}
}
}
None
})();
let ipv6 = net::discover_ipv6_on_bridge(&bridge_name, &mac_lower);
println!(
"Got IPv6 from neighbor table on bridge: {}",
ipv6.clone().unwrap_or("not found".to_string())
);
println!("Got IPv6 from neighbor table on bridge: {}", ipv6.clone().unwrap_or("not found".to_string()));
println!(
"[cloudhv] VM '{}' guest addresses: IPv4={}, IPv6={}",
id,
ipv4.as_deref().unwrap_or(""),
ipv6.as_deref().unwrap_or("")
);
println!(
"[cloudhv] VM '{}' guest addresses: IPv4={}, IPv6={}",
id,
ipv4.as_deref().unwrap_or(""),
ipv6.as_deref().unwrap_or("")
);
} else {
println!(
"[cloudhv] VM '{}' guest addresses discovery skipped (no default bridge in use)",
id
);
}
Ok(())
}