use std::path::Path;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TapConfig {
pub tap_name: String,
pub host_ip: String,
pub guest_ip: String,
pub subnet_mask: String,
pub guest_mac: String,
}
impl TapConfig {
#[must_use]
pub fn for_vm(index: u16) -> Self {
let offset = 10u32 + u32::from(index) * 4;
let octet3 = (offset / 256) as u8;
let octet4_host = ((offset % 256) + 1) as u8;
let octet4_guest = ((offset % 256) + 2) as u8;
Self {
tap_name: format!("kavach-tap{index}"),
host_ip: format!("172.16.{octet3}.{octet4_host}"),
guest_ip: format!("172.16.{octet3}.{octet4_guest}"),
subnet_mask: "255.255.255.252".into(),
guest_mac: format!("AA:FC:00:00:{:02X}:{:02X}", index >> 8, index & 0xFF),
}
}
pub async fn setup(&self) -> crate::Result<()> {
tracing::debug!(tap = %self.tap_name, host_ip = %self.host_ip, "setting up TAP device");
run_cmd("ip", &["tuntap", "add", &self.tap_name, "mode", "tap"]).await?;
let cidr = format!("{}/30", self.host_ip);
run_cmd("ip", &["addr", "add", &cidr, "dev", &self.tap_name]).await?;
run_cmd("ip", &["link", "set", &self.tap_name, "up"]).await?;
run_cmd(
"iptables",
&[
"-A",
"FORWARD",
"-i",
&self.tap_name,
"-m",
"state",
"--state",
"ESTABLISHED,RELATED",
"-j",
"ACCEPT",
],
)
.await?;
run_cmd(
"iptables",
&["-A", "FORWARD", "-i", &self.tap_name, "-j", "DROP"],
)
.await?;
tracing::debug!(tap = %self.tap_name, "TAP device configured");
Ok(())
}
pub async fn teardown(&self) -> crate::Result<()> {
tracing::debug!(tap = %self.tap_name, "tearing down TAP device");
let _ = run_cmd(
"iptables",
&["-D", "FORWARD", "-i", &self.tap_name, "-j", "DROP"],
)
.await;
let _ = run_cmd(
"iptables",
&[
"-D",
"FORWARD",
"-i",
&self.tap_name,
"-m",
"state",
"--state",
"ESTABLISHED,RELATED",
"-j",
"ACCEPT",
],
)
.await;
let _ = run_cmd("ip", &["link", "del", &self.tap_name]).await;
tracing::debug!(tap = %self.tap_name, "TAP device removed");
Ok(())
}
#[must_use]
pub fn to_network_interface(&self) -> super::config::NetworkInterface {
super::config::NetworkInterface {
iface_id: "eth0".into(),
guest_mac: self.guest_mac.clone(),
host_dev_name: self.tap_name.clone(),
}
}
#[must_use]
pub fn guest_boot_args(&self) -> String {
format!(
"ip={}::{}:{}::eth0:off",
self.guest_ip, self.host_ip, self.subnet_mask
)
}
pub async fn exists(&self) -> bool {
let path = format!("/sys/class/net/{}", self.tap_name);
Path::new(&path).exists()
}
}
async fn run_cmd(program: &str, args: &[&str]) -> crate::Result<()> {
let output = tokio::process::Command::new(program)
.args(args)
.output()
.await
.map_err(|e| {
crate::KavachError::ExecFailed(format!("{program} {}: {e}", args.join(" ")))
})?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
return Err(crate::KavachError::ExecFailed(format!(
"{program} failed: {stderr}"
)));
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn tap_config_for_vm() {
let tap = TapConfig::for_vm(0);
assert_eq!(tap.tap_name, "kavach-tap0");
assert_eq!(tap.host_ip, "172.16.0.11");
assert_eq!(tap.guest_ip, "172.16.0.12");
assert_eq!(tap.subnet_mask, "255.255.255.252");
}
#[test]
fn tap_config_sequential_ips() {
let tap0 = TapConfig::for_vm(0);
let tap1 = TapConfig::for_vm(1);
assert_ne!(tap0.host_ip, tap1.host_ip);
assert_ne!(tap0.guest_ip, tap1.guest_ip);
assert_ne!(tap0.tap_name, tap1.tap_name);
}
#[test]
fn network_interface_generation() {
let tap = TapConfig::for_vm(0);
let iface = tap.to_network_interface();
assert_eq!(iface.host_dev_name, "kavach-tap0");
assert_eq!(iface.iface_id, "eth0");
}
#[test]
fn guest_boot_args() {
let tap = TapConfig::for_vm(0);
let args = tap.guest_boot_args();
assert!(args.contains("172.16.0.12"));
assert!(args.contains("172.16.0.11"));
}
#[test]
fn serde_roundtrip() {
let tap = TapConfig::for_vm(5);
let json = serde_json::to_string(&tap).unwrap();
let back: TapConfig = serde_json::from_str(&json).unwrap();
assert_eq!(tap.tap_name, back.tap_name);
assert_eq!(tap.host_ip, back.host_ip);
}
#[test]
fn mac_address_format() {
let tap = TapConfig::for_vm(256);
assert_eq!(tap.guest_mac, "AA:FC:00:00:01:00");
}
#[tokio::test]
async fn tap_exists_false() {
let tap = TapConfig::for_vm(9999);
assert!(!tap.exists().await);
}
}