Skip to content

Commit 16b4e01

Browse files
committed
ip link: add hsr type support
Introduced `ip link add type hsr` command and `ip link show hsr` display. Supported arguments: slave1 (required), slave2 (required), interlink, supervision, version, proto. Integration test case included to verify creation and detailed show output match iproute2. Signed-off-by: Gris Ge <cnfourt@gmail.com>
1 parent 020ce1f commit 16b4e01

9 files changed

Lines changed: 292 additions & 21 deletions

File tree

Cargo.toml

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -32,8 +32,5 @@ tokio = { version = "1.30", features = ["rt", "net", "time", "macros"] }
3232
pretty_assertions = "1.4.1"
3333
rand = "0.10.0"
3434

35-
[patch.crates-io.rtnetlink]
36-
git = "https://github.com/rust-netlink/rtnetlink"
37-
3835
[patch.crates-io.netlink-packet-route]
3936
git = "https://github.com/rust-netlink/netlink-packet-route"

src/ip/link/add.rs

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

src/ip/link/detail.rs

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -140,12 +140,9 @@ impl CliLinkInfoDetail {
140140
self.inet6_addr_gen_mode = String::new();
141141
}
142142

143-
pub(crate) fn resolve_vxlan_link(
144-
&mut self,
145-
index_2_name: &HashMap<u32, String>,
146-
) {
143+
pub(crate) fn resolve_link(&mut self, index_2_name: &HashMap<u32, String>) {
147144
if let Some(ref mut linkinfo) = self.linkinfo {
148-
linkinfo.resolve_vxlan_link(index_2_name);
145+
linkinfo.resolve_link(index_2_name);
149146
}
150147
}
151148
}

src/ip/link/ifaces/hsr.rs

Lines changed: 188 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,188 @@
1+
// SPDX-License-Identifier: MIT
2+
3+
use std::collections::HashMap;
4+
5+
use iproute_rs::{CliError, mac_to_string};
6+
use rtnetlink::{
7+
LinkHsr, LinkMessageBuilder,
8+
packet_route::link::{HsrProtocol, InfoHsr},
9+
};
10+
use serde::Serialize;
11+
12+
use super::parse::parse_u8;
13+
use crate::link::LinkBaseConf;
14+
15+
#[derive(Serialize)]
16+
pub(crate) struct CliLinkInfoDataHsr {
17+
#[serde(skip)]
18+
port1_index: Option<u32>,
19+
#[serde(skip)]
20+
port2_index: Option<u32>,
21+
#[serde(skip)]
22+
interlink_index: Option<u32>,
23+
#[serde(rename = "slave1", skip_serializing_if = "Option::is_none")]
24+
port1: Option<String>,
25+
#[serde(rename = "slave2", skip_serializing_if = "Option::is_none")]
26+
port2: Option<String>,
27+
#[serde(skip_serializing_if = "Option::is_none")]
28+
interlink: Option<String>,
29+
#[serde(skip_serializing_if = "Option::is_none", rename = "seq_nr")]
30+
sequence: Option<u16>,
31+
#[serde(
32+
skip_serializing_if = "Option::is_none",
33+
rename = "supervision_addr"
34+
)]
35+
supervision: Option<String>,
36+
#[serde(skip_serializing_if = "Option::is_none")]
37+
proto: Option<u8>,
38+
#[serde(skip_serializing_if = "Option::is_none")]
39+
version: Option<u8>,
40+
}
41+
42+
impl CliLinkInfoDataHsr {
43+
pub(crate) fn resolve_link(&mut self, index_2_name: &HashMap<u32, String>) {
44+
let resolve = |idx: u32| {
45+
index_2_name
46+
.get(&idx)
47+
.cloned()
48+
.unwrap_or_else(|| format!("if{idx}"))
49+
};
50+
self.port1 = self.port1_index.map(resolve);
51+
self.port2 = self.port2_index.map(resolve);
52+
self.interlink = self.interlink_index.map(resolve);
53+
}
54+
}
55+
56+
impl From<&[InfoHsr]> for CliLinkInfoDataHsr {
57+
fn from(info: &[InfoHsr]) -> Self {
58+
let mut port1_index = None;
59+
let mut port2_index = None;
60+
let mut interlink_index = None;
61+
let mut sequence = None;
62+
let mut supervision = None;
63+
let mut proto = None;
64+
let mut version = None;
65+
66+
for nla in info {
67+
match nla {
68+
InfoHsr::Port1(v) => port1_index = Some(*v),
69+
InfoHsr::Port2(v) => port2_index = Some(*v),
70+
InfoHsr::Interlink(v) => interlink_index = Some(*v),
71+
InfoHsr::SeqNr(v) => sequence = Some(*v),
72+
InfoHsr::SupervisionAddr(v) => {
73+
supervision = Some(mac_to_string(v))
74+
}
75+
InfoHsr::Protocol(HsrProtocol::Hsr) => proto = Some(0),
76+
InfoHsr::Protocol(HsrProtocol::Prp) => proto = Some(1),
77+
InfoHsr::Protocol(HsrProtocol::Other(v)) => proto = Some(*v),
78+
InfoHsr::Version(v) => version = Some(*v),
79+
_ => (),
80+
}
81+
}
82+
83+
Self {
84+
port1_index,
85+
port2_index,
86+
interlink_index,
87+
port1: None,
88+
port2: None,
89+
interlink: None,
90+
sequence,
91+
supervision,
92+
proto,
93+
version,
94+
}
95+
}
96+
}
97+
98+
impl std::fmt::Display for CliLinkInfoDataHsr {
99+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
100+
if let Some(v) = &self.port1 {
101+
write!(f, "slave1 {v}")?;
102+
}
103+
if let Some(v) = &self.port2 {
104+
write!(f, " slave2 {v}")?;
105+
}
106+
if let Some(v) = &self.interlink {
107+
write!(f, " interlink {v}")?;
108+
}
109+
if let Some(v) = self.sequence {
110+
write!(f, " sequence {v}")?;
111+
}
112+
if let Some(v) = &self.supervision {
113+
write!(f, " supervision {v}")?;
114+
}
115+
if let Some(v) = self.proto {
116+
write!(f, " proto {v}")?;
117+
}
118+
if let Some(v) = self.version {
119+
write!(f, " version {v}")?;
120+
}
121+
Ok(())
122+
}
123+
}
124+
125+
impl LinkBaseConf {
126+
pub(crate) async fn apply_hsr(
127+
&self,
128+
handle: &rtnetlink::Handle,
129+
) -> Result<LinkMessageBuilder<LinkHsr>, CliError> {
130+
let mut builder = LinkHsr::new(&self.name);
131+
let mut has_port1 = false;
132+
let mut has_port2 = false;
133+
134+
let mut iter = self.iface_specific.iter();
135+
while let Some(key) = iter.next() {
136+
let mut next_val = || {
137+
iter.next().ok_or_else(|| {
138+
CliError::from(format!("hsr {key} requires a value"))
139+
})
140+
};
141+
match key.as_str() {
142+
"slave1" => {
143+
let v = next_val()?;
144+
let ifindex = self.get_ifindex_by_name(handle, v).await?;
145+
builder = builder.port1(ifindex);
146+
has_port1 = true;
147+
}
148+
"slave2" => {
149+
let v = next_val()?;
150+
let ifindex = self.get_ifindex_by_name(handle, v).await?;
151+
builder = builder.port2(ifindex);
152+
has_port2 = true;
153+
}
154+
"interlink" => {
155+
let v = next_val()?;
156+
let ifindex = self.get_ifindex_by_name(handle, v).await?;
157+
builder = builder.interlink(ifindex);
158+
}
159+
"supervision" => {
160+
let v = next_val()?;
161+
builder = builder.supervision(parse_u8(v, "supervision")?);
162+
}
163+
"version" => {
164+
let v = next_val()?;
165+
builder = builder.version(parse_u8(v, "version")?);
166+
}
167+
"proto" => {
168+
let v = next_val()?;
169+
let proto: u8 = parse_u8(v, "proto")?;
170+
builder = builder.protocol(proto.into());
171+
}
172+
_ => {
173+
return Err(CliError::from(format!(
174+
"Unknown hsr argument: {key}"
175+
)));
176+
}
177+
}
178+
}
179+
180+
if !has_port1 || !has_port2 {
181+
return Err(CliError::from(
182+
"hsr requires slave1 and slave2 arguments",
183+
));
184+
}
185+
186+
Ok(builder)
187+
}
188+
}

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 hsr;
56
pub(super) mod parse;
67
pub(super) mod veth;
78
pub(super) mod vlan;

src/ip/link/link_info.rs

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ use serde::Serialize;
77

88
use super::ifaces::{
99
bridge::{CliLinkInfoDataBridge, CliLinkInfoDataBridgePort},
10+
hsr::CliLinkInfoDataHsr,
1011
veth::CliLinkInfoDataVeth,
1112
vlan::CliLinkInfoDataVlan,
1213
vxlan::CliLinkInfoDataVxlan,
@@ -65,12 +66,9 @@ impl TryFrom<&[LinkInfo]> for CliLinkInfo {
6566
}
6667

6768
impl CliLinkInfo {
68-
pub(crate) fn resolve_vxlan_link(
69-
&mut self,
70-
index_2_name: &HashMap<u32, String>,
71-
) {
69+
pub(crate) fn resolve_link(&mut self, index_2_name: &HashMap<u32, String>) {
7270
if let Some(ref mut data) = self.info_data {
73-
data.resolve_vxlan_link(index_2_name);
71+
data.resolve_link(index_2_name);
7472
}
7573
}
7674
}
@@ -101,6 +99,7 @@ pub(crate) enum CliLinkInfoData {
10199
Bridge(Box<CliLinkInfoDataBridge>),
102100
Bond(Box<CliLinkInfoDataBond>),
103101
Vxlan(Box<CliLinkInfoDataVxlan>),
102+
Hsr(Box<CliLinkInfoDataHsr>),
104103
}
105104

106105
impl TryFrom<&InfoData> for CliLinkInfoData {
@@ -117,18 +116,18 @@ impl TryFrom<&InfoData> for CliLinkInfoData {
117116
InfoData::Vxlan(v) => {
118117
Ok(Self::Vxlan(Box::new(v.as_slice().into())))
119118
}
119+
InfoData::Hsr(v) => Ok(Self::Hsr(Box::new(v.as_slice().into()))),
120120
_ => Err(()),
121121
}
122122
}
123123
}
124124

125125
impl CliLinkInfoData {
126-
pub(crate) fn resolve_vxlan_link(
127-
&mut self,
128-
index_2_name: &HashMap<u32, String>,
129-
) {
130-
if let Self::Vxlan(vxlan) = self {
131-
vxlan.resolve_link(index_2_name);
126+
pub(crate) fn resolve_link(&mut self, index_2_name: &HashMap<u32, String>) {
127+
match self {
128+
Self::Vxlan(vxlan) => vxlan.resolve_link(index_2_name),
129+
Self::Hsr(hsr) => hsr.resolve_link(index_2_name),
130+
_ => (),
132131
}
133132
}
134133
}
@@ -141,6 +140,7 @@ impl std::fmt::Display for CliLinkInfoData {
141140
CliLinkInfoData::Bridge(v) => write!(f, "{v}"),
142141
CliLinkInfoData::Bond(v) => write!(f, "{v}"),
143142
CliLinkInfoData::Vxlan(v) => write!(f, "{v}"),
143+
CliLinkInfoData::Hsr(v) => write!(f, "{v}"),
144144
}
145145
}
146146
}

src/ip/link/show.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -387,9 +387,9 @@ fn resolve_controller_and_link_names(links: &mut [CliLinkInfo]) {
387387
}
388388
}
389389

390-
// Resolve VxLAN link ifindex to interface name
390+
// Resolve link ifindex (VxLAN, HSR, etc.) to interface name
391391
if let Some(ref mut details) = link.details {
392-
details.resolve_vxlan_link(&index_2_name);
392+
details.resolve_link(&index_2_name);
393393
}
394394
}
395395
}

src/ip/link/tests/hsr.rs

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
// SPDX-License-Identifier: MIT
2+
3+
use crate::tests::{NetnsGuard, with_netns};
4+
5+
const HSR_NAME: &str = "thsr";
6+
const PORT1_NAME: &str = "thsr-p1";
7+
const PORT2_NAME: &str = "thsr-p2";
8+
9+
/// Kernel maintains `hsr->sequence_nr` starting at `USHRT_MAX - 1024` (64511)
10+
/// and increments it on every sent data or supervision frame (supervision
11+
/// frames fire every 2 seconds). Between two `ip link show` calls the value
12+
/// advances, making direct comparison flaky — we zero it out for testing.
13+
fn normalize_seq(mut s: String) -> String {
14+
for target in [" sequence ", "\"seq_nr\":"] {
15+
let mut result = String::new();
16+
let mut remaining = s.as_str();
17+
while let Some(pos) = remaining.find(target) {
18+
result.push_str(&remaining[..=pos + target.len() - 1]);
19+
remaining = &remaining[pos + target.len()..];
20+
let num_len =
21+
remaining.chars().take_while(|c| c.is_ascii_digit()).count();
22+
result.push('0');
23+
remaining = &remaining[num_len..];
24+
}
25+
result.push_str(remaining);
26+
s = result;
27+
}
28+
s
29+
}
30+
31+
#[test]
32+
fn test_hsr_create_and_show_default() {
33+
with_hsr_iface(&[], |ns| {
34+
ns.assert_eq_output_map(
35+
&["-d", "link", "show", HSR_NAME],
36+
normalize_seq,
37+
);
38+
});
39+
}
40+
41+
#[test]
42+
fn test_hsr_create_and_show_with_options() {
43+
with_hsr_iface(&["supervision", "42", "version", "1"], |ns| {
44+
ns.assert_eq_output_map(
45+
&["-d", "link", "show", HSR_NAME],
46+
normalize_seq,
47+
);
48+
});
49+
}
50+
51+
#[test]
52+
fn test_hsr_create_and_show_json() {
53+
with_hsr_iface(&[], |ns| {
54+
ns.assert_eq_output_map(
55+
&["-d", "-j", "link", "show", HSR_NAME],
56+
normalize_seq,
57+
);
58+
});
59+
}
60+
61+
fn with_hsr_iface<T>(opts: &[&str], test: T)
62+
where
63+
T: FnOnce(&NetnsGuard),
64+
{
65+
with_netns(|ns| {
66+
// Create two dummy interfaces as slaves
67+
ns.exec_cmd(&["ip", "link", "add", PORT1_NAME, "type", "dummy"]);
68+
ns.exec_cmd(&["ip", "link", "add", PORT2_NAME, "type", "dummy"]);
69+
ns.exec_cmd(&["ip", "link", "set", PORT1_NAME, "up"]);
70+
ns.exec_cmd(&["ip", "link", "set", PORT2_NAME, "up"]);
71+
72+
// Create HSR interface via ip-rs
73+
let mut args = vec![
74+
"link", "add", HSR_NAME, "type", "hsr", "slave1", PORT1_NAME,
75+
"slave2", PORT2_NAME,
76+
];
77+
args.extend_from_slice(opts);
78+
79+
ns.ip_rs_exec_cmd(&args);
80+
ns.exec_cmd(&["ip", "link", "set", HSR_NAME, "up"]);
81+
82+
test(ns);
83+
});
84+
}

src/ip/link/tests/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ mod bond;
44
mod bridge;
55
mod color;
66
mod dummy;
7+
mod hsr;
78
mod loopback;
89
mod nlmon;
910
mod veth;

0 commit comments

Comments
 (0)