//! BGP client for GoBGP gRPC integration //! //! Provides a Rust wrapper around the GoBGP gRPC API to advertise //! and withdraw VIP routes for Anycast load balancing. use std::net::IpAddr; use std::sync::Arc; use thiserror::Error; use tonic::transport::Channel; use tracing::{debug, error, info, warn}; /// Result type for BGP operations pub type Result = std::result::Result; /// BGP client errors #[derive(Debug, Error)] pub enum BgpError { #[error("gRPC transport error: {0}")] Transport(String), #[error("BGP route operation failed: {0}")] RouteOperation(String), #[error("Invalid IP address: {0}")] InvalidAddress(String), #[error("GoBGP not reachable at {0}")] ConnectionFailed(String), } /// BGP client configuration #[derive(Debug, Clone)] pub struct BgpConfig { /// GoBGP gRPC server address (e.g., "127.0.0.1:50051") pub gobgp_address: String, /// Local AS number pub local_as: u32, /// Router ID in dotted decimal format pub router_id: String, /// Whether BGP integration is enabled pub enabled: bool, } impl Default for BgpConfig { fn default() -> Self { Self { gobgp_address: "127.0.0.1:50051".to_string(), local_as: 65001, router_id: "10.0.0.1".to_string(), enabled: false, } } } /// BGP client trait for VIP advertisement /// /// Abstracts the BGP speaker interface to allow for different implementations /// (GoBGP, RustyBGP, mock for testing) #[tonic::async_trait] pub trait BgpClient: Send + Sync { /// Advertise a VIP route to BGP peers async fn announce_route(&self, prefix: IpAddr, next_hop: IpAddr) -> Result<()>; /// Withdraw a VIP route from BGP peers async fn withdraw_route(&self, prefix: IpAddr) -> Result<()>; /// Check if client is connected to BGP daemon async fn is_connected(&self) -> bool; } /// GoBGP client implementation /// /// Connects to GoBGP daemon via gRPC and manages route advertisements pub struct GobgpClient { config: BgpConfig, _channel: Option, } impl GobgpClient { /// Create a new GoBGP client pub async fn new(config: BgpConfig) -> Result { if !config.enabled { info!("BGP is disabled in configuration"); return Ok(Self { config, _channel: None, }); } info!( "Connecting to GoBGP at {} (AS {})", config.gobgp_address, config.local_as ); // TODO: Connect to GoBGP gRPC server // For now, we create a client that logs operations but doesn't actually connect // Real implementation would use tonic::transport::Channel::connect() // and the GoBGP protobuf service stubs Ok(Self { config, _channel: None, }) } /// Get local router address for use as next hop fn get_next_hop(&self) -> Result { self.config .router_id .parse() .map_err(|e| BgpError::InvalidAddress(format!("Invalid router_id: {}", e))) } /// Format prefix as CIDR string (always /32 for VIP) fn format_prefix(addr: IpAddr) -> String { match addr { IpAddr::V4(_) => format!("{}/32", addr), IpAddr::V6(_) => format!("{}/128", addr), } } } #[tonic::async_trait] impl BgpClient for GobgpClient { async fn announce_route(&self, prefix: IpAddr, next_hop: IpAddr) -> Result<()> { if !self.config.enabled { debug!("BGP disabled, skipping route announcement for {}", prefix); return Ok(()); } let prefix_str = Self::format_prefix(prefix); info!( "Announcing BGP route: {} via {} (AS {})", prefix_str, next_hop, self.config.local_as ); // TODO: Actual GoBGP gRPC call // This would be something like: // // let mut client = gobgp_client::GobgpApiClient::new(self.channel.clone()); // let path = Path { // nlri: Some(IpAddressPrefix { // prefix_len: 32, // prefix: prefix.to_string(), // }), // pattrs: vec![ // PathAttribute::origin(Origin::Igp), // PathAttribute::next_hop(next_hop.to_string()), // PathAttribute::local_pref(100), // ], // }; // client.add_path(AddPathRequest { path: Some(path) }).await?; debug!("BGP route announced successfully: {}", prefix_str); Ok(()) } async fn withdraw_route(&self, prefix: IpAddr) -> Result<()> { if !self.config.enabled { debug!("BGP disabled, skipping route withdrawal for {}", prefix); return Ok(()); } let prefix_str = Self::format_prefix(prefix); info!("Withdrawing BGP route: {} (AS {})", prefix_str, self.config.local_as); // TODO: Actual GoBGP gRPC call // This would be something like: // // let mut client = gobgp_client::GobgpApiClient::new(self.channel.clone()); // let path = Path { // nlri: Some(IpAddressPrefix { // prefix_len: 32, // prefix: prefix.to_string(), // }), // is_withdraw: true, // // ... other fields // }; // client.delete_path(DeletePathRequest { path: Some(path) }).await?; debug!("BGP route withdrawn successfully: {}", prefix_str); Ok(()) } async fn is_connected(&self) -> bool { if !self.config.enabled { return false; } // TODO: Check GoBGP connection health // For now, always return true if enabled true } } /// Create a BGP client from configuration pub async fn create_bgp_client(config: BgpConfig) -> Result> { let client = GobgpClient::new(config).await?; Ok(Arc::new(client)) } #[cfg(test)] mod tests { use super::*; #[tokio::test] async fn test_bgp_client_disabled() { let config = BgpConfig { enabled: false, ..Default::default() }; let client = GobgpClient::new(config).await.unwrap(); assert!(!client.is_connected().await); // Operations should succeed but do nothing let vip = "10.0.1.100".parse().unwrap(); let next_hop = "10.0.0.1".parse().unwrap(); assert!(client.announce_route(vip, next_hop).await.is_ok()); assert!(client.withdraw_route(vip).await.is_ok()); } #[test] fn test_format_prefix() { let ipv4: IpAddr = "10.0.1.100".parse().unwrap(); assert_eq!(GobgpClient::format_prefix(ipv4), "10.0.1.100/32"); let ipv6: IpAddr = "2001:db8::1".parse().unwrap(); assert_eq!(GobgpClient::format_prefix(ipv6), "2001:db8::1/128"); } }