photoncloud-monorepo/fiberlb/crates/fiberlb-server/src/bgp_client.rs
centra 3eeb303dcb feat: Batch commit for T039.S3 deployment
Includes all pending changes needed for nixos-anywhere:
- fiberlb: L7 policy, rule, certificate types
- deployer: New service for cluster management
- nix-nos: Generic network modules
- Various service updates and fixes

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-13 04:34:51 +09:00

228 lines
6.8 KiB
Rust

//! 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<T> = std::result::Result<T, BgpError>;
/// 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<Channel>,
}
impl GobgpClient {
/// Create a new GoBGP client
pub async fn new(config: BgpConfig) -> Result<Self> {
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<IpAddr> {
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<Arc<dyn BgpClient>> {
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");
}
}