use std::{net::IpAddr, time::Duration};
use super::{
diff::{ConfigDiff, LinkChanges, compute_diff},
types::{
BondMode, DeclaredAddress, DeclaredLink, DeclaredLinkType, DeclaredQdisc,
DeclaredQdiscType, DeclaredRoute, DeclaredRouteType, MacvlanMode, NetworkConfig,
QdiscParent,
},
};
use crate::netlink::{
addr::{Ipv4Address, Ipv6Address},
connection::Connection,
error::{Error, Result},
link::{BondLink, BridgeLink, DummyLink, IfbLink, MacvlanLink, VethLink, VlanLink, VxlanLink},
protocol::Route,
route::{Ipv4Route, Ipv6Route},
tc::{
ClsactConfig, FqCodelConfig, HtbQdiscConfig, IngressConfig, NetemConfig, PrioConfig,
SfqConfig, TbfConfig,
},
};
#[derive(Debug, Clone, Default)]
pub struct ApplyOptions {
pub dry_run: bool,
pub continue_on_error: bool,
pub purge: bool,
}
#[derive(Debug, Default)]
pub struct ApplyResult {
pub changes_made: usize,
pub errors: Vec<ApplyError>,
pub summary: Vec<String>,
}
impl ApplyResult {
pub fn is_success(&self) -> bool {
self.errors.is_empty()
}
pub fn summary_text(&self) -> String {
if self.summary.is_empty() {
"No changes made".to_string()
} else {
self.summary.join("\n")
}
}
}
#[derive(Debug)]
pub struct ApplyError {
pub operation: String,
pub error: Error,
}
impl std::fmt::Display for ApplyError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}: {}", self.operation, self.error)
}
}
pub async fn apply_config(
config: &NetworkConfig,
conn: &Connection<Route>,
options: ApplyOptions,
) -> Result<ApplyResult> {
let diff = compute_diff(config, conn).await?;
apply_diff(&diff, conn, options).await
}
pub async fn apply_diff(
diff: &ConfigDiff,
conn: &Connection<Route>,
options: ApplyOptions,
) -> Result<ApplyResult> {
let mut result = ApplyResult::default();
if diff.is_empty() {
return Ok(result);
}
for link in &diff.links_to_add {
let op = format!("create link {}", link.name);
if options.dry_run {
result.summary.push(format!("Would {}", op));
result.changes_made += 1;
} else {
match create_link(conn, link).await {
Ok(()) => {
result.summary.push(format!("Created link {}", link.name));
result.changes_made += 1;
}
Err(e) => {
if options.continue_on_error {
result.errors.push(ApplyError {
operation: op,
error: e,
});
} else {
return Err(e);
}
}
}
}
}
for (name, changes) in &diff.links_to_modify {
let op = format!("modify link {} ({})", name, changes.summary());
if options.dry_run {
result.summary.push(format!("Would {}", op));
result.changes_made += 1;
} else {
match modify_link(conn, name, changes).await {
Ok(()) => {
result
.summary
.push(format!("Modified link {} ({})", name, changes.summary()));
result.changes_made += 1;
}
Err(e) => {
if options.continue_on_error {
result.errors.push(ApplyError {
operation: op,
error: e,
});
} else {
return Err(e);
}
}
}
}
}
for addr in &diff.addresses_to_add {
let op = format!(
"add address {}/{} on {}",
addr.address, addr.prefix_len, addr.dev
);
if options.dry_run {
result.summary.push(format!("Would {}", op));
result.changes_made += 1;
} else {
match add_address(conn, addr).await {
Ok(()) => {
result.summary.push(format!(
"Added address {}/{} on {}",
addr.address, addr.prefix_len, addr.dev
));
result.changes_made += 1;
}
Err(e) => {
if options.continue_on_error {
result.errors.push(ApplyError {
operation: op,
error: e,
});
} else {
return Err(e);
}
}
}
}
}
for route in &diff.routes_to_add {
let op = format!("add route {}/{}", route.destination, route.prefix_len);
if options.dry_run {
result.summary.push(format!("Would {}", op));
result.changes_made += 1;
} else {
match add_route(conn, route).await {
Ok(()) => {
result.summary.push(format!(
"Added route {}/{}",
route.destination, route.prefix_len
));
result.changes_made += 1;
}
Err(e) => {
if options.continue_on_error {
result.errors.push(ApplyError {
operation: op,
error: e,
});
} else {
return Err(e);
}
}
}
}
}
for qdisc in &diff.qdiscs_to_replace {
let op = format!("replace qdisc {} on {}", qdisc.qdisc_type.kind(), qdisc.dev);
if options.dry_run {
result.summary.push(format!("Would {}", op));
result.changes_made += 1;
} else {
match replace_qdisc(conn, qdisc).await {
Ok(()) => {
result.summary.push(format!(
"Replaced qdisc {} on {}",
qdisc.qdisc_type.kind(),
qdisc.dev
));
result.changes_made += 1;
}
Err(e) => {
if options.continue_on_error {
result.errors.push(ApplyError {
operation: op,
error: e,
});
} else {
return Err(e);
}
}
}
}
}
for qdisc in &diff.qdiscs_to_add {
let op = format!("add qdisc {} on {}", qdisc.qdisc_type.kind(), qdisc.dev);
if options.dry_run {
result.summary.push(format!("Would {}", op));
result.changes_made += 1;
} else {
match add_qdisc(conn, qdisc).await {
Ok(()) => {
result.summary.push(format!(
"Added qdisc {} on {}",
qdisc.qdisc_type.kind(),
qdisc.dev
));
result.changes_made += 1;
}
Err(e) => {
if options.continue_on_error {
result.errors.push(ApplyError {
operation: op,
error: e,
});
} else {
return Err(e);
}
}
}
}
}
if options.purge {
for (dev, parent) in &diff.qdiscs_to_remove {
let op = format!("remove qdisc on {} ({:?})", dev, parent);
if options.dry_run {
result.summary.push(format!("Would {}", op));
result.changes_made += 1;
} else {
let parent_handle = match parent {
QdiscParent::Root => crate::TcHandle::ROOT,
QdiscParent::Ingress => crate::TcHandle::INGRESS,
};
match conn.del_qdisc(dev, parent_handle).await {
Ok(()) => {
result.summary.push(format!("Removed qdisc on {}", dev));
result.changes_made += 1;
}
Err(e) if e.is_not_found() => {
}
Err(e) => {
if options.continue_on_error {
result.errors.push(ApplyError {
operation: op,
error: e,
});
} else {
return Err(e);
}
}
}
}
}
for (dst, prefix_len, table) in &diff.routes_to_remove {
let op = format!("remove route {}/{}", dst, prefix_len);
if options.dry_run {
result.summary.push(format!("Would {}", op));
result.changes_made += 1;
} else {
match remove_route(conn, *dst, *prefix_len, *table).await {
Ok(()) => {
result
.summary
.push(format!("Removed route {}/{}", dst, prefix_len));
result.changes_made += 1;
}
Err(e) if e.is_not_found() => {
}
Err(e) => {
if options.continue_on_error {
result.errors.push(ApplyError {
operation: op,
error: e,
});
} else {
return Err(e);
}
}
}
}
}
for (dev, addr, prefix_len) in &diff.addresses_to_remove {
let op = format!("remove address {}/{} from {}", addr, prefix_len, dev);
if options.dry_run {
result.summary.push(format!("Would {}", op));
result.changes_made += 1;
} else {
match remove_address(conn, dev, *addr, *prefix_len).await {
Ok(()) => {
result.summary.push(format!(
"Removed address {}/{} from {}",
addr, prefix_len, dev
));
result.changes_made += 1;
}
Err(e) if e.is_not_found() => {
}
Err(e) => {
if options.continue_on_error {
result.errors.push(ApplyError {
operation: op,
error: e,
});
} else {
return Err(e);
}
}
}
}
}
for name in &diff.links_to_remove {
let op = format!("remove link {}", name);
if options.dry_run {
result.summary.push(format!("Would {}", op));
result.changes_made += 1;
} else {
match conn.del_link(name).await {
Ok(()) => {
result.summary.push(format!("Removed link {}", name));
result.changes_made += 1;
}
Err(e) if e.is_not_found() => {
}
Err(e) => {
if options.continue_on_error {
result.errors.push(ApplyError {
operation: op,
error: e,
});
} else {
return Err(e);
}
}
}
}
}
}
Ok(result)
}
async fn create_link(conn: &Connection<Route>, link: &DeclaredLink) -> Result<()> {
match &link.link_type {
DeclaredLinkType::Dummy => {
let mut config = DummyLink::new(&link.name);
if let Some(mtu) = link.mtu {
config = config.mtu(mtu);
}
if let Some(addr) = link.address {
config = config.address(addr);
}
conn.add_link(config).await?;
}
DeclaredLinkType::Veth { peer } => {
let mut config = VethLink::new(&link.name, peer);
if let Some(mtu) = link.mtu {
config = config.mtu(mtu);
}
if let Some(addr) = link.address {
config = config.address(addr);
}
conn.add_link(config).await?;
}
DeclaredLinkType::Bridge => {
let mut config = BridgeLink::new(&link.name);
if let Some(mtu) = link.mtu {
config = config.mtu(mtu);
}
if let Some(addr) = link.address {
config = config.address(addr);
}
conn.add_link(config).await?;
}
DeclaredLinkType::Vlan { parent, vlan_id } => {
let mut config = VlanLink::new(&link.name, parent, *vlan_id);
if let Some(mtu) = link.mtu {
config = config.mtu(mtu);
}
conn.add_link(config).await?;
}
DeclaredLinkType::Vxlan { vni, remote } => {
let mut config = VxlanLink::new(&link.name, *vni);
if let Some(IpAddr::V4(remote_v4)) = remote {
config = config.remote(*remote_v4);
}
conn.add_link(config).await?;
}
DeclaredLinkType::Macvlan { parent, mode } => {
let mut config = MacvlanLink::new(&link.name, parent);
config = config.mode(convert_macvlan_mode(*mode));
if let Some(addr) = link.address {
config = config.address(addr);
}
conn.add_link(config).await?;
}
DeclaredLinkType::Bond {
mode,
miimon,
xmit_hash_policy,
min_links,
} => {
let mut config = BondLink::new(&link.name).mode(convert_bond_mode(*mode));
if let Some(ms) = miimon {
config = config.miimon(*ms);
}
if let Some(policy) = xmit_hash_policy
&& let Ok(p) = crate::netlink::link::XmitHashPolicy::try_from(*policy)
{
config = config.xmit_hash_policy(p);
}
if let Some(count) = min_links {
config = config.min_links(*count);
}
if let Some(mtu) = link.mtu {
config = config.mtu(mtu);
}
if let Some(addr) = link.address {
config = config.address(addr);
}
conn.add_link(config).await?;
}
DeclaredLinkType::Ifb => {
let config = IfbLink::new(&link.name);
conn.add_link(config).await?;
}
DeclaredLinkType::Physical => {
}
}
if link.state == super::types::LinkState::Up {
conn.set_link_up(&link.name).await?;
}
if let Some(master) = &link.master {
conn.set_link_master(&link.name, master).await?;
}
Ok(())
}
async fn modify_link(conn: &Connection<Route>, name: &str, changes: &LinkChanges) -> Result<()> {
if changes.set_up {
conn.set_link_up(name).await?;
}
if changes.set_down {
conn.set_link_down(name).await?;
}
if let Some(mtu) = changes.set_mtu {
conn.set_link_mtu(name, mtu).await?;
}
if let Some(master) = &changes.set_master {
conn.set_link_master(name, master).await?;
}
if changes.unset_master {
conn.set_link_nomaster(name).await?;
}
Ok(())
}
async fn add_address(conn: &Connection<Route>, addr: &DeclaredAddress) -> Result<()> {
match addr.address {
IpAddr::V4(v4) => {
let config = Ipv4Address::new(&addr.dev, v4, addr.prefix_len);
conn.add_address(config).await
}
IpAddr::V6(v6) => {
let config = Ipv6Address::new(&addr.dev, v6, addr.prefix_len);
conn.add_address(config).await
}
}
}
async fn remove_address(
conn: &Connection<Route>,
dev: &str,
addr: IpAddr,
prefix_len: u8,
) -> Result<()> {
conn.del_address(dev, addr, prefix_len).await
}
async fn add_route(conn: &Connection<Route>, route: &DeclaredRoute) -> Result<()> {
match route.destination {
IpAddr::V4(dst) => {
let mut config = Ipv4Route::from_addr(dst, route.prefix_len);
if let Some(IpAddr::V4(gw)) = route.gateway {
config = config.gateway(gw);
}
if let Some(dev) = &route.dev {
config = config.dev(dev);
}
if let Some(metric) = route.metric {
config = config.metric(metric);
}
if let Some(table) = route.table {
config = config.table(table);
}
config = match route.route_type {
DeclaredRouteType::Unicast => config,
DeclaredRouteType::Blackhole => {
config.route_type(crate::netlink::types::route::RouteType::Blackhole)
}
DeclaredRouteType::Unreachable => {
config.route_type(crate::netlink::types::route::RouteType::Unreachable)
}
DeclaredRouteType::Prohibit => {
config.route_type(crate::netlink::types::route::RouteType::Prohibit)
}
};
conn.add_route(config).await
}
IpAddr::V6(dst) => {
let mut config = Ipv6Route::from_addr(dst, route.prefix_len);
if let Some(IpAddr::V6(gw)) = route.gateway {
config = config.gateway(gw);
}
if let Some(dev) = &route.dev {
config = config.dev(dev);
}
if let Some(metric) = route.metric {
config = config.metric(metric);
}
if let Some(table) = route.table {
config = config.table(table);
}
config = match route.route_type {
DeclaredRouteType::Unicast => config,
DeclaredRouteType::Blackhole => {
config.route_type(crate::netlink::types::route::RouteType::Blackhole)
}
DeclaredRouteType::Unreachable => {
config.route_type(crate::netlink::types::route::RouteType::Unreachable)
}
DeclaredRouteType::Prohibit => {
config.route_type(crate::netlink::types::route::RouteType::Prohibit)
}
};
conn.add_route(config).await
}
}
}
async fn remove_route(
conn: &Connection<Route>,
dst: IpAddr,
prefix_len: u8,
_table: u32,
) -> Result<()> {
match dst {
IpAddr::V4(v4) => {
let route = Ipv4Route::from_addr(v4, prefix_len);
conn.del_route(route).await
}
IpAddr::V6(v6) => {
let route = Ipv6Route::from_addr(v6, prefix_len);
conn.del_route(route).await
}
}
}
async fn add_qdisc(conn: &Connection<Route>, qdisc: &DeclaredQdisc) -> Result<()> {
match &qdisc.qdisc_type {
DeclaredQdiscType::Netem {
delay_us,
jitter_us,
loss_percent,
limit,
} => {
let mut config = NetemConfig::new();
if let Some(delay) = delay_us {
config = config.delay(Duration::from_micros(*delay as u64));
}
if let Some(jitter) = jitter_us {
config = config.jitter(Duration::from_micros(*jitter as u64));
}
if let Some(loss) = loss_percent {
config = config.loss(crate::util::Percent::new(*loss));
}
if let Some(lim) = limit {
config = config.limit(*lim);
}
conn.add_qdisc(&qdisc.dev, config.build()).await
}
DeclaredQdiscType::Htb { default_class } => {
let config = HtbQdiscConfig::new().default_class(*default_class);
conn.add_qdisc_full(
&qdisc.dev,
crate::TcHandle::ROOT,
Some(crate::TcHandle::major_only(1)),
config,
)
.await
}
DeclaredQdiscType::FqCodel {
limit,
target_us,
interval_us,
} => {
let mut config = FqCodelConfig::new();
if let Some(lim) = limit {
config = config.limit(*lim);
}
if let Some(target) = target_us {
config = config.target(Duration::from_micros(*target as u64));
}
if let Some(interval) = interval_us {
config = config.interval(Duration::from_micros(*interval as u64));
}
conn.add_qdisc(&qdisc.dev, config).await
}
DeclaredQdiscType::Tbf {
rate_bps,
burst_bytes,
limit_bytes,
} => {
let mut config = TbfConfig::new()
.rate(crate::util::Rate::bytes_per_sec(*rate_bps))
.burst(crate::util::Bytes::new(*burst_bytes as u64));
if let Some(limit) = limit_bytes {
config = config.limit(crate::util::Bytes::new(*limit as u64));
}
conn.add_qdisc(&qdisc.dev, config).await
}
DeclaredQdiscType::Sfq { perturb_secs } => {
let mut config = SfqConfig::new();
if let Some(perturb) = perturb_secs {
config = config.perturb(*perturb as i32);
}
conn.add_qdisc(&qdisc.dev, config).await
}
DeclaredQdiscType::Prio { bands } => {
let mut config = PrioConfig::new();
if let Some(b) = bands {
config = config.bands(*b as i32);
}
conn.add_qdisc(&qdisc.dev, config).await
}
DeclaredQdiscType::Ingress => conn.add_qdisc(&qdisc.dev, IngressConfig::new()).await,
DeclaredQdiscType::Clsact => conn.add_qdisc(&qdisc.dev, ClsactConfig::new()).await,
}
}
async fn replace_qdisc(conn: &Connection<Route>, qdisc: &DeclaredQdisc) -> Result<()> {
let parent_handle = match qdisc.parent {
QdiscParent::Root => crate::TcHandle::ROOT,
QdiscParent::Ingress => crate::TcHandle::INGRESS,
};
match conn.del_qdisc(&qdisc.dev, parent_handle).await {
Ok(()) => {}
Err(e) if e.is_not_found() => {}
Err(e) => return Err(e),
}
add_qdisc(conn, qdisc).await
}
fn convert_macvlan_mode(mode: MacvlanMode) -> crate::netlink::link::MacvlanMode {
match mode {
MacvlanMode::Private => crate::netlink::link::MacvlanMode::Private,
MacvlanMode::Vepa => crate::netlink::link::MacvlanMode::Vepa,
MacvlanMode::Bridge => crate::netlink::link::MacvlanMode::Bridge,
MacvlanMode::Passthru => crate::netlink::link::MacvlanMode::Passthru,
MacvlanMode::Source => crate::netlink::link::MacvlanMode::Source,
}
}
fn convert_bond_mode(mode: BondMode) -> crate::netlink::link::BondMode {
match mode {
BondMode::BalanceRr => crate::netlink::link::BondMode::BalanceRr,
BondMode::ActiveBackup => crate::netlink::link::BondMode::ActiveBackup,
BondMode::BalanceXor => crate::netlink::link::BondMode::BalanceXor,
BondMode::Broadcast => crate::netlink::link::BondMode::Broadcast,
BondMode::Ieee802_3ad => crate::netlink::link::BondMode::Lacp,
BondMode::BalanceTlb => crate::netlink::link::BondMode::BalanceTlb,
BondMode::BalanceAlb => crate::netlink::link::BondMode::BalanceAlb,
}
}