use std::net::IpAddr;
pub use crate::netlink::link::{
AdSelect as BondAdSelect, LacpRate as BondLacpRate, NetkitMode, NetkitPolicy, NetkitScrub,
VlanProtocol,
};
#[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
}
}
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
#[cfg_attr(feature = "serde", serde(rename_all = "kebab-case"))]
#[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()
}
}
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub enum DeclaredLinkType {
Dummy,
Veth { peer: String },
Bridge,
Vlan {
parent: String,
vlan_id: u16,
protocol: Option<VlanProtocol>,
},
Vxlan {
vni: u32,
remote: Option<IpAddr>,
local: Option<IpAddr>,
port: Option<u16>,
underlay_dev: Option<String>,
},
Macvlan { parent: String, mode: MacvlanMode },
Bond {
mode: BondMode,
miimon: Option<u32>,
xmit_hash_policy: Option<u8>,
min_links: Option<u32>,
ad_select: Option<BondAdSelect>,
lacp_rate: Option<BondLacpRate>,
downdelay: Option<u32>,
updelay: Option<u32>,
resend_igmp: Option<u32>,
},
Ifb,
Vrf { table: u32 },
Ovpn,
Netkit {
peer: String,
mode: Option<NetkitMode>,
primary_policy: Option<NetkitPolicy>,
peer_policy: Option<NetkitPolicy>,
scrub: Option<NetkitScrub>,
peer_scrub: Option<NetkitScrub>,
},
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::Vrf { .. } => Some("vrf"),
Self::Netkit { .. } => Some("netkit"),
Self::Ovpn => Some("ovpn"),
Self::Physical => None,
}
}
}
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
#[non_exhaustive]
pub enum LinkState {
Up,
#[default]
Down,
Unchanged,
}
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
#[non_exhaustive]
pub enum MacvlanMode {
Private,
Vepa,
#[default]
Bridge,
Passthru,
Source,
}
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
#[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,
protocol: None,
};
self
}
pub fn vlan_protocol(mut self, p: VlanProtocol) -> Self {
if let DeclaredLinkType::Vlan { protocol, .. } = &mut self.link_type {
*protocol = Some(p);
}
self
}
pub fn vxlan(mut self, vni: u32) -> Self {
self.link_type = DeclaredLinkType::Vxlan {
vni,
remote: None,
local: None,
port: None,
underlay_dev: None,
};
self
}
pub fn vxlan_remote(mut self, remote_addr: IpAddr) -> Self {
if let DeclaredLinkType::Vxlan { remote, .. } = &mut self.link_type {
*remote = Some(remote_addr);
}
self
}
pub fn vxlan_local(mut self, local_addr: IpAddr) -> Self {
if let DeclaredLinkType::Vxlan { local, .. } = &mut self.link_type {
*local = Some(local_addr);
}
self
}
pub fn vxlan_port(mut self, udp_port: u16) -> Self {
if let DeclaredLinkType::Vxlan { port, .. } = &mut self.link_type {
*port = Some(udp_port);
}
self
}
pub fn vxlan_underlay_dev(mut self, dev: impl Into<String>) -> Self {
if let DeclaredLinkType::Vxlan {
underlay_dev, ..
} = &mut self.link_type
{
*underlay_dev = Some(dev.into());
}
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,
ad_select: None,
lacp_rate: None,
downdelay: None,
updelay: None,
resend_igmp: 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 bond_ad_select(mut self, sel: BondAdSelect) -> Self {
if let DeclaredLinkType::Bond { ad_select, .. } = &mut self.link_type {
*ad_select = Some(sel);
}
self
}
pub fn bond_lacp_rate(mut self, rate: BondLacpRate) -> Self {
if let DeclaredLinkType::Bond { lacp_rate, .. } = &mut self.link_type {
*lacp_rate = Some(rate);
}
self
}
pub fn bond_downdelay(mut self, ms: u32) -> Self {
if let DeclaredLinkType::Bond { downdelay, .. } = &mut self.link_type {
*downdelay = Some(ms);
}
self
}
pub fn bond_updelay(mut self, ms: u32) -> Self {
if let DeclaredLinkType::Bond { updelay, .. } = &mut self.link_type {
*updelay = Some(ms);
}
self
}
pub fn bond_resend_igmp(mut self, count: u32) -> Self {
if let DeclaredLinkType::Bond { resend_igmp, .. } = &mut self.link_type {
*resend_igmp = Some(count);
}
self
}
pub fn ifb(mut self) -> Self {
self.link_type = DeclaredLinkType::Ifb;
self
}
pub fn ovpn(mut self) -> Self {
self.link_type = DeclaredLinkType::Ovpn;
self
}
pub fn netkit(mut self, peer: impl Into<String>) -> Self {
self.link_type = DeclaredLinkType::Netkit {
peer: peer.into(),
mode: None,
primary_policy: None,
peer_policy: None,
scrub: None,
peer_scrub: None,
};
self
}
pub fn netkit_mode(mut self, m: NetkitMode) -> Self {
if let DeclaredLinkType::Netkit { mode, .. } = &mut self.link_type {
*mode = Some(m);
}
self
}
pub fn netkit_primary_policy(mut self, p: NetkitPolicy) -> Self {
if let DeclaredLinkType::Netkit { primary_policy, .. } = &mut self.link_type {
*primary_policy = Some(p);
}
self
}
pub fn netkit_peer_policy(mut self, p: NetkitPolicy) -> Self {
if let DeclaredLinkType::Netkit { peer_policy, .. } = &mut self.link_type {
*peer_policy = Some(p);
}
self
}
pub fn netkit_scrub(mut self, s: NetkitScrub) -> Self {
if let DeclaredLinkType::Netkit { scrub, .. } = &mut self.link_type {
*scrub = Some(s);
}
self
}
pub fn netkit_peer_scrub(mut self, s: NetkitScrub) -> Self {
if let DeclaredLinkType::Netkit { peer_scrub, .. } = &mut self.link_type {
*peer_scrub = Some(s);
}
self
}
pub fn vrf(mut self, table: u32) -> Self {
self.link_type = DeclaredLinkType::Vrf { table };
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,
}
}
}
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
#[cfg_attr(feature = "serde", serde(rename_all = "kebab-case"))]
#[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 },
}
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
#[cfg_attr(feature = "serde", serde(rename_all = "kebab-case"))]
#[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()
}
}
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
#[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 {
pub fn default_v4() -> Self {
Self::new("0.0.0.0/0").expect("0.0.0.0/0 is a valid IPv4 CIDR")
}
pub fn default_v6() -> Self {
Self::new("::/0").expect("::/0 is a valid IPv6 CIDR")
}
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,
}
}
}
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
#[cfg_attr(feature = "serde", serde(rename_all = "kebab-case"))]
#[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
}
}
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
#[non_exhaustive]
pub enum QdiscParent {
#[default]
Root,
Ingress,
}
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
#[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,
}),
}
}
}
#[cfg(test)]
mod plan_190_tests {
use super::*;
#[test]
fn vrf_builder_sets_table() {
let link = LinkBuilder::new("vrf-red").vrf(100).build();
match link.link_type {
DeclaredLinkType::Vrf { table } => assert_eq!(table, 100),
other => panic!("expected DeclaredLinkType::Vrf, got {other:?}"),
}
}
#[test]
fn vrf_kind_string_is_vrf() {
let lt = DeclaredLinkType::Vrf { table: 7 };
assert_eq!(lt.kind(), Some("vrf"));
}
#[test]
fn ovpn_builder_creates_ovpn_variant() {
let link = LinkBuilder::new("ovpn0").ovpn().build();
assert!(matches!(link.link_type, DeclaredLinkType::Ovpn));
}
#[test]
fn ovpn_kind_string_is_ovpn() {
assert_eq!(DeclaredLinkType::Ovpn.kind(), Some("ovpn"));
}
#[test]
fn netkit_builder_peer_carried_others_default_none() {
let link = LinkBuilder::new("nk0").netkit("nk1").build();
match link.link_type {
DeclaredLinkType::Netkit {
peer,
mode,
primary_policy,
peer_policy,
scrub,
peer_scrub,
} => {
assert_eq!(peer, "nk1");
assert!(mode.is_none());
assert!(primary_policy.is_none());
assert!(peer_policy.is_none());
assert!(scrub.is_none());
assert!(peer_scrub.is_none());
}
other => panic!("expected Netkit, got {other:?}"),
}
}
#[test]
fn netkit_builder_full_setter_chain() {
let link = LinkBuilder::new("nk0")
.netkit("nk1")
.netkit_mode(NetkitMode::L2)
.netkit_primary_policy(NetkitPolicy::Forward)
.netkit_peer_policy(NetkitPolicy::Blackhole)
.netkit_scrub(NetkitScrub::Default)
.netkit_peer_scrub(NetkitScrub::None)
.build();
match link.link_type {
DeclaredLinkType::Netkit {
peer,
mode,
primary_policy,
peer_policy,
scrub,
peer_scrub,
} => {
assert_eq!(peer, "nk1");
assert_eq!(mode, Some(NetkitMode::L2));
assert_eq!(primary_policy, Some(NetkitPolicy::Forward));
assert_eq!(peer_policy, Some(NetkitPolicy::Blackhole));
assert_eq!(scrub, Some(NetkitScrub::Default));
assert_eq!(peer_scrub, Some(NetkitScrub::None));
}
other => panic!("expected Netkit, got {other:?}"),
}
}
#[test]
fn netkit_kind_string_is_netkit() {
let lt = DeclaredLinkType::Netkit {
peer: "x".into(),
mode: None,
primary_policy: None,
peer_policy: None,
scrub: None,
peer_scrub: None,
};
assert_eq!(lt.kind(), Some("netkit"));
}
#[test]
fn bond_builder_defaults_all_new_knobs_to_none() {
let link = LinkBuilder::new("bond0").bond().build();
match link.link_type {
DeclaredLinkType::Bond {
ad_select,
lacp_rate,
downdelay,
updelay,
resend_igmp,
..
} => {
assert!(ad_select.is_none());
assert!(lacp_rate.is_none());
assert!(downdelay.is_none());
assert!(updelay.is_none());
assert!(resend_igmp.is_none());
}
other => panic!("expected Bond, got {other:?}"),
}
}
#[test]
fn bond_builder_all_5_setters_round_trip() {
let link = LinkBuilder::new("bond0")
.bond()
.bond_ad_select(BondAdSelect::Bandwidth)
.bond_lacp_rate(BondLacpRate::Fast)
.bond_downdelay(200)
.bond_updelay(500)
.bond_resend_igmp(3)
.build();
match link.link_type {
DeclaredLinkType::Bond {
ad_select,
lacp_rate,
downdelay,
updelay,
resend_igmp,
..
} => {
assert_eq!(ad_select, Some(BondAdSelect::Bandwidth));
assert_eq!(lacp_rate, Some(BondLacpRate::Fast));
assert_eq!(downdelay, Some(200));
assert_eq!(updelay, Some(500));
assert_eq!(resend_igmp, Some(3));
}
other => panic!("expected Bond, got {other:?}"),
}
}
#[test]
fn bond_setters_no_op_on_non_bond() {
let link = LinkBuilder::new("eth0")
.dummy()
.bond_ad_select(BondAdSelect::Stable)
.bond_lacp_rate(BondLacpRate::Slow)
.bond_downdelay(100)
.bond_updelay(100)
.bond_resend_igmp(1)
.build();
assert!(matches!(link.link_type, DeclaredLinkType::Dummy));
}
#[test]
fn vxlan_builder_defaults_to_none_for_new_knobs() {
let link = LinkBuilder::new("vx0").vxlan(42).build();
match link.link_type {
DeclaredLinkType::Vxlan {
vni,
remote,
local,
port,
underlay_dev,
} => {
assert_eq!(vni, 42);
assert!(remote.is_none());
assert!(local.is_none());
assert!(port.is_none());
assert!(underlay_dev.is_none());
}
other => panic!("expected Vxlan, got {other:?}"),
}
}
#[test]
fn vxlan_builder_local_port_underlay_round_trip() {
use std::net::Ipv4Addr;
let link = LinkBuilder::new("vx0")
.vxlan(100)
.vxlan_remote(IpAddr::V4(Ipv4Addr::new(10, 0, 0, 1)))
.vxlan_local(IpAddr::V4(Ipv4Addr::new(10, 0, 0, 2)))
.vxlan_port(4790)
.vxlan_underlay_dev("eth0")
.build();
match link.link_type {
DeclaredLinkType::Vxlan {
vni,
remote,
local,
port,
underlay_dev,
} => {
assert_eq!(vni, 100);
assert_eq!(remote, Some(IpAddr::V4(Ipv4Addr::new(10, 0, 0, 1))));
assert_eq!(local, Some(IpAddr::V4(Ipv4Addr::new(10, 0, 0, 2))));
assert_eq!(port, Some(4790));
assert_eq!(underlay_dev.as_deref(), Some("eth0"));
}
other => panic!("expected Vxlan, got {other:?}"),
}
}
#[test]
fn vxlan_setters_no_op_on_non_vxlan() {
use std::net::Ipv4Addr;
let link = LinkBuilder::new("eth0")
.dummy()
.vxlan_local(IpAddr::V4(Ipv4Addr::new(1, 2, 3, 4)))
.vxlan_port(4790)
.vxlan_underlay_dev("ignored")
.build();
assert!(matches!(link.link_type, DeclaredLinkType::Dummy));
}
#[test]
fn vlan_builder_protocol_defaults_to_none() {
let link = LinkBuilder::new("eth0.100").vlan("eth0", 100).build();
match link.link_type {
DeclaredLinkType::Vlan { protocol, .. } => assert!(protocol.is_none()),
other => panic!("expected Vlan, got {other:?}"),
}
}
#[test]
fn vlan_builder_protocol_setter_records_dot1ad() {
let link = LinkBuilder::new("eth0.100")
.vlan("eth0", 100)
.vlan_protocol(VlanProtocol::Dot1ad)
.build();
match link.link_type {
DeclaredLinkType::Vlan { protocol, .. } => {
assert_eq!(protocol, Some(VlanProtocol::Dot1ad));
}
other => panic!("expected Vlan, got {other:?}"),
}
}
#[test]
fn vlan_protocol_setter_no_op_on_non_vlan() {
let link = LinkBuilder::new("eth0")
.dummy()
.vlan_protocol(VlanProtocol::Dot1ad)
.build();
assert!(matches!(link.link_type, DeclaredLinkType::Dummy));
}
#[test]
fn vlan_protocol_wire_values() {
assert_eq!(VlanProtocol::Dot1q.as_u16(), 0x8100);
assert_eq!(VlanProtocol::Dot1ad.as_u16(), 0x88a8);
}
#[test]
fn vrf_in_network_config_carries_master_chain() {
let cfg = NetworkConfig::new()
.link("vrf-red", |b| b.vrf(100))
.link("eth0", |b| b.dummy().master("vrf-red"));
assert_eq!(cfg.links.len(), 2);
assert!(matches!(
cfg.links[0].link_type,
DeclaredLinkType::Vrf { table: 100 }
));
assert_eq!(cfg.links[1].master.as_deref(), Some("vrf-red"));
}
}