Skip to content

Commit 98d4250

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 98d4250

6 files changed

Lines changed: 150 additions & 0 deletions

File tree

src/ip/link/add.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,9 @@ impl LinkAddCommand {
4848
InfoKind::Nlmon => {
4949
base_conf.apply(LinkNlmon::new(&base_conf.name))?
5050
}
51+
InfoKind::Veth => {
52+
base_conf.apply(base_conf.apply_veth()?)?
53+
}
5154
InfoKind::Vlan => {
5255
base_conf.apply(base_conf.apply_vlan(&handle).await?)?
5356
}

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

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: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
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
75+
let _ = exec_cmd(&["ip", "link", "del", name]);
76+
let _ = exec_cmd(&["ip", "link", "del", peer]);
77+
78+
if let Err(e) = result {
79+
std::panic::resume_unwind(e);
80+
}
81+
}

0 commit comments

Comments
 (0)