use std::net::Ipv4Addr;
use crate::error::{VmRuntimeError, VmRuntimeResult};
use crate::network::ipv4_net::Ipv4Net;
pub const TAP_NAME_PREFIX: &str = "tap-";
pub const TAP_HASH_LEN: usize = 8;
pub fn vm_short_hash(vm_id: &str) -> String {
let digest = fnv1a_64(vm_id.as_bytes());
let hex = format!("{digest:016x}");
hex[..TAP_HASH_LEN].to_string()
}
pub fn tap_name_for(vm_id: &str) -> String {
format!("{TAP_NAME_PREFIX}{}", vm_short_hash(vm_id))
}
pub fn allocate_guest_ip(vm_id: &str, subnet: Ipv4Net) -> VmRuntimeResult<Ipv4Addr> {
let total = subnet.num_addresses();
if total < 4 {
return Err(VmRuntimeError::NetworkConfig(format!(
"subnet {subnet} too small: need /30 or larger (got {total} addresses)"
)));
}
let usable = total - 3; let digest = fnv1a_64(vm_id.as_bytes());
let offset = (digest % usable) as u32 + 2;
subnet.nth(offset).ok_or_else(|| {
VmRuntimeError::NetworkConfig(format!(
"computed offset {offset} out of range for subnet {subnet}"
))
})
}
pub fn gateway_for(subnet: Ipv4Net) -> VmRuntimeResult<Ipv4Addr> {
subnet.nth(1).ok_or_else(|| {
VmRuntimeError::NetworkConfig(format!("subnet {subnet} has no usable host address"))
})
}
pub fn allocate_guest_mac(vm_id: &str) -> [u8; 6] {
let digest = fnv1a_64(vm_id.as_bytes());
let mut mac = [0u8; 6];
mac[0] = 0x02; mac[1] = ((digest >> 32) & 0xff) as u8;
mac[2] = ((digest >> 24) & 0xff) as u8;
mac[3] = ((digest >> 16) & 0xff) as u8;
mac[4] = ((digest >> 8) & 0xff) as u8;
mac[5] = (digest & 0xff) as u8;
mac
}
pub fn format_mac(mac: [u8; 6]) -> String {
format!(
"{:02x}:{:02x}:{:02x}:{:02x}:{:02x}:{:02x}",
mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]
)
}
const FNV_OFFSET_BASIS: u64 = 0xcbf2_9ce4_8422_2325;
const FNV_PRIME: u64 = 0x0000_0100_0000_01b3;
fn fnv1a_64(bytes: &[u8]) -> u64 {
let mut hash = FNV_OFFSET_BASIS;
for &b in bytes {
hash ^= u64::from(b);
hash = hash.wrapping_mul(FNV_PRIME);
}
hash
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn fnv1a_matches_reference_vectors() {
assert_eq!(fnv1a_64(b""), 0xcbf2_9ce4_8422_2325);
assert_eq!(fnv1a_64(b"a"), 0xaf63_dc4c_8601_ec8c);
assert_eq!(fnv1a_64(b"foobar"), 0x8594_4171_f739_67e8);
assert_eq!(fnv1a_64(b"hello"), 0xa430_d846_80aa_bd0b);
}
#[test]
fn vm_short_hash_is_deterministic() {
let a = vm_short_hash("vm-abc-123");
let b = vm_short_hash("vm-abc-123");
assert_eq!(a, b);
assert_eq!(a.len(), TAP_HASH_LEN);
assert!(a.chars().all(|c| c.is_ascii_hexdigit()));
}
#[test]
fn tap_name_fits_kernel_limit() {
let name = tap_name_for("very-long-vm-identifier-1234567890");
assert!(name.len() <= 15, "tap '{name}' must be <= 15 bytes");
assert!(name.starts_with("tap-"));
}
#[test]
fn tap_name_changes_with_vm_id() {
assert_ne!(tap_name_for("a"), tap_name_for("b"));
}
#[test]
fn ip_allocation_is_deterministic() {
let subnet: Ipv4Net = "172.30.0.0/24".parse().unwrap();
let first = allocate_guest_ip("vm-1", subnet).unwrap();
let second = allocate_guest_ip("vm-1", subnet).unwrap();
assert_eq!(first, second);
}
#[test]
fn ip_allocation_skips_reserved() {
let subnet: Ipv4Net = "172.30.0.0/24".parse().unwrap();
let gateway = gateway_for(subnet).unwrap();
let broadcast = subnet.broadcast();
let network = subnet.network();
for i in 0..256u32 {
let id = format!("vm-{i}");
let ip = allocate_guest_ip(&id, subnet).unwrap();
assert!(subnet.contains(ip), "{ip} outside {subnet}");
assert_ne!(ip, network, "must skip network address");
assert_ne!(ip, gateway, "must skip gateway");
assert_ne!(ip, broadcast, "must skip broadcast");
}
}
#[test]
fn ip_allocation_rejects_too_small_subnet() {
let subnet: Ipv4Net = "10.0.0.0/31".parse().unwrap();
assert!(allocate_guest_ip("vm-x", subnet).is_err());
}
#[test]
fn mac_is_locally_administered_unicast() {
let mac = allocate_guest_mac("anything");
assert_eq!(mac[0] & 0x02, 0x02, "locally-administered bit not set");
assert_eq!(
mac[0] & 0x01,
0x00,
"unicast bit not set (multicast forbidden)"
);
}
#[test]
fn mac_is_deterministic() {
assert_eq!(allocate_guest_mac("vm-1"), allocate_guest_mac("vm-1"));
assert_ne!(allocate_guest_mac("vm-1"), allocate_guest_mac("vm-2"));
}
#[test]
fn mac_format_is_canonical() {
let mac = [0x02, 0xab, 0xcd, 0xef, 0x01, 0x23];
assert_eq!(format_mac(mac), "02:ab:cd:ef:01:23");
}
#[test]
fn gateway_is_first_host_address() {
let subnet: Ipv4Net = "172.30.0.0/24".parse().unwrap();
assert_eq!(gateway_for(subnet).unwrap(), Ipv4Addr::new(172, 30, 0, 1));
}
}