use serde::{Deserialize, Serialize};
pub const VSOCK_TOOL_PORT: u32 = 5000;
pub const VSOCK_HOST_CID: u32 = 2;
#[derive(Debug, Serialize, Deserialize)]
pub struct VsockToolRequest {
pub tool_name: String,
pub arguments: serde_json::Value,
pub timeout_ms: u64,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct VsockToolResponse {
pub success: bool,
pub result: serde_json::Value,
pub error: Option<String>,
pub execution_time_ms: u64,
}
pub struct VsockClient {
guest_cid: u32,
port: u32,
}
impl VsockClient {
pub fn new(guest_cid: u32) -> Self {
Self {
guest_cid,
port: VSOCK_TOOL_PORT,
}
}
pub fn with_port(guest_cid: u32, port: u32) -> Self {
Self { guest_cid, port }
}
#[cfg(target_os = "linux")]
pub async fn execute_tool(
&self,
request: VsockToolRequest,
) -> Result<VsockToolResponse, crate::error::IsolationError> {
use tokio::io::{AsyncReadExt, AsyncWriteExt};
let payload = serde_json::to_vec(&request)
.map_err(|e| crate::error::IsolationError::ExecutionFailed(e.to_string()))?;
let mut stream = tokio::net::UnixStream::connect(format!(
"/tmp/axocoatl-vsock-{}-{}",
self.guest_cid, self.port
))
.await
.map_err(|e| {
crate::error::IsolationError::ExecutionFailed(format!(
"vsock connect to CID {} port {}: {e}",
self.guest_cid, self.port
))
})?;
let len_bytes = (payload.len() as u32).to_be_bytes();
stream.write_all(&len_bytes).await.map_err(|e| {
crate::error::IsolationError::ExecutionFailed(format!("vsock write: {e}"))
})?;
stream.write_all(&payload).await.map_err(|e| {
crate::error::IsolationError::ExecutionFailed(format!("vsock write: {e}"))
})?;
let mut resp_len_bytes = [0u8; 4];
stream.read_exact(&mut resp_len_bytes).await.map_err(|e| {
crate::error::IsolationError::ExecutionFailed(format!("vsock read length: {e}"))
})?;
let resp_len = u32::from_be_bytes(resp_len_bytes) as usize;
let mut resp_buf = vec![0u8; resp_len];
stream.read_exact(&mut resp_buf).await.map_err(|e| {
crate::error::IsolationError::ExecutionFailed(format!("vsock read body: {e}"))
})?;
let response: VsockToolResponse = serde_json::from_slice(&resp_buf).map_err(|e| {
crate::error::IsolationError::ExecutionFailed(format!("vsock parse: {e}"))
})?;
Ok(response)
}
#[cfg(not(target_os = "linux"))]
pub async fn execute_tool(
&self,
_request: VsockToolRequest,
) -> Result<VsockToolResponse, crate::error::IsolationError> {
Err(crate::error::IsolationError::ExecutionFailed(
"vsock communication requires Linux with KVM".to_string(),
))
}
pub fn guest_cid(&self) -> u32 {
self.guest_cid
}
pub fn port(&self) -> u32 {
self.port
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn vsock_request_serde() {
let req = VsockToolRequest {
tool_name: "echo".to_string(),
arguments: serde_json::json!({"text": "hello"}),
timeout_ms: 5000,
};
let json = serde_json::to_string(&req).unwrap();
let back: VsockToolRequest = serde_json::from_str(&json).unwrap();
assert_eq!(back.tool_name, "echo");
}
#[test]
fn vsock_response_serde() {
let resp = VsockToolResponse {
success: true,
result: serde_json::json!({"text": "hello"}),
error: None,
execution_time_ms: 42,
};
let json = serde_json::to_string(&resp).unwrap();
let back: VsockToolResponse = serde_json::from_str(&json).unwrap();
assert!(back.success);
assert_eq!(back.execution_time_ms, 42);
}
#[test]
fn vsock_client_creates() {
let client = VsockClient::new(3);
assert_eq!(client.guest_cid(), 3);
assert_eq!(client.port(), VSOCK_TOOL_PORT);
}
#[test]
fn vsock_client_custom_port() {
let client = VsockClient::with_port(3, 6000);
assert_eq!(client.port(), 6000);
}
}