use std::time::Duration;
use arcbox_helper::client::{Client, ClientError};
use arcbox_helper::error::HelperError;
use arcbox_route::{Ipv4Net, RouteInfo};
use crate::bridge_discovery;
pub const CONTAINER_SUBNET: &str = "172.16.0.0/12";
pub const CONTAINER_SPLIT_SUBNETS: [&str; 2] = ["172.16.0.0/13", "172.24.0.0/13"];
const MAX_ROUTE_ATTEMPTS: u32 = 5;
const ROUTE_RETRY_INTERVAL: Duration = Duration::from_secs(2);
#[derive(Debug, thiserror::Error)]
pub enum RouteError {
#[error("bridge not found in kernel FDB")]
BridgeNotReady,
#[error("helper unavailable: {0}")]
HelperUnavailable(String),
#[error("route operation failed: {0}")]
RouteFailed(String),
#[error("route {subnet} is owned by another network service")]
RouteConflict {
subnet: String,
},
}
impl From<ClientError> for RouteError {
fn from(e: ClientError) -> Self {
match e {
ClientError::Connection(_)
| ClientError::Rpc(_)
| ClientError::UnrecognizedVersion(_)
| ClientError::IncompatibleVersion { .. } => Self::HelperUnavailable(e.to_string()),
ClientError::Helper(err) => Self::RouteFailed(err.to_string()),
}
}
}
fn route_matches_bridge(route: Option<&RouteInfo>, bridge_ifindex: u16) -> bool {
matches!(
route,
Some(route)
if route.ifindex == bridge_ifindex && route.flags & libc::RTF_GATEWAY == 0
)
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum RouteMode {
Preferred,
SplitFallback,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum ExactRouteState {
Missing,
Owned,
External,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
struct RouteSnapshot {
preferred: ExactRouteState,
split: [ExactRouteState; 2],
}
impl RouteSnapshot {
fn is_healthy(self) -> bool {
(self.preferred == ExactRouteState::Owned
&& !self.split.contains(&ExactRouteState::External))
|| self
.split
.iter()
.all(|state| *state == ExactRouteState::Owned)
}
fn initial_mode(self) -> RouteMode {
if self
.split
.iter()
.all(|state| *state == ExactRouteState::Owned)
{
RouteMode::SplitFallback
} else if self.preferred == ExactRouteState::Owned {
RouteMode::Preferred
} else if self.preferred == ExactRouteState::External
|| self.split.contains(&ExactRouteState::Owned)
{
RouteMode::SplitFallback
} else {
RouteMode::Preferred
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum ReconcileAction {
Healthy(RouteMode),
AddPreferred,
EnsureSplit,
Conflict(&'static str),
}
fn plan_reconciliation(mode: RouteMode, snapshot: RouteSnapshot) -> ReconcileAction {
if let Some(index) = snapshot
.split
.iter()
.position(|state| *state == ExactRouteState::External)
{
return ReconcileAction::Conflict(CONTAINER_SPLIT_SUBNETS[index]);
}
if mode == RouteMode::SplitFallback {
return if snapshot
.split
.iter()
.all(|state| *state == ExactRouteState::Owned)
{
ReconcileAction::Healthy(RouteMode::SplitFallback)
} else {
ReconcileAction::EnsureSplit
};
}
match snapshot.preferred {
ExactRouteState::Owned => ReconcileAction::Healthy(RouteMode::Preferred),
ExactRouteState::External => ReconcileAction::EnsureSplit,
ExactRouteState::Missing => ReconcileAction::AddPreferred,
}
}
fn parse_network(value: &str) -> Result<Ipv4Net, RouteError> {
value.parse().map_err(|error| {
RouteError::RouteFailed(format!("invalid container network {value}: {error}"))
})
}
fn classify_route(route: Option<&RouteInfo>, bridge_ifindex: u16) -> ExactRouteState {
match route {
None => ExactRouteState::Missing,
Some(route) if route_matches_bridge(Some(route), bridge_ifindex) => ExactRouteState::Owned,
Some(_) => ExactRouteState::External,
}
}
fn inspect_routes_sync(bridge_name: &str) -> Result<RouteSnapshot, RouteError> {
let bridge_ifindex =
arcbox_route::interface_index(bridge_name).map_err(|_| RouteError::BridgeNotReady)?;
let preferred = parse_network(CONTAINER_SUBNET)?;
let split = [
parse_network(CONTAINER_SPLIT_SUBNETS[0])?,
parse_network(CONTAINER_SPLIT_SUBNETS[1])?,
];
let query = |network| {
arcbox_route::get(network)
.map(|route| classify_route(route.as_ref(), bridge_ifindex))
.map_err(RouteError::RouteFailed)
};
Ok(RouteSnapshot {
preferred: query(preferred)?,
split: [query(split[0])?, query(split[1])?],
})
}
async fn inspect_routes(bridge_name: &str) -> Result<RouteSnapshot, RouteError> {
let bridge_name = bridge_name.to_string();
tokio::task::spawn_blocking(move || inspect_routes_sync(&bridge_name))
.await
.map_err(|error| RouteError::RouteFailed(format!("route check task failed: {error}")))?
}
async fn add_route(client: &Client, subnet: &str, bridge_name: &str) -> Result<bool, RouteError> {
match client.route_add(subnet, bridge_name).await {
Ok(()) => Ok(true),
Err(ClientError::Helper(HelperError::RouteConflict { .. })) => Ok(false),
Err(error) => Err(error.into()),
}
}
async fn ensure_split_routes(
bridge_name: &str,
mut snapshot: RouteSnapshot,
) -> Result<(), RouteError> {
if let Some(index) = snapshot
.split
.iter()
.position(|state| *state == ExactRouteState::External)
{
return Err(RouteError::RouteConflict {
subnet: CONTAINER_SPLIT_SUBNETS[index].to_string(),
});
}
if snapshot
.split
.iter()
.all(|state| *state == ExactRouteState::Owned)
{
return Ok(());
}
let client = Client::connect().await?;
for (index, subnet) in CONTAINER_SPLIT_SUBNETS.iter().enumerate() {
if snapshot.split[index] == ExactRouteState::Owned {
continue;
}
if add_route(&client, subnet, bridge_name).await? {
snapshot.split[index] = ExactRouteState::Owned;
continue;
}
snapshot = inspect_routes(bridge_name).await?;
if snapshot.split[index] != ExactRouteState::Owned {
return Err(RouteError::RouteConflict {
subnet: (*subnet).to_string(),
});
}
}
Ok(())
}
async fn reconcile_with_snapshot(
bridge_name: &str,
mode: RouteMode,
mut snapshot: RouteSnapshot,
) -> Result<RouteMode, RouteError> {
match plan_reconciliation(mode, snapshot) {
ReconcileAction::Healthy(mode) => return Ok(mode),
ReconcileAction::Conflict(subnet) => {
return Err(RouteError::RouteConflict {
subnet: subnet.to_string(),
});
}
ReconcileAction::EnsureSplit => {}
ReconcileAction::AddPreferred => {
let client = Client::connect().await?;
if add_route(&client, CONTAINER_SUBNET, bridge_name).await? {
return Ok(RouteMode::Preferred);
}
snapshot = inspect_routes(bridge_name).await?;
if snapshot.preferred == ExactRouteState::Owned {
return Ok(RouteMode::Preferred);
}
}
}
ensure_split_routes(bridge_name, snapshot).await?;
tracing::info!(
preferred = CONTAINER_SUBNET,
lower = CONTAINER_SPLIT_SUBNETS[0],
upper = CONTAINER_SPLIT_SUBNETS[1],
bridge = bridge_name,
"external container route detected; switched to sticky split fallback"
);
Ok(RouteMode::SplitFallback)
}
pub async fn reconcile_route_for_bridge(
bridge_name: &str,
mode: RouteMode,
) -> Result<RouteMode, RouteError> {
let snapshot = inspect_routes(bridge_name).await?;
reconcile_with_snapshot(bridge_name, mode, snapshot).await
}
pub async fn initialize_route_for_bridge(bridge_name: &str) -> Result<RouteMode, RouteError> {
let snapshot = inspect_routes(bridge_name).await?;
reconcile_with_snapshot(bridge_name, snapshot.initial_mode(), snapshot).await
}
pub async fn container_route_matches_bridge(bridge_name: &str) -> Result<bool, RouteError> {
Ok(container_route_mode(bridge_name).await?.is_some())
}
pub async fn container_route_mode(bridge_name: &str) -> Result<Option<RouteMode>, RouteError> {
let snapshot = inspect_routes(bridge_name).await?;
if !snapshot.is_healthy() {
return Ok(None);
}
Ok(Some(snapshot.initial_mode()))
}
pub async fn repair_route_for_bridge(bridge_name: &str) -> Result<(), RouteError> {
initialize_route_for_bridge(bridge_name).await.map(|_| ())
}
async fn ensure_route(bridge_mac: &str) -> Result<(), RouteError> {
let mac = bridge_mac.to_string();
let bridge = tokio::task::spawn_blocking(move || bridge_discovery::resolve_bridge_by_mac(&mac))
.await
.unwrap_or(None)
.ok_or(RouteError::BridgeNotReady)?;
repair_route_for_bridge(&bridge.name).await?;
tracing::info!(
bridge = %bridge.name,
%bridge_mac,
"container route ensured"
);
Ok(())
}
pub async fn ensure_route_with_retry(bridge_mac: &str) -> Result<(), RouteError> {
for attempt in 1..=MAX_ROUTE_ATTEMPTS {
match ensure_route(bridge_mac).await {
Ok(()) => return Ok(()),
Err(ref e) if attempt < MAX_ROUTE_ATTEMPTS => {
tracing::debug!(
attempt,
max_attempts = MAX_ROUTE_ATTEMPTS,
error = %e,
"route install failed, retrying"
);
tokio::time::sleep(ROUTE_RETRY_INTERVAL).await;
}
Err(e) => {
tracing::warn!(
attempt,
error = %e,
"route install failed after all attempts"
);
return Err(e);
}
}
}
unreachable!()
}
#[cfg(all(feature = "vmnet", target_os = "macos"))]
pub async fn ensure_route_for_bridge(bridge_name: &str) -> Result<(), RouteError> {
for attempt in 1..=2 {
match repair_route_for_bridge(bridge_name).await {
Ok(()) => {
tracing::info!(
bridge = bridge_name,
"container route ensured (vmnet direct)"
);
return Ok(());
}
Err(ref e) if attempt < 2 => {
tracing::debug!(attempt, error = %e, "vmnet route install retry");
tokio::time::sleep(ROUTE_RETRY_INTERVAL).await;
}
Err(e) => return Err(e),
}
}
unreachable!()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn correct_interface_route_matches() {
let route = RouteInfo {
ifindex: 26,
flags: libc::RTF_UP | libc::RTF_STATIC,
};
assert!(route_matches_bridge(Some(&route), 26));
}
#[test]
fn gateway_route_on_expected_interface_does_not_match() {
let route = RouteInfo {
ifindex: 26,
flags: libc::RTF_UP | libc::RTF_STATIC | libc::RTF_GATEWAY,
};
assert!(!route_matches_bridge(Some(&route), 26));
}
#[test]
fn absent_or_wrong_interface_route_does_not_match() {
let route = RouteInfo {
ifindex: 7,
flags: libc::RTF_UP | libc::RTF_STATIC,
};
assert!(!route_matches_bridge(None, 26));
assert!(!route_matches_bridge(Some(&route), 26));
}
#[test]
fn external_preferred_route_selects_split_fallback() {
let snapshot = RouteSnapshot {
preferred: ExactRouteState::External,
split: [ExactRouteState::Missing; 2],
};
assert_eq!(snapshot.initial_mode(), RouteMode::SplitFallback);
assert!(!snapshot.is_healthy());
}
#[test]
fn complete_split_routes_restore_sticky_mode_after_restart() {
let snapshot = RouteSnapshot {
preferred: ExactRouteState::Missing,
split: [ExactRouteState::Owned; 2],
};
assert_eq!(snapshot.initial_mode(), RouteMode::SplitFallback);
assert!(snapshot.is_healthy());
}
#[test]
fn owned_preferred_route_is_selected_without_split_evidence() {
let snapshot = RouteSnapshot {
preferred: ExactRouteState::Owned,
split: [ExactRouteState::Missing; 2],
};
assert_eq!(snapshot.initial_mode(), RouteMode::Preferred);
assert!(snapshot.is_healthy());
}
#[test]
fn external_more_specific_route_makes_preferred_shape_unhealthy() {
let snapshot = RouteSnapshot {
preferred: ExactRouteState::Owned,
split: [ExactRouteState::External, ExactRouteState::Missing],
};
assert!(!snapshot.is_healthy());
}
#[test]
fn healthy_preferred_mode_is_a_noop() {
let snapshot = RouteSnapshot {
preferred: ExactRouteState::Owned,
split: [ExactRouteState::Missing; 2],
};
assert_eq!(
plan_reconciliation(RouteMode::Preferred, snapshot),
ReconcileAction::Healthy(RouteMode::Preferred)
);
}
#[test]
fn external_preferred_route_plans_split_fallback() {
let snapshot = RouteSnapshot {
preferred: ExactRouteState::External,
split: [ExactRouteState::Missing; 2],
};
assert_eq!(
plan_reconciliation(RouteMode::Preferred, snapshot),
ReconcileAction::EnsureSplit
);
}
#[test]
fn split_mode_never_reverts_to_preferred() {
let snapshot = RouteSnapshot {
preferred: ExactRouteState::Missing,
split: [ExactRouteState::Owned; 2],
};
assert_eq!(
plan_reconciliation(RouteMode::SplitFallback, snapshot),
ReconcileAction::Healthy(RouteMode::SplitFallback)
);
}
#[test]
fn external_split_route_is_never_replaced() {
let snapshot = RouteSnapshot {
preferred: ExactRouteState::External,
split: [ExactRouteState::Owned, ExactRouteState::External],
};
assert_eq!(
plan_reconciliation(RouteMode::SplitFallback, snapshot),
ReconcileAction::Conflict("172.24.0.0/13")
);
}
}