working ipv6 ip assignment + ssh with login/passwd
This commit is contained in:
@@ -408,27 +408,35 @@ pub fn vm_start(id: &str) -> Result<(), CloudHvError> {
|
||||
let dhcp_end =
|
||||
std::env::var("HERO_VIRT_DHCP_END").unwrap_or_else(|_| "172.30.0.250".into());
|
||||
|
||||
// Optional IPv6 over Mycelium provisioning (bridge P::2/64 + RA) guarded by env and detection.
|
||||
// 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 mut ipv6_enabled = ipv6_env.eq_ignore_ascii_case("1") || ipv6_env.eq_ignore_ascii_case("true");
|
||||
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;
|
||||
let enable_auto = ipv6_env.is_empty(); // auto-enable if mycelium is detected and not explicitly disabled
|
||||
|
||||
if ipv6_enabled || enable_auto {
|
||||
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")
|
||||
// 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());
|
||||
ipv6_enabled = true;
|
||||
} else if let Ok((ifname, myc_addr)) = get_mycelium_ipv6_addr(&mycelium_if_cfg) {
|
||||
// Derive P::2/64 from the mycelium node address
|
||||
if let Ok((_pfx, router_cidr)) = derive_ipv6_prefix_from_mycelium(&myc_addr) {
|
||||
ipv6_bridge_cidr = Some(router_cidr);
|
||||
mycelium_if_opt = Some(ifname);
|
||||
ipv6_enabled = true;
|
||||
}
|
||||
} 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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -445,8 +453,10 @@ pub fn vm_start(id: &str) -> Result<(), CloudHvError> {
|
||||
|
||||
// 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));
|
||||
@@ -465,6 +475,7 @@ pub fn vm_start(id: &str) -> Result<(), CloudHvError> {
|
||||
log_file,
|
||||
vm_pid_path(id).to_string_lossy()
|
||||
);
|
||||
println!("executing command:\n{heredoc}");
|
||||
// Execute command; this will background cloud-hypervisor and return
|
||||
let result = sal_process::run(&heredoc).execute();
|
||||
match result {
|
||||
@@ -489,6 +500,7 @@ pub fn vm_start(id: &str) -> Result<(), CloudHvError> {
|
||||
Ok(s) => s.trim().parse::<i64>().ok(),
|
||||
Err(_) => None,
|
||||
};
|
||||
println!("reading PID back: {} - (if 0 == not found)", pid.unwrap_or(0));
|
||||
|
||||
// Quick health check: ensure process did not exit immediately due to CLI errors (e.g., duplicate flags)
|
||||
if let Some(pid_num) = pid {
|
||||
@@ -496,6 +508,7 @@ pub fn vm_start(id: &str) -> Result<(), CloudHvError> {
|
||||
if !proc_exists(pid_num) {
|
||||
// Tail log to surface the error cause
|
||||
let tail_cmd = format!("tail -n 200 {}", shell_escape(&log_file));
|
||||
println!("executing tail_cmd command:\n{tail_cmd}");
|
||||
let tail = sal_process::run(&tail_cmd).die(false).silent(true).execute();
|
||||
let mut log_snip = String::new();
|
||||
if let Ok(res) = tail {
|
||||
@@ -524,6 +537,76 @@ pub fn vm_start(id: &str) -> Result<(), CloudHvError> {
|
||||
|
||||
let value = serde_json::to_value(&rec).map_err(|e| CloudHvError::JsonError(e.to_string()))?;
|
||||
write_json(&vm_json_path(id), &value)?;
|
||||
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();
|
||||
|
||||
// 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())
|
||||
);
|
||||
|
||||
// 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
|
||||
})();
|
||||
|
||||
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("")
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -711,50 +794,44 @@ fn stable_mac_from_id(id: &str) -> String {
|
||||
format!("{:02x}:{:02x}:{:02x}:{:02x}:{:02x}:{:02x}", b0, b1, b2, b3, b4, b5)
|
||||
}
|
||||
|
||||
/// Discover the mycelium IPv6 address and validate the interface.
|
||||
///
|
||||
/// Returns (interface_name, address_string).
|
||||
/// Discover the mycelium IPv6 address by inspecting the interface itself (no CLI dependency).
|
||||
/// Returns (interface_name, first global IPv6 address found on the interface).
|
||||
fn get_mycelium_ipv6_addr(iface_hint: &str) -> Result<(String, String), CloudHvError> {
|
||||
// Parse `mycelium inspect` for "Address: ..."
|
||||
let insp = sal_process::run("mycelium inspect").silent(true).die(false).execute();
|
||||
let mut addr: Option<String> = None;
|
||||
if let Ok(res) = insp {
|
||||
if res.success {
|
||||
for l in res.stdout.lines() {
|
||||
let lt = l.trim();
|
||||
if let Some(rest) = lt.strip_prefix("Address:") {
|
||||
let val = rest.trim().trim_matches('"').to_string();
|
||||
if !val.is_empty() {
|
||||
addr = Some(val);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
let iface = std::env::var("HERO_VIRT_MYCELIUM_IF").unwrap_or_else(|_| iface_hint.to_string());
|
||||
|
||||
// Validate interface exists and has IPv6 configured (best-effort)
|
||||
// Query IPv6 addresses on the interface
|
||||
let cmd = format!("ip -6 addr show dev {}", shell_escape(&iface));
|
||||
match sal_process::run(&cmd).silent(true).die(false).execute() {
|
||||
Ok(r) if r.success => {
|
||||
// proceed
|
||||
}
|
||||
let res = sal_process::run(&cmd).silent(true).die(false).execute();
|
||||
let out = match res {
|
||||
Ok(r) if r.success => r.stdout,
|
||||
_ => {
|
||||
return Err(CloudHvError::DependencyMissing(format!(
|
||||
"mycelium interface '{}' not found or no IPv6 configured",
|
||||
iface
|
||||
)));
|
||||
)))
|
||||
}
|
||||
};
|
||||
|
||||
// Extract the first global IPv6 address present on the interface.
|
||||
for line in out.lines() {
|
||||
let lt = line.trim();
|
||||
// Example line: "inet6 578:9fcf:.../7 scope global"
|
||||
if lt.starts_with("inet6 ") && lt.contains("scope global") {
|
||||
let parts: Vec<&str> = lt.split_whitespace().collect();
|
||||
if let Some(addr_cidr) = parts.get(1) {
|
||||
let addr_only = addr_cidr.split('/').next().unwrap_or("").trim();
|
||||
println!("got addr from host: {addr_only}");
|
||||
if !addr_only.is_empty() && addr_only.parse::<std::net::Ipv6Addr>().is_ok() {
|
||||
return Ok((iface, addr_only.to_string()));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(a) = addr {
|
||||
Ok((iface, a))
|
||||
} else {
|
||||
Err(CloudHvError::DependencyMissing(
|
||||
"failed to read mycelium IPv6 address via `mycelium inspect`".into(),
|
||||
))
|
||||
}
|
||||
Err(CloudHvError::DependencyMissing(format!(
|
||||
"no global IPv6 found on interface '{}'",
|
||||
iface
|
||||
)))
|
||||
}
|
||||
|
||||
/// Derive a /64 prefix P from the mycelium IPv6 and return (P/64, P::2/64).
|
||||
@@ -803,10 +880,15 @@ SUBNET={subnet}
|
||||
DHCP_START={dstart}
|
||||
DHCP_END={dend}
|
||||
IPV6_CIDR={v6cidr}
|
||||
LEASE_FILE=/var/lib/misc/dnsmasq-hero-$BR.leases
|
||||
|
||||
# Determine default WAN interface
|
||||
WAN_IF=$(ip -o route show default | awk '{{print $5}}' | head -n1)
|
||||
|
||||
if [ -z \"$WAN_IF\" ]; then
|
||||
echo \"No default WAN interface detected (required for IPv4 NAT)\" >&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
# Bridge creation (idempotent)
|
||||
ip link show \"$BR\" >/dev/null 2>&1 || ip link add name \"$BR\" type bridge
|
||||
ip addr replace \"$BR_ADDR\" dev \"$BR\"
|
||||
@@ -829,24 +911,38 @@ nft list chain ip hero postrouting | grep -q \"ip saddr $SUBNET oifname \\\"$WAN
|
||||
|
||||
# dnsmasq DHCPv4 + RA/DHCPv6 config (idempotent)
|
||||
mkdir -p /etc/dnsmasq.d
|
||||
mkdir -p /var/lib/misc
|
||||
CFG=/etc/dnsmasq.d/hero-$BR.conf
|
||||
TMP=/etc/dnsmasq.d/.hero-$BR.conf.new
|
||||
|
||||
# Always include IPv4 section
|
||||
cat >\"$TMP\" <<EOF
|
||||
interface=$BR
|
||||
bind-interfaces
|
||||
dhcp-range=$DHCP_START,$DHCP_END,12h
|
||||
dhcp-option=option:dns-server,1.1.1.1,8.8.8.8
|
||||
EOF
|
||||
RELOAD=0
|
||||
CONF=/etc/dnsmasq.conf
|
||||
# Ensure conf-dir includes /etc/dnsmasq.d (simple fixed-string check to avoid regex escapes in Rust)
|
||||
if ! grep -qF \"conf-dir=/etc/dnsmasq.d\" \"$CONF\"; then
|
||||
printf '%s\n' 'conf-dir=/etc/dnsmasq.d,*.conf' >> \"$CONF\"
|
||||
RELOAD=1
|
||||
fi
|
||||
|
||||
|
||||
# Ensure lease file exists and is writable by dnsmasq user
|
||||
touch \"$LEASE_FILE\" || true
|
||||
chown dnsmasq:dnsmasq \"$LEASE_FILE\" 2>/dev/null || true
|
||||
|
||||
# Always include IPv4 section
|
||||
printf '%s\n' \
|
||||
\"interface=$BR\" \
|
||||
\"bind-interfaces\" \
|
||||
\"dhcp-authoritative\" \
|
||||
\"dhcp-range=$DHCP_START,$DHCP_END,12h\" \
|
||||
\"dhcp-option=option:dns-server,1.1.1.1,8.8.8.8\" \
|
||||
\"dhcp-leasefile=$LEASE_FILE\" >\"$TMP\"
|
||||
|
||||
# Optionally append IPv6 RA/DHCPv6
|
||||
if [ -n \"$IPV6_CIDR\" ]; then
|
||||
cat >>\"$TMP\" <<'EOFV6'
|
||||
enable-ra
|
||||
dhcp-range=::,constructor:BR_PLACEHOLDER,ra-names,64,12h
|
||||
dhcp-option=option6:dns-server,[2001:4860:4860::8888],[2606:4700:4700::1111]
|
||||
EOFV6
|
||||
printf '%s\n' \
|
||||
\"enable-ra\" \
|
||||
\"dhcp-range=::,constructor:BR_PLACEHOLDER,ra-names,64,12h\" \
|
||||
\"dhcp-option=option6:dns-server,[2001:4860:4860::8888],[2606:4700:4700::1111]\" >>\"$TMP\"
|
||||
sed -i \"s/BR_PLACEHOLDER/$BR/g\" \"$TMP\"
|
||||
fi
|
||||
|
||||
@@ -861,6 +957,11 @@ else
|
||||
rm -f \"$TMP\"
|
||||
systemctl enable --now dnsmasq || true
|
||||
fi
|
||||
|
||||
# Reload if main conf was updated to include conf-dir
|
||||
if [ \"$RELOAD\" = \"1\" ]; then
|
||||
systemctl reload dnsmasq || systemctl restart dnsmasq || true
|
||||
fi
|
||||
",
|
||||
br = shell_escape(bridge_name),
|
||||
br_addr = shell_escape(bridge_addr_cidr),
|
||||
@@ -872,6 +973,7 @@ fi
|
||||
|
||||
// Use a unique heredoc delimiter to avoid clashing with inner <<EOF blocks
|
||||
let heredoc_net = format!("bash -e -s <<'HERONET'\n{}\nHERONET\n", body);
|
||||
println!("executing command:\n{heredoc_net}");
|
||||
|
||||
match sal_process::run(&heredoc_net).silent(true).execute() {
|
||||
Ok(res) if res.success => Ok(()),
|
||||
|
Reference in New Issue
Block a user