Skip to content

Commit 865434f

Browse files
committed
support veth link type
Introduce veth (virtual ethernet) interface support for both creation and query. Usage: ip link add NAME type veth peer PEER_NAME The peer name is parsed from iface_specific. The display path extracts the peer name from InfoVeth::Peer LinkMessage attributes and shows it as 'peer <name>' in detailed output. Integration tests cover basic show, detailed show, JSON show, and detailed JSON show. Signed-off-by: Gris Ge <cnfourt@gmail.com>
1 parent 40f12dd commit 865434f

6 files changed

Lines changed: 149 additions & 0 deletions

File tree

src/ip/link/add.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ impl LinkAddCommand {
4848
InfoKind::Nlmon => {
4949
base_conf.apply(LinkNlmon::new(&base_conf.name))?
5050
}
51+
InfoKind::Veth => base_conf.apply(base_conf.apply_veth()?)?,
5152
InfoKind::Vlan => {
5253
base_conf.apply(base_conf.apply_vlan(&handle).await?)?
5354
}

src/ip/link/ifaces/mod.rs

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

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

src/ip/link/ifaces/veth.rs

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
// SPDX-License-Identifier: MIT
2+
3+
use iproute_rs::CliError;
4+
use rtnetlink::{
5+
LinkMessageBuilder, LinkVeth,
6+
packet_route::link::{InfoVeth, LinkAttribute},
7+
};
8+
use serde::Serialize;
9+
10+
use crate::link::LinkBaseConf;
11+
12+
#[derive(Serialize)]
13+
pub(crate) struct CliLinkInfoDataVeth {
14+
peer: String,
15+
}
16+
17+
impl From<&InfoVeth> for CliLinkInfoDataVeth {
18+
fn from(info: &InfoVeth) -> Self {
19+
let mut peer = String::new();
20+
if let InfoVeth::Peer(msg) = info {
21+
for attr in &msg.attributes {
22+
if let LinkAttribute::IfName(name) = attr {
23+
peer = name.clone();
24+
break;
25+
}
26+
}
27+
}
28+
Self { peer }
29+
}
30+
}
31+
32+
impl LinkBaseConf {
33+
pub(crate) fn apply_veth(
34+
&self,
35+
) -> Result<LinkMessageBuilder<LinkVeth>, CliError> {
36+
let mut iter = self.iface_specific.iter();
37+
match iter.next() {
38+
Some(v) if v == "peer" => {}
39+
Some(other) => {
40+
return Err(CliError::from(format!(
41+
"veth expects peer argument, got {other}"
42+
)));
43+
}
44+
None => {
45+
return Err(CliError::from("veth requires peer argument"));
46+
}
47+
}
48+
let Some(peer) = iter.next() else {
49+
return Err(CliError::from("veth peer requires a value"));
50+
};
51+
Ok(LinkVeth::new(&self.name, peer))
52+
}
53+
}
54+
55+
impl std::fmt::Display for CliLinkInfoDataVeth {
56+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
57+
if !self.peer.is_empty() {
58+
write!(f, "peer {}", self.peer)?;
59+
}
60+
Ok(())
61+
}
62+
}

src/ip/link/link_info.rs

Lines changed: 4 additions & 0 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+
veth::CliLinkInfoDataVeth,
1011
vlan::CliLinkInfoDataVlan,
1112
vxlan::CliLinkInfoDataVxlan,
1213
};
@@ -96,6 +97,7 @@ impl std::fmt::Display for CliLinkInfo {
9697
#[serde(untagged)]
9798
pub(crate) enum CliLinkInfoData {
9899
Vlan(Box<CliLinkInfoDataVlan>),
100+
Veth(Box<CliLinkInfoDataVeth>),
99101
Bridge(Box<CliLinkInfoDataBridge>),
100102
Bond(Box<CliLinkInfoDataBond>),
101103
Vxlan(Box<CliLinkInfoDataVxlan>),
@@ -110,6 +112,7 @@ impl TryFrom<&InfoData> for CliLinkInfoData {
110112
Ok(Self::Bridge(Box::new(v.as_slice().into())))
111113
}
112114
InfoData::Vlan(v) => Ok(Self::Vlan(Box::new(v.as_slice().into()))),
115+
InfoData::Veth(v) => Ok(Self::Veth(Box::new(v.into()))),
113116
InfoData::Bond(v) => Ok(Self::Bond(Box::new(v.as_slice().into()))),
114117
InfoData::Vxlan(v) => {
115118
Ok(Self::Vxlan(Box::new(v.as_slice().into())))
@@ -134,6 +137,7 @@ impl std::fmt::Display for CliLinkInfoData {
134137
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
135138
match self {
136139
CliLinkInfoData::Vlan(v) => write!(f, "{v}"),
140+
CliLinkInfoData::Veth(v) => write!(f, "{v}"),
137141
CliLinkInfoData::Bridge(v) => write!(f, "{v}"),
138142
CliLinkInfoData::Bond(v) => write!(f, "{v}"),
139143
CliLinkInfoData::Vxlan(v) => write!(f, "{v}"),

src/ip/link/tests/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,5 +6,6 @@ mod color;
66
mod dummy;
77
mod loopback;
88
mod nlmon;
9+
mod veth;
910
mod vlan;
1011
mod vxlan;

src/ip/link/tests/veth.rs

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
// SPDX-License-Identifier: MIT
2+
3+
use crate::tests::{exec_cmd, ip_rs_exec_cmd};
4+
5+
#[test]
6+
fn test_link_show_veth() {
7+
let ifname = "tveth0";
8+
let peer = "tveth0_peer";
9+
10+
with_veth_iface(ifname, peer, || {
11+
let expected_output = exec_cmd(&["ip", "link", "show", ifname]);
12+
13+
let our_output = ip_rs_exec_cmd(&["link", "show", ifname]);
14+
15+
pretty_assertions::assert_eq!(expected_output, our_output);
16+
})
17+
}
18+
19+
#[test]
20+
fn test_link_detailed_show_veth() {
21+
let ifname = "tveth1";
22+
let peer = "tveth1_peer";
23+
24+
with_veth_iface(ifname, peer, || {
25+
let expected_output = exec_cmd(&["ip", "-d", "link", "show", ifname]);
26+
27+
let our_output = ip_rs_exec_cmd(&["-d", "link", "show", ifname]);
28+
29+
pretty_assertions::assert_eq!(expected_output, our_output);
30+
})
31+
}
32+
33+
#[test]
34+
fn test_link_show_veth_json() {
35+
let ifname = "tveth2";
36+
let peer = "tveth2_peer";
37+
38+
with_veth_iface(ifname, peer, || {
39+
let expected_output = exec_cmd(&["ip", "-j", "link", "show", ifname]);
40+
41+
let our_output = ip_rs_exec_cmd(&["-j", "link", "show", ifname]);
42+
43+
pretty_assertions::assert_eq!(expected_output, our_output);
44+
})
45+
}
46+
47+
#[test]
48+
fn test_link_detailed_show_veth_json() {
49+
let ifname = "tveth3";
50+
let peer = "tveth3_peer";
51+
52+
with_veth_iface(ifname, peer, || {
53+
let expected_output =
54+
exec_cmd(&["ip", "-d", "-j", "link", "show", ifname]);
55+
56+
let our_output = ip_rs_exec_cmd(&["-d", "-j", "link", "show", ifname]);
57+
58+
pretty_assertions::assert_eq!(expected_output, our_output);
59+
})
60+
}
61+
62+
fn with_veth_iface<T>(name: &str, peer: &str, test: T)
63+
where
64+
T: FnOnce() + std::panic::UnwindSafe,
65+
{
66+
ip_rs_exec_cmd(&["link", "add", name, "type", "veth", "peer", peer]);
67+
exec_cmd(&["ip", "link", "set", name, "up"]);
68+
exec_cmd(&["ip", "link", "set", peer, "up"]);
69+
70+
let result = std::panic::catch_unwind(|| {
71+
test();
72+
});
73+
74+
// clean up (deleting veth removes both ends)
75+
let _ = exec_cmd(&["ip", "link", "del", name]);
76+
77+
if let Err(e) = result {
78+
std::panic::resume_unwind(e);
79+
}
80+
}

0 commit comments

Comments
 (0)