working VM setup

This commit is contained in:
Maxime Van Hees
2025-09-02 15:17:52 +02:00
parent f4512b66cf
commit 0f4ed1d64d
3 changed files with 50 additions and 14 deletions

View File

@@ -437,7 +437,23 @@ pub fn vm_start(id: &str) -> Result<(), CloudHvError> {
// 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)?;
// Derive IPv6 subnet for NAT
let ipv6_subnet = ipv6_bridge_cidr.as_ref().map(|cidr| {
let parts: Vec<&str> = cidr.split('/').collect();
if parts.len() == 2 {
let addr = parts[0];
if let Ok(ip) = addr.parse::<std::net::Ipv6Addr>() {
let seg = ip.segments();
let pfx = std::net::Ipv6Addr::new(seg[0], seg[1], seg[2], seg[3], 0, 0, 0, 0);
format!("{}/64", pfx)
} else {
"".to_string()
}
} else {
"".to_string()
}
});
net::ensure_nat(&nat.subnet_cidr, ipv6_subnet.as_deref())?;
let lease_used = net::ensure_dnsmasq(
&nat.bridge_name,
&nat.dhcp_start,

View File

@@ -86,7 +86,8 @@ sysctl -w net.ipv4.ip_forward=1 >/dev/null || true
/// Ensure nftables NAT masquerading for the given subnet toward the default WAN interface.
/// Creates table/chain if missing and adds/keeps a single masquerade rule.
pub fn ensure_nat(subnet_cidr: &str) -> Result<(), CloudHvError> {
/// If ipv6_subnet is provided, also sets up IPv6 NAT.
pub fn ensure_nat(subnet_cidr: &str, ipv6_subnet: Option<&str>) -> Result<(), CloudHvError> {
for bin in ["ip", "nft"] {
if sal_process::which(bin).is_none() {
return Err(CloudHvError::DependencyMissing(format!(
@@ -95,23 +96,34 @@ pub fn ensure_nat(subnet_cidr: &str) -> Result<(), CloudHvError> {
)));
}
}
let v6_subnet = ipv6_subnet.unwrap_or("");
let body = format!(
"set -e
SUBNET={subnet}
IPV6_SUBNET={v6subnet}
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
echo \"No default WAN interface detected (required for NAT)\" >&2
exit 2
fi
# IPv4 NAT
nft list table ip hero >/dev/null 2>&1 || nft add table ip hero
nft list chain ip hero postrouting >/dev/null 2>&1 || nft add chain ip hero postrouting {{ type nat hook postrouting priority 100 \\; }}
# Only add rule if not present
nft list chain ip hero postrouting | grep -q \"ip saddr $SUBNET oifname \\\"$WAN_IF\\\" masquerade\" \
|| nft add rule ip hero postrouting ip saddr $SUBNET oifname \"$WAN_IF\" masquerade
# IPv6 NAT (if subnet provided)
if [ -n \"$IPV6_SUBNET\" ]; then
nft list table ip6 hero >/dev/null 2>&1 || nft add table ip6 hero
nft list chain ip6 hero postrouting >/dev/null 2>&1 || nft add chain ip6 hero postrouting {{ type nat hook postrouting priority 100 \\; }}
nft list chain ip6 hero postrouting | grep -q \"ip6 saddr $IPV6_SUBNET oifname \\\"$WAN_IF\\\" masquerade\" \
|| nft add rule ip6 hero postrouting ip6 saddr $IPV6_SUBNET oifname \"$WAN_IF\" masquerade
fi
",
subnet = shell_escape(subnet_cidr),
v6subnet = shell_escape(v6_subnet),
);
run_heredoc("HERONAT", &body)
}
@@ -174,11 +186,13 @@ printf '%s\\n' \
# Optional IPv6 RA/DHCPv6
if [ -n \"$IPV6_CIDR\" ]; then
BRIDGE_ADDR=\"${{IPV6_CIDR%/*}}\"
BRIDGE_PREFIX=$(echo \"$IPV6_CIDR\" | cut -d: -f1-4)::
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\"
\"dhcp-range=$BRIDGE_PREFIX,ra-names,12h\" \
\"dhcp-option=option6:dns-server,[2001:4860:4860::8888]\" \
\"dhcp-option=option6:route,3600,400::/7,[$BRIDGE_ADDR]\" >>\"$TMP\"
fi
if [ ! -f \"$CFG\" ] || ! cmp -s \"$CFG\" \"$TMP\"; then

View File

@@ -175,11 +175,11 @@ pub fn image_prepare(opts: &ImagePrepOptions) -> Result<ImagePrepResult, ImagePr
// Build bash script that performs all steps and echos "RAW|ROOT_UUID|BOOT_UUID" at end
let disable_ci_net = opts.disable_cloud_init_net;
// IPv6 static guest assignment (derive from mycelium interface) - enabled by default
// If HERO_VIRT_IPV6_STATIC_GUEST=false, keep dynamic behavior (SLAAC/DHCPv6).
// IPv6 static guest assignment (derive from mycelium interface) - disabled by default to use RA
// If HERO_VIRT_IPV6_STATIC_GUEST=true, use static IPv6; else use RA/SLAAC.
let static_v6 = std::env::var("HERO_VIRT_IPV6_STATIC_GUEST")
.map(|v| matches!(v.to_lowercase().as_str(), "" | "1" | "true" | "yes"))
.unwrap_or(true);
.unwrap_or(false);
let myc_if =
std::env::var("HERO_VIRT_MYCELIUM_IF").unwrap_or_else(|_| "mycelium".into());
@@ -211,6 +211,7 @@ pub fn image_prepare(opts: &ImagePrepOptions) -> Result<ImagePrepResult, ImagePr
// Derive per-host /64 from mycelium and deterministic per-VM guest address
let mut np_v6_block = String::new();
let mut accept_ra = String::new();
let mut dhcp6_effective = opts.net.dhcp6;
if static_v6 {
if let Some(h) = host_v6 {
@@ -229,12 +230,16 @@ pub fn image_prepare(opts: &ImagePrepOptions) -> Result<ImagePrepResult, ImagePr
// Inject a YAML block for static v6
np_v6_block = format!(
" addresses:\n - {}/64\n routes:\n - to: \"::/0\"\n via: {}\n",
guest_ip, gw_ip
" addresses:\n - {}/64\n routes:\n - to: \"::/0\"\n via: {}\n - to: \"400::/7\"\n via: {}\n",
guest_ip, gw_ip, gw_ip
);
// Disable dhcp6 when we provide a static address
dhcp6_effective = false;
}
} else {
// Use RA for IPv6
accept_ra = "\n accept-ra: true".to_string();
dhcp6_effective = false;
}
// Keep script small and robust; avoid brace-heavy awk to simplify escaping.
@@ -416,8 +421,8 @@ network:
macaddress: {vm_mac}
set-name: eth0
dhcp4: {dhcp4}
dhcp6: {dhcp6}
{np_v6_block} nameservers:
dhcp6: {dhcp6}{accept_ra}{np_v6_block}
nameservers:
addresses: [8.8.8.8, 1.1.1.1, 2001:4860:4860::8888]
EOF
# Enable SSH password authentication and set a default password for 'ubuntu'
@@ -721,6 +726,7 @@ exit 0
vm_mac = vm_mac,
dhcp4 = if opts.net.dhcp4 { "true" } else { "false" },
dhcp6 = if dhcp6_effective { "true" } else { "false" },
accept_ra = accept_ra,
np_v6_block = np_v6_block,
disable_ci_net = if disable_ci_net { "true" } else { "false" }
);