use std::net::IpAddr;
#[derive(Debug, Clone, Default)]
pub struct NetworkConfig {
pub(crate) links: Vec<DeclaredLink>,
pub(crate) addresses: Vec<DeclaredAddress>,
pub(crate) routes: Vec<DeclaredRoute>,
pub(crate) qdiscs: Vec<DeclaredQdisc>,
}
impl NetworkConfig {
pub fn new() -> Self {
Self::default()
}
pub fn link(mut self, name: &str, f: impl FnOnce(LinkBuilder) -> LinkBuilder) -> Self {
let builder = f(LinkBuilder::new(name));
self.links.push(builder.build());
self
}
pub fn address(mut self, dev: &str, addr: &str) -> Result<Self, AddressParseError> {
let declared = DeclaredAddress::parse(dev, addr)?;
self.addresses.push(declared);
Ok(self)
}
pub fn route(
mut self,
dst: &str,
f: impl FnOnce(RouteBuilder) -> RouteBuilder,
) -> Result<Self, RouteParseError> {
let builder = f(RouteBuilder::new(dst)?);
self.routes.push(builder.build());
Ok(self)
}
pub fn qdisc(mut self, dev: &str, f: impl FnOnce(QdiscBuilder) -> QdiscBuilder) -> Self {
let builder = f(QdiscBuilder::new(dev));
self.qdiscs.push(builder.build());
self
}
pub fn links(&self) -> &[DeclaredLink] {
&self.links
}
pub fn addresses(&self) -> &[DeclaredAddress] {
&self.addresses
}
pub fn routes(&self) -> &[DeclaredRoute] {
&self.routes
}
pub fn qdiscs(&self) -> &[DeclaredQdisc] {
&self.qdiscs
}
}
#[derive(Debug, Clone)]
pub struct DeclaredLink {
pub(crate) name: String,
pub(crate) link_type: DeclaredLinkType,
pub(crate) state: LinkState,
pub(crate) mtu: Option<u32>,
pub(crate) master: Option<String>,
pub(crate) address: Option<[u8; 6]>,
}
impl DeclaredLink {
pub fn name(&self) -> &str {
&self.name
}
pub fn link_type(&self) -> &DeclaredLinkType {
&self.link_type
}
pub fn state(&self) -> LinkState {
self.state
}
pub fn mtu(&self) -> Option<u32> {
self.mtu
}
pub fn master(&self) -> Option<&str> {
self.master.as_deref()
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub enum DeclaredLinkType {
Dummy,
Veth { peer: String },
Bridge,
Vlan { parent: String, vlan_id: u16 },
Vxlan { vni: u32, remote: Option<IpAddr> },
Macvlan { parent: String, mode: MacvlanMode },
Bond {
mode: BondMode,
miimon: Option<u32>,
xmit_hash_policy: Option<u8>,
min_links: Option<u32>,
},
Ifb,
Physical,
}
impl DeclaredLinkType {
pub fn kind(&self) -> Option<&str> {
match self {
Self::Dummy => Some("dummy"),
Self::Veth { .. } => Some("veth"),
Self::Bridge => Some("bridge"),
Self::Vlan { .. } => Some("vlan"),
Self::Vxlan { .. } => Some("vxlan"),
Self::Macvlan { .. } => Some("macvlan"),
Self::Bond { .. } => Some("bond"),
Self::Ifb => Some("ifb"),
Self::Physical => None,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
#[non_exhaustive]
pub enum LinkState {
Up,
#[default]
Down,
Unchanged,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
#[non_exhaustive]
pub enum MacvlanMode {
Private,
Vepa,
#[default]
Bridge,
Passthru,
Source,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
#[non_exhaustive]
pub enum BondMode {
#[default]
BalanceRr,
ActiveBackup,
BalanceXor,
Broadcast,
Ieee802_3ad,
BalanceTlb,
BalanceAlb,
}
#[derive(Debug)]
#[must_use = "builders do nothing unless used"]
pub struct LinkBuilder {
name: String,
link_type: DeclaredLinkType,
state: LinkState,
mtu: Option<u32>,
master: Option<String>,
address: Option<[u8; 6]>,
}
impl LinkBuilder {
fn new(name: &str) -> Self {
Self {
name: name.to_string(),
link_type: DeclaredLinkType::Physical,
state: LinkState::Unchanged,
mtu: None,
master: None,
address: None,
}
}
pub fn dummy(mut self) -> Self {
self.link_type = DeclaredLinkType::Dummy;
self
}
pub fn veth(mut self, peer: &str) -> Self {
self.link_type = DeclaredLinkType::Veth {
peer: peer.to_string(),
};
self
}
pub fn bridge(mut self) -> Self {
self.link_type = DeclaredLinkType::Bridge;
self
}
pub fn vlan(mut self, parent: &str, vlan_id: u16) -> Self {
self.link_type = DeclaredLinkType::Vlan {
parent: parent.to_string(),
vlan_id,
};
self
}
pub fn vxlan(mut self, vni: u32) -> Self {
self.link_type = DeclaredLinkType::Vxlan { vni, remote: None };
self
}
pub fn vxlan_remote(mut self, remote: IpAddr) -> Self {
if let DeclaredLinkType::Vxlan { vni, .. } = self.link_type {
self.link_type = DeclaredLinkType::Vxlan {
vni,
remote: Some(remote),
};
}
self
}
pub fn macvlan(mut self, parent: &str) -> Self {
self.link_type = DeclaredLinkType::Macvlan {
parent: parent.to_string(),
mode: MacvlanMode::default(),
};
self
}
pub fn macvlan_mode(mut self, mode: MacvlanMode) -> Self {
if let DeclaredLinkType::Macvlan { parent, .. } = &self.link_type {
self.link_type = DeclaredLinkType::Macvlan {
parent: parent.clone(),
mode,
};
}
self
}
pub fn bond(mut self) -> Self {
self.link_type = DeclaredLinkType::Bond {
mode: BondMode::default(),
miimon: None,
xmit_hash_policy: None,
min_links: None,
};
self
}
pub fn bond_mode(mut self, mode: BondMode) -> Self {
if let DeclaredLinkType::Bond {
mode: ref mut m, ..
} = self.link_type
{
*m = mode;
}
self
}
pub fn miimon(mut self, ms: u32) -> Self {
if let DeclaredLinkType::Bond { miimon, .. } = &mut self.link_type {
*miimon = Some(ms);
}
self
}
pub fn xmit_hash_policy(mut self, policy: u8) -> Self {
if let DeclaredLinkType::Bond {
xmit_hash_policy, ..
} = &mut self.link_type
{
*xmit_hash_policy = Some(policy);
}
self
}
pub fn min_links(mut self, count: u32) -> Self {
if let DeclaredLinkType::Bond { min_links, .. } = &mut self.link_type {
*min_links = Some(count);
}
self
}
pub fn ifb(mut self) -> Self {
self.link_type = DeclaredLinkType::Ifb;
self
}
pub fn up(mut self) -> Self {
self.state = LinkState::Up;
self
}
pub fn down(mut self) -> Self {
self.state = LinkState::Down;
self
}
pub fn mtu(mut self, mtu: u32) -> Self {
self.mtu = Some(mtu);
self
}
pub fn master(mut self, master: &str) -> Self {
self.master = Some(master.to_string());
self
}
pub fn address(mut self, addr: [u8; 6]) -> Self {
self.address = Some(addr);
self
}
fn build(self) -> DeclaredLink {
DeclaredLink {
name: self.name,
link_type: self.link_type,
state: self.state,
mtu: self.mtu,
master: self.master,
address: self.address,
}
}
}
#[derive(Debug, Clone)]
pub struct DeclaredAddress {
pub(crate) dev: String,
pub(crate) address: IpAddr,
pub(crate) prefix_len: u8,
}
impl DeclaredAddress {
pub fn parse(dev: &str, addr: &str) -> Result<Self, AddressParseError> {
let (ip_str, prefix_str) = addr
.split_once('/')
.ok_or_else(|| AddressParseError::MissingPrefix(addr.to_string()))?;
let address: IpAddr = ip_str
.parse()
.map_err(|_| AddressParseError::InvalidAddress(ip_str.to_string()))?;
let prefix_len: u8 = prefix_str
.parse()
.map_err(|_| AddressParseError::InvalidPrefix(prefix_str.to_string()))?;
let max_prefix = if address.is_ipv4() { 32 } else { 128 };
if prefix_len > max_prefix {
return Err(AddressParseError::PrefixTooLarge {
prefix: prefix_len,
max: max_prefix,
});
}
Ok(Self {
dev: dev.to_string(),
address,
prefix_len,
})
}
pub fn dev(&self) -> &str {
&self.dev
}
pub fn address(&self) -> IpAddr {
self.address
}
pub fn prefix_len(&self) -> u8 {
self.prefix_len
}
pub fn is_ipv4(&self) -> bool {
self.address.is_ipv4()
}
pub fn is_ipv6(&self) -> bool {
self.address.is_ipv6()
}
}
#[derive(Debug, Clone, thiserror::Error)]
#[non_exhaustive]
pub enum AddressParseError {
#[error("address missing prefix: {0} (expected format: 192.168.1.1/24)")]
MissingPrefix(String),
#[error("invalid IP address: {0}")]
InvalidAddress(String),
#[error("invalid prefix length: {0}")]
InvalidPrefix(String),
#[error("prefix length {prefix} exceeds maximum {max}")]
PrefixTooLarge { prefix: u8, max: u8 },
}
#[derive(Debug, Clone)]
pub struct DeclaredRoute {
pub(crate) destination: IpAddr,
pub(crate) prefix_len: u8,
pub(crate) gateway: Option<IpAddr>,
pub(crate) dev: Option<String>,
pub(crate) metric: Option<u32>,
pub(crate) table: Option<u32>,
pub(crate) route_type: DeclaredRouteType,
}
impl DeclaredRoute {
pub fn destination(&self) -> IpAddr {
self.destination
}
pub fn prefix_len(&self) -> u8 {
self.prefix_len
}
pub fn gateway(&self) -> Option<IpAddr> {
self.gateway
}
pub fn dev(&self) -> Option<&str> {
self.dev.as_deref()
}
pub fn metric(&self) -> Option<u32> {
self.metric
}
pub fn table(&self) -> Option<u32> {
self.table
}
pub fn route_type(&self) -> DeclaredRouteType {
self.route_type
}
pub fn is_ipv4(&self) -> bool {
self.destination.is_ipv4()
}
pub fn is_ipv6(&self) -> bool {
self.destination.is_ipv6()
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
#[non_exhaustive]
pub enum DeclaredRouteType {
#[default]
Unicast,
Blackhole,
Unreachable,
Prohibit,
}
#[derive(Debug, Clone, thiserror::Error)]
#[non_exhaustive]
pub enum RouteParseError {
#[error("destination missing prefix: {0} (expected format: 10.0.0.0/8)")]
MissingPrefix(String),
#[error("invalid destination address: {0}")]
InvalidDestination(String),
#[error("invalid prefix length: {0}")]
InvalidPrefix(String),
#[error("prefix length {prefix} exceeds maximum {max}")]
PrefixTooLarge { prefix: u8, max: u8 },
#[error("invalid gateway address: {0}")]
InvalidGateway(String),
}
#[derive(Debug)]
#[must_use = "builders do nothing unless used"]
pub struct RouteBuilder {
destination: IpAddr,
prefix_len: u8,
gateway: Option<IpAddr>,
dev: Option<String>,
metric: Option<u32>,
table: Option<u32>,
route_type: DeclaredRouteType,
}
impl RouteBuilder {
fn new(dst: &str) -> Result<Self, RouteParseError> {
let (ip_str, prefix_str) = dst
.split_once('/')
.ok_or_else(|| RouteParseError::MissingPrefix(dst.to_string()))?;
let destination: IpAddr = ip_str
.parse()
.map_err(|_| RouteParseError::InvalidDestination(ip_str.to_string()))?;
let prefix_len: u8 = prefix_str
.parse()
.map_err(|_| RouteParseError::InvalidPrefix(prefix_str.to_string()))?;
let max_prefix = if destination.is_ipv4() { 32 } else { 128 };
if prefix_len > max_prefix {
return Err(RouteParseError::PrefixTooLarge {
prefix: prefix_len,
max: max_prefix,
});
}
Ok(Self {
destination,
prefix_len,
gateway: None,
dev: None,
metric: None,
table: None,
route_type: DeclaredRouteType::default(),
})
}
pub fn via(mut self, gateway: &str) -> Self {
if let Ok(addr) = gateway.parse() {
self.gateway = Some(addr);
}
self
}
pub fn dev(mut self, dev: &str) -> Self {
self.dev = Some(dev.to_string());
self
}
pub fn metric(mut self, metric: u32) -> Self {
self.metric = Some(metric);
self
}
pub fn table(mut self, table: u32) -> Self {
self.table = Some(table);
self
}
pub fn blackhole(mut self) -> Self {
self.route_type = DeclaredRouteType::Blackhole;
self
}
pub fn unreachable(mut self) -> Self {
self.route_type = DeclaredRouteType::Unreachable;
self
}
pub fn prohibit(mut self) -> Self {
self.route_type = DeclaredRouteType::Prohibit;
self
}
fn build(self) -> DeclaredRoute {
DeclaredRoute {
destination: self.destination,
prefix_len: self.prefix_len,
gateway: self.gateway,
dev: self.dev,
metric: self.metric,
table: self.table,
route_type: self.route_type,
}
}
}
#[derive(Debug, Clone)]
pub struct DeclaredQdisc {
pub(crate) dev: String,
pub(crate) parent: QdiscParent,
pub(crate) qdisc_type: DeclaredQdiscType,
}
impl DeclaredQdisc {
pub fn dev(&self) -> &str {
&self.dev
}
pub fn parent(&self) -> QdiscParent {
self.parent
}
pub fn qdisc_type(&self) -> &DeclaredQdiscType {
&self.qdisc_type
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
#[non_exhaustive]
pub enum QdiscParent {
#[default]
Root,
Ingress,
}
#[derive(Debug, Clone)]
#[non_exhaustive]
pub enum DeclaredQdiscType {
Netem {
delay_us: Option<u32>,
jitter_us: Option<u32>,
loss_percent: Option<f64>,
limit: Option<u32>,
},
Htb { default_class: u32 },
FqCodel {
limit: Option<u32>,
target_us: Option<u32>,
interval_us: Option<u32>,
},
Tbf {
rate_bps: u64,
burst_bytes: u32,
limit_bytes: Option<u32>,
},
Sfq { perturb_secs: Option<u32> },
Prio { bands: Option<u8> },
Ingress,
Clsact,
}
impl DeclaredQdiscType {
pub fn kind(&self) -> &str {
match self {
Self::Netem { .. } => "netem",
Self::Htb { .. } => "htb",
Self::FqCodel { .. } => "fq_codel",
Self::Tbf { .. } => "tbf",
Self::Sfq { .. } => "sfq",
Self::Prio { .. } => "prio",
Self::Ingress => "ingress",
Self::Clsact => "clsact",
}
}
}
#[derive(Debug)]
#[must_use = "builders do nothing unless used"]
pub struct QdiscBuilder {
dev: String,
parent: QdiscParent,
qdisc_type: Option<DeclaredQdiscType>,
}
impl QdiscBuilder {
fn new(dev: &str) -> Self {
Self {
dev: dev.to_string(),
parent: QdiscParent::Root,
qdisc_type: None,
}
}
pub fn netem(mut self) -> Self {
self.qdisc_type = Some(DeclaredQdiscType::Netem {
delay_us: None,
jitter_us: None,
loss_percent: None,
limit: None,
});
self
}
pub fn delay_ms(mut self, ms: u32) -> Self {
if let Some(DeclaredQdiscType::Netem { delay_us, .. }) = &mut self.qdisc_type {
*delay_us = Some(ms * 1000);
}
self
}
pub fn delay_us(mut self, us: u32) -> Self {
if let Some(DeclaredQdiscType::Netem { delay_us, .. }) = &mut self.qdisc_type {
*delay_us = Some(us);
}
self
}
pub fn jitter_ms(mut self, ms: u32) -> Self {
if let Some(DeclaredQdiscType::Netem { jitter_us, .. }) = &mut self.qdisc_type {
*jitter_us = Some(ms * 1000);
}
self
}
pub fn loss(mut self, percent: f64) -> Self {
if let Some(DeclaredQdiscType::Netem { loss_percent, .. }) = &mut self.qdisc_type {
*loss_percent = Some(percent);
}
self
}
pub fn limit(mut self, packets: u32) -> Self {
if let Some(DeclaredQdiscType::Netem { limit, .. }) = &mut self.qdisc_type {
*limit = Some(packets);
}
self
}
pub fn htb(mut self) -> Self {
self.qdisc_type = Some(DeclaredQdiscType::Htb { default_class: 0 });
self
}
pub fn default_class(mut self, class: u32) -> Self {
if let Some(DeclaredQdiscType::Htb { default_class }) = &mut self.qdisc_type {
*default_class = class;
}
self
}
pub fn fq_codel(mut self) -> Self {
self.qdisc_type = Some(DeclaredQdiscType::FqCodel {
limit: None,
target_us: None,
interval_us: None,
});
self
}
pub fn tbf(mut self, rate_bps: u64, burst_bytes: u32) -> Self {
self.qdisc_type = Some(DeclaredQdiscType::Tbf {
rate_bps,
burst_bytes,
limit_bytes: None,
});
self
}
pub fn sfq(mut self) -> Self {
self.qdisc_type = Some(DeclaredQdiscType::Sfq { perturb_secs: None });
self
}
pub fn prio(mut self) -> Self {
self.qdisc_type = Some(DeclaredQdiscType::Prio { bands: None });
self
}
pub fn ingress(mut self) -> Self {
self.parent = QdiscParent::Ingress;
self.qdisc_type = Some(DeclaredQdiscType::Ingress);
self
}
pub fn clsact(mut self) -> Self {
self.qdisc_type = Some(DeclaredQdiscType::Clsact);
self
}
fn build(self) -> DeclaredQdisc {
DeclaredQdisc {
dev: self.dev,
parent: self.parent,
qdisc_type: self.qdisc_type.unwrap_or(DeclaredQdiscType::FqCodel {
limit: None,
target_us: None,
interval_us: None,
}),
}
}
}