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>
228 lines
6.8 KiB
Rust
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");
|
|
}
|
|
}
|