//! Local VIP ownership management for FiberLB. //! //! This module claims and releases VIP `/32` addresses on a local interface so //! the kernel actually treats the advertised VIP as a local destination. use std::net::{IpAddr, Ipv4Addr}; use std::path::Path; use tokio::process::Command; /// Result type for VIP ownership operations. pub type Result = std::result::Result; /// VIP ownership errors. #[derive(Debug, thiserror::Error)] pub enum VipOwnershipError { #[error("I/O error: {0}")] Io(#[from] std::io::Error), #[error("kernel VIP ownership command failed: {0}")] CommandFailed(String), #[error("unsupported VIP ownership operation: {0}")] Unsupported(String), } /// Trait for local VIP ownership. #[tonic::async_trait] pub trait VipAddressOwner: Send + Sync { /// Ensure a VIP is locally owned on this node. async fn ensure_present(&self, vip: IpAddr) -> Result<()>; /// Ensure a VIP is no longer locally owned on this node. async fn ensure_absent(&self, vip: IpAddr) -> Result<()>; } /// Linux kernel-backed VIP owner using `ip addr add/del`. pub struct KernelVipAddressOwner { interface: String, ip_command: String, } impl KernelVipAddressOwner { /// Create a kernel-backed VIP owner for the given interface. pub fn new(interface: impl Into) -> Self { Self { interface: interface.into(), ip_command: resolve_ip_command(), } } async fn run_ip(&self, args: &[String]) -> Result<()> { let output = Command::new(&self.ip_command).args(args).output().await?; if output.status.success() { return Ok(()); } Err(VipOwnershipError::CommandFailed(format!( "{} {}: {}", self.ip_command, args.join(" "), render_command_output(&output) ))) } async fn add_v4(&self, vip: Ipv4Addr) -> Result<()> { let args = vec![ "-4".to_string(), "addr".to_string(), "add".to_string(), format!("{vip}/32"), "dev".to_string(), self.interface.clone(), ]; match self.run_ip(&args).await { Ok(()) => Ok(()), Err(VipOwnershipError::CommandFailed(message)) if message.contains("File exists") => { Ok(()) } Err(error) => Err(error), } } async fn del_v4(&self, vip: Ipv4Addr) -> Result<()> { let args = vec![ "-4".to_string(), "addr".to_string(), "del".to_string(), format!("{vip}/32"), "dev".to_string(), self.interface.clone(), ]; match self.run_ip(&args).await { Ok(()) => Ok(()), Err(VipOwnershipError::CommandFailed(message)) if message.contains("Cannot assign requested address") => { Ok(()) } Err(error) => Err(error), } } } #[tonic::async_trait] impl VipAddressOwner for KernelVipAddressOwner { async fn ensure_present(&self, vip: IpAddr) -> Result<()> { match vip { IpAddr::V4(vip) => self.add_v4(vip).await, IpAddr::V6(_) => Err(VipOwnershipError::Unsupported(format!( "VIP ownership currently supports IPv4 only (got {vip})" ))), } } async fn ensure_absent(&self, vip: IpAddr) -> Result<()> { match vip { IpAddr::V4(vip) => self.del_v4(vip).await, IpAddr::V6(_) => Err(VipOwnershipError::Unsupported(format!( "VIP ownership currently supports IPv4 only (got {vip})" ))), } } } fn resolve_ip_command() -> String { if let Ok(path) = std::env::var("FIBERLB_IP_COMMAND") { let trimmed = path.trim(); if !trimmed.is_empty() { return trimmed.to_string(); } } for candidate in [ "/run/current-system/sw/bin/ip", "/usr/sbin/ip", "/sbin/ip", "/usr/bin/ip", "/bin/ip", ] { if Path::new(candidate).exists() { return candidate.to_string(); } } "ip".to_string() } fn render_command_output(output: &std::process::Output) -> String { let stderr = String::from_utf8_lossy(&output.stderr).trim().to_string(); if !stderr.is_empty() { return stderr; } let stdout = String::from_utf8_lossy(&output.stdout).trim().to_string(); if !stdout.is_empty() { return stdout; } format!("exit status {}", output.status) }