Skip to content

Commit 8cb3e4d

Browse files
committed
ip link: add type bridge creation support
Introduced support for `ip link add <name> type bridge [options]` with all 36 bridge-specific options matching the iproute2 CLI. Also fixed display of MulticastQuerierState fields (querier IPv4/IPv6 addresses) to match iproute2 output. Integration test cases added for bridge creation with various options. Signed-off-by: Gris Ge <cnfourt@gmail.com>
1 parent 38440f6 commit 8cb3e4d

6 files changed

Lines changed: 593 additions & 44 deletions

File tree

src/ip/link/add.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,9 @@ impl LinkAddCommand {
5555
InfoKind::Bond => {
5656
base_conf.apply(base_conf.apply_bond(&handle).await?)?
5757
}
58+
InfoKind::Bridge => {
59+
base_conf.apply(base_conf.apply_bridge().await?)?
60+
}
5861
t => {
5962
return Err(CliError::from(format!(
6063
"Unsupported device type: {t}"

src/ip/link/ifaces/bond.rs

Lines changed: 3 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,9 @@ use rtnetlink::{
1515
};
1616
use serde::Serialize;
1717

18+
use super::parse::{
19+
parse_from_str, parse_on_off_01, parse_u8, parse_u16, parse_u32,
20+
};
1821
use crate::link::LinkBaseConf;
1922

2023
#[derive(Serialize)]
@@ -525,34 +528,3 @@ impl LinkBaseConf {
525528
Ok(builder)
526529
}
527530
}
528-
529-
fn parse_on_off_01(s: &str) -> Result<bool, CliError> {
530-
match s {
531-
"on" | "1" => Ok(true),
532-
"off" | "0" => Ok(false),
533-
_ => Err(CliError::from(format!("expected on/off or 0/1, got {s}"))),
534-
}
535-
}
536-
537-
fn parse_u32(s: &str, name: &str) -> Result<u32, CliError> {
538-
s.parse::<u32>()
539-
.map_err(|_| CliError::from(format!("Invalid {name} value: {s}")))
540-
}
541-
542-
fn parse_u8(s: &str, name: &str) -> Result<u8, CliError> {
543-
s.parse::<u8>()
544-
.map_err(|_| CliError::from(format!("Invalid {name} value: {s}")))
545-
}
546-
547-
fn parse_u16(s: &str, name: &str) -> Result<u16, CliError> {
548-
s.parse::<u16>()
549-
.map_err(|_| CliError::from(format!("Invalid {name} value: {s}")))
550-
}
551-
552-
fn parse_from_str<T: FromStr>(s: &str, name: &str) -> Result<T, CliError>
553-
where
554-
T::Err: std::fmt::Display,
555-
{
556-
s.parse::<T>()
557-
.map_err(|e| CliError::from(format!("Invalid {name} value: {e}")))
558-
}

src/ip/link/ifaces/bridge.rs

Lines changed: 260 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,20 @@
11
// SPDX-License-Identifier: MIT
22

3-
use iproute_rs::mac_to_string;
4-
use rtnetlink::packet_route::link::{
5-
BridgeBooleanOptionFlags as BoolOptFlags, BridgePortState, InfoBridge,
6-
InfoBridgePort, VlanProtocol,
3+
use iproute_rs::{CliError, mac_to_string, parse_mac_str};
4+
use rtnetlink::{
5+
LinkBridge, LinkMessageBuilder,
6+
packet_route::link::{
7+
BridgeBooleanOptionFlags as BoolOptFlags, BridgeQuerierState,
8+
InfoBridge, InfoBridgePort, VlanProtocol,
9+
},
710
};
811
use serde::Serialize;
912

13+
use super::parse::{
14+
parse_from_str, parse_on_off_01, parse_u8, parse_u16, parse_u32, parse_u64,
15+
};
16+
use crate::link::LinkBaseConf;
17+
1018
#[derive(Serialize)]
1119
pub(crate) struct CliLinkInfoDataBridge {
1220
forward_delay: u32,
@@ -74,6 +82,10 @@ pub(crate) struct CliLinkInfoDataBridge {
7482
nf_call_iptables: u8,
7583
nf_call_ip6tables: u8,
7684
nf_call_arptables: u8,
85+
#[serde(skip_serializing_if = "Option::is_none")]
86+
mcast_querier_ipv4_addr: Option<String>,
87+
#[serde(skip_serializing_if = "Option::is_none")]
88+
mcast_querier_ipv6_addr: Option<String>,
7789
}
7890

7991
impl From<&[InfoBridge]> for CliLinkInfoDataBridge {
@@ -130,6 +142,8 @@ impl From<&[InfoBridge]> for CliLinkInfoDataBridge {
130142
let mut mst_enabled = None;
131143
let mut mdb_offload_fail_notification = None;
132144
let mut fdb_local_vlan_0 = None;
145+
let mut mcast_querier_ipv4_addr = None;
146+
let mut mcast_querier_ipv6_addr = None;
133147

134148
for nla in info {
135149
match nla {
@@ -263,6 +277,19 @@ impl From<&[InfoBridge]> for CliLinkInfoDataBridge {
263277
);
264278
}
265279
}
280+
InfoBridge::MulticastQuerierState(states) => {
281+
for state in states {
282+
match state {
283+
BridgeQuerierState::Ipv4Address(a) => {
284+
mcast_querier_ipv4_addr = Some(a.to_string());
285+
}
286+
BridgeQuerierState::Ipv6Address(a) => {
287+
mcast_querier_ipv6_addr = Some(a.to_string());
288+
}
289+
_ => (),
290+
}
291+
}
292+
}
266293
_ => (),
267294
}
268295
}
@@ -320,6 +347,8 @@ impl From<&[InfoBridge]> for CliLinkInfoDataBridge {
320347
nf_call_iptables,
321348
nf_call_ip6tables,
322349
nf_call_arptables,
350+
mcast_querier_ipv4_addr,
351+
mcast_querier_ipv6_addr,
323352
}
324353
}
325354
}
@@ -397,6 +426,12 @@ impl std::fmt::Display for CliLinkInfoDataBridge {
397426
write!(f, " mcast_router {}", self.mcast_router)?;
398427
write!(f, " mcast_query_use_ifaddr {}", self.mcast_query_use_ifaddr)?;
399428
write!(f, " mcast_querier {}", self.mcast_querier)?;
429+
if let Some(ref addr) = self.mcast_querier_ipv4_addr {
430+
write!(f, " mcast_querier_ipv4_addr {}", addr)?;
431+
}
432+
if let Some(ref addr) = self.mcast_querier_ipv6_addr {
433+
write!(f, " mcast_querier_ipv6_addr {}", addr)?;
434+
}
400435
write!(f, " mcast_hash_elasticity {}", self.mcast_hash_elasticity)?;
401436
write!(f, " mcast_hash_max {}", self.mcast_hash_max)?;
402437
write!(f, " mcast_last_member_count {}", self.mcast_last_member_cnt)?;
@@ -524,15 +559,7 @@ impl From<&[InfoBridgePort]> for CliLinkInfoDataBridgePort {
524559
for nla in info {
525560
match nla {
526561
InfoBridgePort::State(v) => {
527-
state = match v {
528-
BridgePortState::Disabled => "disabled".to_string(),
529-
BridgePortState::Listening => "listening".to_string(),
530-
BridgePortState::Learning => "learning".to_string(),
531-
BridgePortState::Forwarding => "forwarding".to_string(),
532-
BridgePortState::Blocking => "blocking".to_string(),
533-
BridgePortState::Other(n) => format!("{}", n),
534-
_ => "unknown".to_string(),
535-
};
562+
state = v.to_string();
536563
}
537564
InfoBridgePort::Priority(v) => priority = *v as u32,
538565
InfoBridgePort::Cost(v) => cost = *v,
@@ -704,6 +731,226 @@ fn format_bridge_timer(v: u64) -> String {
704731
format!("{:>7.2}", seconds)
705732
}
706733

734+
impl LinkBaseConf {
735+
pub(crate) async fn apply_bridge(
736+
&self,
737+
) -> Result<LinkMessageBuilder<LinkBridge>, CliError> {
738+
let mut builder = LinkBridge::new(&self.name);
739+
740+
let mut iter = self.iface_specific.iter();
741+
while let Some(key) = iter.next() {
742+
let mut next_val = || {
743+
iter.next().ok_or_else(|| {
744+
CliError::from(format!("bridge {key} requires a value"))
745+
})
746+
};
747+
match key.as_str() {
748+
"forward_delay" => {
749+
let v = next_val()?;
750+
builder =
751+
builder.forward_delay(parse_u32(v, "forward_delay")?);
752+
}
753+
"hello_time" => {
754+
let v = next_val()?;
755+
builder = builder.hello_time(parse_u32(v, "hello_time")?);
756+
}
757+
"max_age" => {
758+
let v = next_val()?;
759+
builder = builder.max_age(parse_u32(v, "max_age")?);
760+
}
761+
"ageing_time" => {
762+
let v = next_val()?;
763+
builder = builder.ageing_time(parse_u32(v, "ageing_time")?);
764+
}
765+
"stp_state" => {
766+
let v = next_val()?;
767+
builder =
768+
builder.stp_state(parse_from_str(v, "stp_state")?);
769+
}
770+
"priority" => {
771+
let v = next_val()?;
772+
builder = builder.priority(parse_u16(v, "priority")?);
773+
}
774+
"vlan_filtering" => {
775+
let v = next_val()?;
776+
builder = builder.vlan_filtering(parse_on_off_01(v)?);
777+
}
778+
"vlan_protocol" => {
779+
let v = next_val()?;
780+
builder = builder
781+
.vlan_protocol(parse_from_str(v, "vlan_protocol")?);
782+
}
783+
"vlan_default_pvid" => {
784+
let v = next_val()?;
785+
builder = builder
786+
.vlan_default_pvid(parse_u16(v, "vlan_default_pvid")?);
787+
}
788+
"vlan_stats_enabled" => {
789+
let v = next_val()?;
790+
builder = builder.vlan_stats_enabled(parse_on_off_01(v)?);
791+
}
792+
"vlan_stats_per_port" => {
793+
let v = next_val()?;
794+
builder = builder.vlan_stats_per_port(parse_on_off_01(v)?);
795+
}
796+
"group_fwd_mask" => {
797+
let v = next_val()?;
798+
builder =
799+
builder.group_fwd_mask(parse_u16(v, "group_fwd_mask")?);
800+
}
801+
"group_address" => {
802+
let v = next_val()?;
803+
let mac: [u8; 6] =
804+
parse_mac_str(v)?.try_into().map_err(|_| {
805+
CliError::from(format!(
806+
"Invalid group_address MAC: {v}"
807+
))
808+
})?;
809+
builder = builder.group_address(mac);
810+
}
811+
"mcast_snooping" => {
812+
let v = next_val()?;
813+
builder = builder.mcast_snooping(parse_on_off_01(v)?);
814+
}
815+
"mcast_vlan_snooping" => {
816+
let v = next_val()?;
817+
builder = builder.mcast_vlan_snooping(parse_on_off_01(v)?);
818+
}
819+
"mcast_router" => {
820+
let v = next_val()?;
821+
builder = builder
822+
.mcast_router(parse_from_str(v, "mcast_router")?);
823+
}
824+
"mcast_query_use_ifaddr" => {
825+
let v = next_val()?;
826+
builder =
827+
builder.mcast_query_use_ifaddr(parse_on_off_01(v)?);
828+
}
829+
"mcast_querier" => {
830+
let v = next_val()?;
831+
builder = builder.mcast_querier(parse_on_off_01(v)?);
832+
}
833+
"mcast_hash_max" => {
834+
let v = next_val()?;
835+
builder =
836+
builder.mcast_hash_max(parse_u32(v, "mcast_hash_max")?);
837+
}
838+
"mcast_last_member_count" => {
839+
let v = next_val()?;
840+
builder = builder.mcast_last_member_count(parse_u32(
841+
v,
842+
"mcast_last_member_count",
843+
)?);
844+
}
845+
"mcast_startup_query_count" => {
846+
let v = next_val()?;
847+
builder = builder.mcast_startup_query_count(parse_u32(
848+
v,
849+
"mcast_startup_query_count",
850+
)?);
851+
}
852+
"mcast_last_member_interval" => {
853+
let v = next_val()?;
854+
builder = builder.mcast_last_member_interval(parse_u64(
855+
v,
856+
"mcast_last_member_interval",
857+
)?);
858+
}
859+
"mcast_membership_interval" => {
860+
let v = next_val()?;
861+
builder = builder.mcast_membership_interval(parse_u64(
862+
v,
863+
"mcast_membership_interval",
864+
)?);
865+
}
866+
"mcast_querier_interval" => {
867+
let v = next_val()?;
868+
builder = builder.mcast_querier_interval(parse_u64(
869+
v,
870+
"mcast_querier_interval",
871+
)?);
872+
}
873+
"mcast_query_interval" => {
874+
let v = next_val()?;
875+
builder = builder.mcast_query_interval(parse_u64(
876+
v,
877+
"mcast_query_interval",
878+
)?);
879+
}
880+
"mcast_query_response_interval" => {
881+
let v = next_val()?;
882+
builder = builder.mcast_query_response_interval(parse_u64(
883+
v,
884+
"mcast_query_response_interval",
885+
)?);
886+
}
887+
"mcast_startup_query_interval" => {
888+
let v = next_val()?;
889+
builder = builder.mcast_startup_query_interval(parse_u64(
890+
v,
891+
"mcast_startup_query_interval",
892+
)?);
893+
}
894+
"mcast_stats_enabled" => {
895+
let v = next_val()?;
896+
builder = builder.mcast_stats_enabled(parse_on_off_01(v)?);
897+
}
898+
"mcast_igmp_version" => {
899+
let v = next_val()?;
900+
builder = builder
901+
.mcast_igmp_version(parse_u8(v, "mcast_igmp_version")?);
902+
}
903+
"mcast_mld_version" => {
904+
let v = next_val()?;
905+
builder = builder
906+
.mcast_mld_version(parse_u8(v, "mcast_mld_version")?);
907+
}
908+
"nf_call_iptables" => {
909+
let v = next_val()?;
910+
builder = builder.nf_call_iptables(parse_on_off_01(v)?);
911+
}
912+
"nf_call_ip6tables" => {
913+
let v = next_val()?;
914+
builder = builder.nf_call_ip6tables(parse_on_off_01(v)?);
915+
}
916+
"nf_call_arptables" => {
917+
let v = next_val()?;
918+
builder = builder.nf_call_arptables(parse_on_off_01(v)?);
919+
}
920+
"no_linklocal_learn" => {
921+
let v = next_val()?;
922+
builder = builder.no_linklocal_learn(parse_on_off_01(v)?);
923+
}
924+
"mdb_offload_fail_notification" => {
925+
let v = next_val()?;
926+
builder = builder
927+
.mdb_offload_fail_notification(parse_on_off_01(v)?);
928+
}
929+
"mst_enabled" => {
930+
let v = next_val()?;
931+
builder = builder.mst_enabled(parse_on_off_01(v)?);
932+
}
933+
"fdb_local_vlan_0" => {
934+
let v = next_val()?;
935+
builder = builder.fdb_local_vlan_0(parse_on_off_01(v)?);
936+
}
937+
"fdb_max_learned" => {
938+
let v = next_val()?;
939+
builder = builder
940+
.fdb_max_learned(parse_u32(v, "fdb_max_learned")?);
941+
}
942+
_ => {
943+
return Err(CliError::from(format!(
944+
"Unknown bridge argument: {key}"
945+
)));
946+
}
947+
}
948+
}
949+
950+
Ok(builder)
951+
}
952+
}
953+
707954
/// Format bridge ID to match iproute2's format:
708955
/// Priority is 4 hex digits, MAC address bytes use minimal formatting (no
709956
/// leading zeros for bytes < 0x10)

src/ip/link/ifaces/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
pub(super) mod bond;
44
pub(super) mod bridge;
5+
pub(super) mod parse;
56
pub(super) mod veth;
67
pub(super) mod vlan;
78
pub(super) mod vxlan;

0 commit comments

Comments
 (0)