use std::{
collections::{HashMap, HashSet},
net::IpAddr,
};
use super::types::{
DeclaredAddress, DeclaredLink, DeclaredLinkType, DeclaredQdisc, DeclaredRoute, LinkState,
NetworkConfig, QdiscParent,
};
use crate::netlink::{
connection::Connection,
error::Result,
messages::{AddressMessage, LinkMessage, RouteMessage, TcMessage},
protocol::Route,
types::{link::OperState, route::RouteType},
};
#[derive(Debug, Default)]
pub struct ConfigDiff {
pub links_to_add: Vec<DeclaredLink>,
pub links_to_remove: Vec<String>,
pub links_to_modify: Vec<(String, LinkChanges)>,
pub addresses_to_add: Vec<DeclaredAddress>,
pub addresses_to_remove: Vec<(String, IpAddr, u8)>,
pub routes_to_add: Vec<DeclaredRoute>,
pub routes_to_remove: Vec<(IpAddr, u8, u32)>,
pub qdiscs_to_add: Vec<DeclaredQdisc>,
pub qdiscs_to_remove: Vec<(String, QdiscParent)>,
pub qdiscs_to_replace: Vec<DeclaredQdisc>,
}
impl ConfigDiff {
pub fn is_empty(&self) -> bool {
self.links_to_add.is_empty()
&& self.links_to_remove.is_empty()
&& self.links_to_modify.is_empty()
&& self.addresses_to_add.is_empty()
&& self.addresses_to_remove.is_empty()
&& self.routes_to_add.is_empty()
&& self.routes_to_remove.is_empty()
&& self.qdiscs_to_add.is_empty()
&& self.qdiscs_to_remove.is_empty()
&& self.qdiscs_to_replace.is_empty()
}
pub fn change_count(&self) -> usize {
self.links_to_add.len()
+ self.links_to_remove.len()
+ self.links_to_modify.len()
+ self.addresses_to_add.len()
+ self.addresses_to_remove.len()
+ self.routes_to_add.len()
+ self.routes_to_remove.len()
+ self.qdiscs_to_add.len()
+ self.qdiscs_to_remove.len()
+ self.qdiscs_to_replace.len()
}
pub fn summary(&self) -> String {
let mut lines = Vec::new();
for link in &self.links_to_add {
lines.push(format!(
"+ link {} ({})",
link.name,
link.link_type.kind().unwrap_or("physical")
));
}
for name in &self.links_to_remove {
lines.push(format!("- link {}", name));
}
for (name, changes) in &self.links_to_modify {
lines.push(format!("~ link {} ({})", name, changes.summary()));
}
for addr in &self.addresses_to_add {
lines.push(format!(
"+ address {}/{} on {}",
addr.address, addr.prefix_len, addr.dev
));
}
for (dev, addr, prefix) in &self.addresses_to_remove {
lines.push(format!("- address {}/{} on {}", addr, prefix, dev));
}
for route in &self.routes_to_add {
let via = route
.gateway
.map(|g| format!(" via {}", g))
.unwrap_or_default();
let dev = route
.dev
.as_ref()
.map(|d| format!(" dev {}", d))
.unwrap_or_default();
lines.push(format!(
"+ route {}/{}{}{}",
route.destination, route.prefix_len, via, dev
));
}
for (dst, prefix, table) in &self.routes_to_remove {
let table_str = if *table != 254 {
format!(" table {}", table)
} else {
String::new()
};
lines.push(format!("- route {}/{}{}", dst, prefix, table_str));
}
for qdisc in &self.qdiscs_to_add {
lines.push(format!(
"+ qdisc {} on {} ({:?})",
qdisc.qdisc_type.kind(),
qdisc.dev,
qdisc.parent
));
}
for (dev, parent) in &self.qdiscs_to_remove {
lines.push(format!("- qdisc on {} ({:?})", dev, parent));
}
for qdisc in &self.qdiscs_to_replace {
lines.push(format!(
"~ qdisc {} on {} ({:?})",
qdisc.qdisc_type.kind(),
qdisc.dev,
qdisc.parent
));
}
if lines.is_empty() {
"No changes needed".to_string()
} else {
lines.join("\n")
}
}
}
#[derive(Debug, Default)]
pub struct LinkChanges {
pub set_up: bool,
pub set_down: bool,
pub set_mtu: Option<u32>,
pub set_master: Option<String>,
pub unset_master: bool,
}
impl LinkChanges {
pub fn is_empty(&self) -> bool {
!self.set_up
&& !self.set_down
&& self.set_mtu.is_none()
&& self.set_master.is_none()
&& !self.unset_master
}
pub fn summary(&self) -> String {
let mut parts: Vec<String> = Vec::new();
if self.set_up {
parts.push("up".to_string());
}
if self.set_down {
parts.push("down".to_string());
}
if let Some(mtu) = self.set_mtu {
parts.push(format!("mtu={}", mtu));
}
if let Some(master) = &self.set_master {
parts.push(format!("master={}", master));
}
if self.unset_master {
parts.push("nomaster".to_string());
}
parts.join(", ")
}
}
pub async fn compute_diff(config: &NetworkConfig, conn: &Connection<Route>) -> Result<ConfigDiff> {
let mut diff = ConfigDiff::default();
let current_links = conn.get_links().await?;
let current_addresses = conn.get_addresses().await?;
let current_routes = conn.get_routes().await?;
let current_qdiscs = conn.get_qdiscs().await?;
let link_by_name: HashMap<&str, &LinkMessage> = current_links
.iter()
.filter_map(|l| l.name.as_deref().map(|n| (n, l)))
.collect();
let ifindex_to_name: HashMap<u32, &str> = current_links
.iter()
.filter_map(|l| l.name.as_deref().map(|n| (l.ifindex(), n)))
.collect();
diff_links(config, &link_by_name, &mut diff);
diff_addresses(config, ¤t_addresses, &ifindex_to_name, &mut diff);
diff_routes(config, ¤t_routes, &ifindex_to_name, &mut diff);
diff_qdiscs(config, ¤t_qdiscs, &ifindex_to_name, &mut diff);
Ok(diff)
}
fn diff_links(
config: &NetworkConfig,
current: &HashMap<&str, &LinkMessage>,
diff: &mut ConfigDiff,
) {
let _desired_names: HashSet<&str> = config.links.iter().map(|l| l.name.as_str()).collect();
for declared in &config.links {
if let Some(existing) = current.get(declared.name.as_str()) {
let changes = compute_link_changes(declared, existing);
if !changes.is_empty() {
diff.links_to_modify.push((declared.name.clone(), changes));
}
} else {
if declared.link_type != DeclaredLinkType::Physical {
diff.links_to_add.push(declared.clone());
}
}
}
}
fn compute_link_changes(declared: &DeclaredLink, existing: &LinkMessage) -> LinkChanges {
let mut changes = LinkChanges::default();
match declared.state {
LinkState::Up => {
if existing.operstate != Some(OperState::Up) {
changes.set_up = true;
}
}
LinkState::Down => {
if existing.operstate == Some(OperState::Up) {
changes.set_down = true;
}
}
LinkState::Unchanged => {}
}
if let Some(desired_mtu) = declared.mtu
&& existing.mtu != Some(desired_mtu)
{
changes.set_mtu = Some(desired_mtu);
}
if declared.master.is_some() && existing.master.is_none() {
changes.set_master = declared.master.clone();
} else if declared.master.is_none() && existing.master.is_some() {
changes.unset_master = true;
}
changes
}
fn diff_addresses(
config: &NetworkConfig,
current: &[AddressMessage],
ifindex_to_name: &HashMap<u32, &str>,
diff: &mut ConfigDiff,
) {
let desired: HashSet<(&str, IpAddr, u8)> = config
.addresses
.iter()
.map(|a| (a.dev.as_str(), a.address, a.prefix_len))
.collect();
let current_set: HashSet<(&str, IpAddr, u8)> = current
.iter()
.filter_map(|a| {
let name = ifindex_to_name.get(&a.ifindex())?;
let addr = a.address?;
Some((*name, addr, a.prefix_len()))
})
.collect();
for declared in &config.addresses {
let key = (declared.dev.as_str(), declared.address, declared.prefix_len);
if !current_set.contains(&key) {
diff.addresses_to_add.push(declared.clone());
}
}
let _ = desired; }
fn diff_routes(
config: &NetworkConfig,
current: &[RouteMessage],
ifindex_to_name: &HashMap<u32, &str>,
diff: &mut ConfigDiff,
) {
let desired: HashSet<(IpAddr, u8, u32)> = config
.routes
.iter()
.map(|r| (r.destination, r.prefix_len, r.table.unwrap_or(254)))
.collect();
let current_set: HashSet<(IpAddr, u8, u32)> = current
.iter()
.filter(|r| {
matches!(
r.route_type(),
RouteType::Unicast
| RouteType::Blackhole
| RouteType::Unreachable
| RouteType::Prohibit
)
})
.map(|r| {
let dst = r.destination.unwrap_or_else(|| {
if r.is_ipv4() {
IpAddr::V4(std::net::Ipv4Addr::UNSPECIFIED)
} else {
IpAddr::V6(std::net::Ipv6Addr::UNSPECIFIED)
}
});
(dst, r.dst_len(), r.table_id())
})
.collect();
for declared in &config.routes {
let table = declared.table.unwrap_or(254);
let key = (declared.destination, declared.prefix_len, table);
if !current_set.contains(&key) {
diff.routes_to_add.push(declared.clone());
}
}
let _ = desired;
let _ = ifindex_to_name;
}
fn diff_qdiscs(
config: &NetworkConfig,
current: &[TcMessage],
ifindex_to_name: &HashMap<u32, &str>,
diff: &mut ConfigDiff,
) {
let mut current_root_qdisc: HashMap<&str, &TcMessage> = HashMap::new();
let mut current_ingress_qdisc: HashMap<&str, &TcMessage> = HashMap::new();
for qdisc in current {
if let Some(name) = ifindex_to_name.get(&qdisc.ifindex()) {
if qdisc.is_root() {
current_root_qdisc.insert(*name, qdisc);
} else if qdisc.is_ingress() {
current_ingress_qdisc.insert(*name, qdisc);
}
}
}
for declared in &config.qdiscs {
let current_map = match declared.parent {
QdiscParent::Root => ¤t_root_qdisc,
QdiscParent::Ingress => ¤t_ingress_qdisc,
};
if let Some(existing) = current_map.get(declared.dev.as_str()) {
let existing_kind = existing.kind().unwrap_or("");
let desired_kind = declared.qdisc_type.kind();
if existing_kind != desired_kind {
diff.qdiscs_to_replace.push(declared.clone());
}
} else {
diff.qdiscs_to_add.push(declared.clone());
}
}
}