Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -27,4 +27,3 @@ tokio = { version = "1.30", features = ["rt", "net", "time", "macros"] }

[dev-dependencies]
pretty_assertions = "1.4.1"
ctor = "0.5.0"
41 changes: 38 additions & 3 deletions Makefile
Original file line number Diff line number Diff line change
@@ -1,4 +1,39 @@
check:
cargo build;
build:
cargo build

tests_setup: build
sudo ip netns list | grep -qw iproute-rs-test && sudo ip netns del iproute-rs-test || true
sudo ip netns add iproute-rs-test

# Create veth pair and move one end into the test netns
sudo ip link add veth0 type veth peer name veth1
sudo ip link set veth1 netns iproute-rs-test
sudo ip addr add 192.0.2.1/24 dev veth0
sudo ip addr add 22:33::44:55/120 dev veth0
sudo ip link set veth0 up
sudo ip -n iproute-rs-test addr add 192.0.2.2/24 dev veth1
sudo ip -n iproute-rs-test addr add 22:33::44:66/120 dev veth1
sudo ip -n iproute-rs-test link set veth1 up
sudo ip -n iproute-rs-test link set lo up

# create dummy, altname, bridge and vlan inside the test netns
sudo ip -n iproute-rs-test link add dummy0 type dummy
sudo ip -n iproute-rs-test link property add dev dummy0 altname dmmy-zero
sudo ip -n iproute-rs-test link add br0 type bridge
sudo ip -n iproute-rs-test link add link dummy0 name dummy0.1 type vlan id 1
sudo ip -n iproute-rs-test link set dev dummy0.1 master br0

sudo ip -n iproute-rs-test link set dummy0 up
sudo ip -n iproute-rs-test link set dummy0.1 up
sudo ip -n iproute-rs-test link set br0 up

echo "setup network namespace for tests finished"
sudo ip -n iproute-rs-test -c -d link show

tests_teardown:
sudo ip netns del iproute-rs-test

check: tests_setup
env CARGO_TARGET_X86_64_UNKNOWN_LINUX_GNU_RUNNER="sudo" \
cargo test -- --test-threads=1 --show-output $(WHAT) ;
cargo test -- --test-threads=1 --show-output $(WHAT) ; \
sudo ip netns del iproute-rs-test
8 changes: 8 additions & 0 deletions src/color.rs
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,14 @@ impl CliColor {
}
})
}

pub fn address_color(family: &str) -> Self {
match family {
"inet" => Self::Ipv4Addr,
"inet6" => Self::Ipv6Addr,
_ => Self::Clear,
}
}
}

#[macro_export]
Expand Down
63 changes: 63 additions & 0 deletions src/ip/address/cli.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
// SPDX-License-Identifier: MIT

use iproute_rs::CliError;

use crate::link::CliLinkInfo;

use super::show::handle_show;

pub(crate) struct AddressCommand;

impl AddressCommand {
pub(crate) const CMD: &'static str = "address";

pub(crate) fn gen_command() -> clap::Command {
clap::Command::new(Self::CMD)
.about("network address configuration")
.subcommand_required(false)
.subcommand(
clap::Command::new("show")
.about("show links' addresses")
.alias("list")
.alias("lst")
.arg(
clap::Arg::new("options")
.action(clap::ArgAction::Append)
.trailing_var_arg(true),
),
)
.subcommand(
clap::Command::new("add").about("add address to link").arg(
clap::Arg::new("options")
.action(clap::ArgAction::Append)
.trailing_var_arg(true),
),
)
.subcommand(
clap::Command::new("delete").about("delete address from link"),
)
.subcommand(
clap::Command::new("change")
.alias("set")
.about("change device attributes"),
)
}

pub(crate) async fn handle(
matches: &clap::ArgMatches,
) -> Result<Vec<CliLinkInfo>, CliError> {
if let Some(matches) = matches.subcommand_matches("add") {
println!("HAHA {matches:?}");
todo!()
} else if let Some(matches) = matches.subcommand_matches("show") {
let opts: Vec<&str> = matches
.get_many::<String>("options")
.unwrap_or_default()
.map(String::as_str)
.collect();
handle_show(&opts, matches.get_flag("DETAILS")).await
} else {
handle_show(&[], matches.get_flag("DETAILS")).await
}
}
}
10 changes: 10 additions & 0 deletions src/ip/address/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
// SPDX-License-Identifier: MIT

mod cli;
mod show;

#[cfg(test)]
mod tests;

pub(crate) use self::cli::AddressCommand;
pub(crate) use self::show::CliAddressInfo;
259 changes: 259 additions & 0 deletions src/ip/address/show.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,259 @@
// SPDX-License-Identifier: MIT

use std::collections::HashMap;

use crate::{CliError, link::CliLinkInfo};
use futures_util::TryStreamExt;
use iproute_rs::{CanDisplay, CanOutput, CliColor, write_with_color};
use rtnetlink::packet_route::{
AddressFamily,
address::{AddressAttribute, AddressFlags, AddressMessage, AddressScope},
};
use serde::Serialize;

#[derive(Serialize, Default)]
pub(crate) struct CliAddressInfo {
#[serde(skip)]
index: u32,
family: String,
local: String,
prefixlen: u8,
#[serde(skip_serializing_if = "Option::is_none")]
broadcast: Option<String>,
scope: String,
#[serde(skip_serializing_if = "Option::is_none")]
tentative: Option<bool>,
#[serde(skip_serializing_if = "String::is_empty")]
label: String,
valid_life_time: u32,
preferred_life_time: u32,
}

impl std::fmt::Display for CliAddressInfo {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{} ", self.family)?;
write_with_color!(
f,
CliColor::address_color(&self.family),
"{}",
self.local
)?;
write!(f, "/{}", self.prefixlen)?;
if let Some(broadcast) = &self.broadcast {
write!(f, " brd ")?;
write_with_color!(
f,
CliColor::address_color(&self.family),
"{}",
broadcast
)?;
}
write!(f, " scope {} ", self.scope)?;
if Some(true) == self.tentative {
write!(f, "tentative ")?;
}
write!(f, "{}", self.label)?;

write!(
f,
"\n valid_lft {} preferred_lft {}",
if self.valid_life_time == u32::MAX {
"forever".to_string()
} else {
self.valid_life_time.to_string()
},
if self.preferred_life_time == u32::MAX {
"forever".to_string()
} else {
self.preferred_life_time.to_string()
}
)?;
Ok(())
}
}

impl CanDisplay for CliAddressInfo {
fn gen_string(&self) -> String {
self.to_string()
}
}

impl CanOutput for CliAddressInfo {}

fn addr_family_to_cli_string(addr_family: &AddressFamily) -> String {
match addr_family {
AddressFamily::Unspec => "unspec".to_string(),
AddressFamily::Local => "local".to_string(),
AddressFamily::Unix => "unix".to_string(),
AddressFamily::Inet => "inet".to_string(),
AddressFamily::Ax25 => "ax25".to_string(),
AddressFamily::Ipx => "ipx".to_string(),
AddressFamily::Appletalk => "appletalk".to_string(),
AddressFamily::Netrom => "netrom".to_string(),
AddressFamily::Bridge => "bridge".to_string(),
AddressFamily::Atmpvc => "atmpvc".to_string(),
AddressFamily::X25 => "x25".to_string(),
AddressFamily::Inet6 => "inet6".to_string(),
AddressFamily::Rose => "rose".to_string(),
AddressFamily::Decnet => "decnet".to_string(),
AddressFamily::Netbeui => "netbeui".to_string(),
AddressFamily::Security => "security".to_string(),
AddressFamily::Key => "key".to_string(),
AddressFamily::Route => "route".to_string(),
AddressFamily::Netlink => "netlink".to_string(),
AddressFamily::Packet => "packet".to_string(),
AddressFamily::Ash => "ash".to_string(),
AddressFamily::Econet => "econet".to_string(),
AddressFamily::Atmsvc => "atmsvc".to_string(),
AddressFamily::Rds => "rds".to_string(),
AddressFamily::Sna => "sna".to_string(),
AddressFamily::Irda => "irda".to_string(),
AddressFamily::Pppox => "pppox".to_string(),
AddressFamily::Wanpipe => "wanpipe".to_string(),
AddressFamily::Llc => "llc".to_string(),
#[cfg(not(target_os = "android"))]
AddressFamily::Ib => "ib".to_string(),
#[cfg(not(target_os = "android"))]
AddressFamily::Mpls => "mpls".to_string(),
AddressFamily::Can => "can".to_string(),
AddressFamily::Tipc => "tipc".to_string(),
AddressFamily::Bluetooth => "bluetooth".to_string(),
AddressFamily::Iucv => "iucv".to_string(),
AddressFamily::Rxrpc => "rxrpc".to_string(),
AddressFamily::Isdn => "isdn".to_string(),
AddressFamily::Phonet => "phonet".to_string(),
AddressFamily::Ieee802154 => "ieee802154".to_string(),
AddressFamily::Caif => "caif".to_string(),
AddressFamily::Alg => "alg".to_string(),
AddressFamily::Nfc => "nfc".to_string(),
AddressFamily::Vsock => "vsock".to_string(),
AddressFamily::Kcm => "kcm".to_string(),
AddressFamily::Qipcrtr => "qipcrtr".to_string(),
AddressFamily::Smc => "smc".to_string(),
AddressFamily::Xdp => "xdp".to_string(),
AddressFamily::Mctp => "mctp".to_string(),
AddressFamily::Other(_) | _ => "unwnown".to_string(),
}
}

fn addr_scope_to_cli_string(addr_scope: &AddressScope) -> String {
match addr_scope {
AddressScope::Universe => "global",
AddressScope::Site => "site",
AddressScope::Link => "link",
AddressScope::Host => "host",
AddressScope::Nowhere => "nowhere",
AddressScope::Other(_) | _ => "unknown",
}
.to_string()
}

fn parse_nl_msg_to_address(
nl_msg: AddressMessage,
) -> Result<CliAddressInfo, CliError> {
let index = nl_msg.header.index;
let family = addr_family_to_cli_string(&nl_msg.header.family);
let mut local = String::new();
let prefixlen = nl_msg.header.prefix_len;
let mut broadcast = None;
let scope = addr_scope_to_cli_string(&nl_msg.header.scope);
let mut tentative = None;
let mut label = String::new();
let mut valid_life_time = u32::MAX;
let mut preferred_life_time = u32::MAX;

for nla in nl_msg.attributes {
match nla {
AddressAttribute::Local(a) => {
local = a.to_string();
}
AddressAttribute::Address(a) => {
if local.is_empty() {
local = a.to_string();
}
}
AddressAttribute::Broadcast(a) => {
broadcast = Some(a.to_string());
}
AddressAttribute::Label(s) => {
label = s;
}
AddressAttribute::CacheInfo(c) => {
valid_life_time = c.ifa_valid;
preferred_life_time = c.ifa_preferred;
}
AddressAttribute::Flags(f) => {
// If there is no tentative flag the field should be None
tentative = (nl_msg.header.family == AddressFamily::Inet6
&& f.contains(AddressFlags::Tentative))
.then_some(true);
}
_ => {
// println!("Remains {:?}", nla);
}
}
}

Ok(CliAddressInfo {
index,
family,
local,
prefixlen,
broadcast,
scope,
tentative,
label,
valid_life_time,
preferred_life_time,
})
}

pub(crate) async fn handle_show(
opts: &[&str],
include_details: bool,
) -> Result<Vec<CliLinkInfo>, CliError> {
let (connection, handle, _) = rtnetlink::new_connection()?;

tokio::spawn(connection);

let mut address_get_handle = handle.address().get();
// let mut link_get_handle = handle.link().get();

if let Some(iface_name) = opts.first() {
let link_get_handle =
handle.link().get().match_name(iface_name.to_string());
let link =
link_get_handle.execute().try_next().await?.ok_or_else(|| {
CliError::from(
format!("Device \"{iface_name}\" does not exist.").as_str(),
)
})?;
address_get_handle =
address_get_handle.set_link_index_filter(link.header.index);
}

let mut addresses = address_get_handle.execute();
let mut addresses_infos: Vec<CliAddressInfo> = Vec::new();

while let Some(nl_msg) = addresses.try_next().await? {
addresses_infos.push(parse_nl_msg_to_address(nl_msg)?);
}

let mut links_info: HashMap<u32, _> =
crate::link::handle_show(opts, include_details)
.await?
.into_iter()
.map(|link_info| (link_info.get_ifindex(), link_info))
.collect();

for addr_info in addresses_infos {
if let Some(link_info) = links_info.get_mut(&addr_info.index) {
link_info.add_address(addr_info);
}
}

let mut result: Vec<CliLinkInfo> = links_info.into_values().collect();
result.sort_by_key(|link| link.get_ifindex());

Ok(result)
}
Loading