about as good as i want idfk yet
This commit is contained in:
+1
-1
@@ -1,7 +1,7 @@
|
||||
[package]
|
||||
name = "lda-ipjs"
|
||||
description = "ip -j show schemas"
|
||||
version = "0.0.2"
|
||||
version = "0.0.3"
|
||||
edition = "2024"
|
||||
publish = ["gitea"]
|
||||
|
||||
|
||||
@@ -11,7 +11,10 @@ pub mod nl;
|
||||
pub use crate::subcommands::Backend;
|
||||
use crate::utils::serialize::mac::option_mac;
|
||||
use macaddr::MacAddr;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde::{Deserialize, Deserializer, Serialize, Serializer};
|
||||
use std::net::{IpAddr, Ipv4Addr};
|
||||
|
||||
use crate::subcommands::link::OperState;
|
||||
|
||||
/// i dont include what i dont know about (almost all ts)
|
||||
#[derive(Serialize, Debug, Deserialize)]
|
||||
@@ -19,7 +22,7 @@ pub struct AddrOutput {
|
||||
pub ifindex: u32,
|
||||
pub ifname: String,
|
||||
/// i imagine UP or DOWN, unknown
|
||||
pub operstate: String,
|
||||
pub operstate: OperState,
|
||||
// 6 has a serde and the enum doesnt? why. (serializing ts is ass although... im not given an array. they string formatted ts)
|
||||
#[serde(with = "option_mac", default)]
|
||||
pub address: Option<MacAddr>,
|
||||
@@ -31,18 +34,102 @@ pub struct AddrOutput {
|
||||
// Raw JSON shape from ip -j -4 address show
|
||||
#[derive(Debug, Deserialize, Serialize)]
|
||||
pub struct AddrInfo {
|
||||
pub family: Option<String>,
|
||||
pub family: Option<AddressFamily>,
|
||||
|
||||
pub local: Option<String>,
|
||||
pub prefixlen: Option<u8>,
|
||||
#[serde(flatten, default)]
|
||||
pub cidr: InterfaceCidr,
|
||||
|
||||
pub broadcast: Option<String>,
|
||||
pub broadcast: Option<Ipv4Addr>,
|
||||
|
||||
pub scope: Option<String>,
|
||||
pub label: Option<String>,
|
||||
// many more exist; we only take what we need
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum AddressFamily {
|
||||
Inet,
|
||||
Inet6,
|
||||
Other,
|
||||
}
|
||||
|
||||
impl AddressFamily {
|
||||
pub fn parse_lossy(value: &str) -> Self {
|
||||
match value.to_ascii_lowercase().as_str() {
|
||||
"inet" => Self::Inet,
|
||||
"inet6" => Self::Inet6,
|
||||
_ => Self::Other,
|
||||
}
|
||||
}
|
||||
|
||||
pub const fn as_str(self) -> &'static str {
|
||||
match self {
|
||||
Self::Inet => "inet",
|
||||
Self::Inet6 => "inet6",
|
||||
Self::Other => "other",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Serialize for AddressFamily {
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: Serializer,
|
||||
{
|
||||
serializer.serialize_str(self.as_str())
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de> Deserialize<'de> for AddressFamily {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
let value = String::deserialize(deserializer)?;
|
||||
Ok(Self::parse_lossy(&value))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Clone, Deserialize, Serialize)]
|
||||
pub struct InterfaceCidr {
|
||||
pub local: Option<IpAddr>,
|
||||
pub prefixlen: Option<u8>,
|
||||
}
|
||||
|
||||
impl InterfaceCidr {
|
||||
pub fn is_complete(&self) -> bool {
|
||||
self.local.is_some() && self.prefixlen.is_some()
|
||||
}
|
||||
}
|
||||
|
||||
impl AddrInfo {
|
||||
pub fn local_addr(&self) -> Option<IpAddr> {
|
||||
self.cidr.local
|
||||
}
|
||||
|
||||
pub fn prefixlen(&self) -> Option<u8> {
|
||||
self.cidr.prefixlen
|
||||
}
|
||||
|
||||
pub fn is_ipv4(&self) -> bool {
|
||||
matches!(self.family, Some(AddressFamily::Inet))
|
||||
}
|
||||
|
||||
pub fn is_ipv6(&self) -> bool {
|
||||
matches!(self.family, Some(AddressFamily::Inet6))
|
||||
}
|
||||
}
|
||||
|
||||
impl AddrOutput {
|
||||
pub fn ipv4_addrs(&self) -> impl Iterator<Item = &AddrInfo> {
|
||||
self.addr_info.iter().filter(|info| info.is_ipv4())
|
||||
}
|
||||
|
||||
pub fn ipv6_addrs(&self) -> impl Iterator<Item = &AddrInfo> {
|
||||
self.addr_info.iter().filter(|info| info.is_ipv6())
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn get(dev: Option<&str>) -> anyhow::Result<Vec<AddrOutput>> {
|
||||
get_with_backend(Backend::Json, dev).await
|
||||
}
|
||||
@@ -57,3 +144,45 @@ pub async fn get_with_backend(
|
||||
Backend::Netlink => nl::get(dev).await,
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::net::{IpAddr, Ipv4Addr};
|
||||
|
||||
use super::{AddrInfo, AddrOutput, AddressFamily};
|
||||
use crate::subcommands::link::OperState;
|
||||
|
||||
#[test]
|
||||
fn address_family_parses_known_values() {
|
||||
assert_eq!(AddressFamily::parse_lossy("inet"), AddressFamily::Inet);
|
||||
assert_eq!(AddressFamily::parse_lossy("INET6"), AddressFamily::Inet6);
|
||||
assert_eq!(AddressFamily::parse_lossy("weird"), AddressFamily::Other);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn addr_info_deserializes_typed_ip_fields() {
|
||||
let info: AddrInfo = serde_json::from_str(
|
||||
r#"{"family":"inet","local":"192.168.1.1","prefixlen":24,"broadcast":"192.168.1.255","scope":"global"}"#,
|
||||
)
|
||||
.expect("addr_info json should deserialize");
|
||||
|
||||
assert_eq!(info.family, Some(AddressFamily::Inet));
|
||||
assert_eq!(
|
||||
info.local_addr(),
|
||||
Some(IpAddr::V4(Ipv4Addr::new(192, 168, 1, 1)))
|
||||
);
|
||||
assert_eq!(info.prefixlen(), Some(24));
|
||||
assert_eq!(info.broadcast, Some(Ipv4Addr::new(192, 168, 1, 255)));
|
||||
assert!(info.cidr.is_complete());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn addr_output_deserializes_typed_operstate() {
|
||||
let output: AddrOutput = serde_json::from_str(
|
||||
r#"{"ifindex":2,"ifname":"br-lan","operstate":"UP","address":"aa:bb:cc:dd:ee:ff","addr_info":[]}"#,
|
||||
)
|
||||
.expect("addr_output json should deserialize");
|
||||
|
||||
assert_eq!(output.operstate, OperState::Up);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,22 +1,109 @@
|
||||
//! i said i aint doing ts no more why am i still here
|
||||
#![cfg(unix)]
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
use futures::TryStreamExt;
|
||||
use rtnetlink::Handle;
|
||||
use rtnetlink::packet_route::{AddressFamily, address::AddressAttribute};
|
||||
|
||||
use crate::subcommands::address::AddrOutput;
|
||||
use crate::subcommands::{
|
||||
address::{AddrInfo, AddrOutput, AddressFamily as IpAddressFamily, InterfaceCidr},
|
||||
link,
|
||||
};
|
||||
|
||||
// shit this one is even worse you needa collect info from two places
|
||||
pub async fn get(dev: Option<&str>) -> anyhow::Result<Vec<AddrOutput>> {
|
||||
let (conn, handle, _) = rtnetlink::new_connection()?;
|
||||
tokio::spawn(conn); // every time?
|
||||
let mut address = handle.address().get();
|
||||
let mut link = handle.link().get();
|
||||
if let Some(dev) = dev {
|
||||
link = link.match_name(dev.to_owned());
|
||||
if let Some(ind) = link.execute().try_next().await?.map(|a| a.header.index) {
|
||||
address = address.set_link_index_filter(ind);
|
||||
};
|
||||
address.execute().try_next().await?;
|
||||
} else {
|
||||
};
|
||||
todo!();
|
||||
tokio::spawn(conn);
|
||||
|
||||
get_with_handle(&handle, dev).await
|
||||
}
|
||||
|
||||
pub async fn get_with_handle(
|
||||
handle: &Handle,
|
||||
dev: Option<&str>,
|
||||
) -> anyhow::Result<Vec<AddrOutput>> {
|
||||
let links = link::nl::get_with_handle(handle, dev).await?;
|
||||
let link_by_index: BTreeMap<u32, crate::subcommands::link::LinkOutput> =
|
||||
links.into_iter().map(|link| (link.ifindex, link)).collect();
|
||||
|
||||
let mut address = handle.address().get();
|
||||
if let Some(dev) = dev {
|
||||
if let Some(index) = link_by_index
|
||||
.values()
|
||||
.find(|link| link.ifname == dev)
|
||||
.map(|link| link.ifindex)
|
||||
{
|
||||
address = address.set_link_index_filter(index);
|
||||
} else {
|
||||
return Ok(Vec::new());
|
||||
}
|
||||
}
|
||||
|
||||
let mut stream = address.execute();
|
||||
let mut addr_info_by_index: BTreeMap<u32, Vec<AddrInfo>> = BTreeMap::new();
|
||||
|
||||
while let Some(msg) = stream.try_next().await? {
|
||||
if !matches!(
|
||||
msg.header.family,
|
||||
AddressFamily::Inet | AddressFamily::Inet6
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let family = match msg.header.family {
|
||||
AddressFamily::Inet => Some(IpAddressFamily::Inet),
|
||||
AddressFamily::Inet6 => Some(IpAddressFamily::Inet6),
|
||||
_ => None,
|
||||
};
|
||||
|
||||
let mut local = None;
|
||||
let mut prefixlen = Some(msg.header.prefix_len);
|
||||
let mut broadcast = None;
|
||||
let mut scope = Some(format!("{:?}", msg.header.scope).to_lowercase());
|
||||
let mut label = None;
|
||||
|
||||
for attr in msg.attributes {
|
||||
match attr {
|
||||
AddressAttribute::Address(addr) | AddressAttribute::Local(addr) => {
|
||||
if local.is_none() {
|
||||
local = addr.to_string().parse().ok();
|
||||
}
|
||||
}
|
||||
AddressAttribute::Broadcast(addr) => {
|
||||
broadcast = addr.to_string().parse().ok();
|
||||
}
|
||||
AddressAttribute::Label(name) => {
|
||||
label = Some(name);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
addr_info_by_index
|
||||
.entry(msg.header.index)
|
||||
.or_default()
|
||||
.push(AddrInfo {
|
||||
family,
|
||||
cidr: InterfaceCidr {
|
||||
local,
|
||||
prefixlen: prefixlen.take(),
|
||||
},
|
||||
broadcast,
|
||||
scope: scope.take(),
|
||||
label,
|
||||
});
|
||||
}
|
||||
|
||||
let out = link_by_index
|
||||
.into_values()
|
||||
.map(|link| AddrOutput {
|
||||
ifindex: link.ifindex,
|
||||
ifname: link.ifname,
|
||||
operstate: link.operstate.unwrap_or(link::OperState::Unknown),
|
||||
address: link.address,
|
||||
addr_info: addr_info_by_index.remove(&link.ifindex).unwrap_or_default(),
|
||||
})
|
||||
.collect();
|
||||
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
@@ -7,18 +7,99 @@ pub mod nl;
|
||||
pub use crate::subcommands::Backend;
|
||||
use crate::utils::serialize::mac::option_mac;
|
||||
use macaddr::MacAddr;
|
||||
use serde::{Deserialize, Serialize};
|
||||
#[cfg(all(unix, feature = "experimental-nl"))]
|
||||
use rtnetlink::packet_route::link::State as NetlinkOperState;
|
||||
use serde::{Deserialize, Deserializer, Serialize, Serializer};
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct LinkOutput {
|
||||
pub ifindex: u32,
|
||||
pub ifname: String,
|
||||
#[serde(default)]
|
||||
pub operstate: Option<String>,
|
||||
pub operstate: Option<OperState>,
|
||||
#[serde(default, with = "option_mac")]
|
||||
pub address: Option<MacAddr>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum OperState {
|
||||
Up,
|
||||
Down,
|
||||
Unknown,
|
||||
Dormant,
|
||||
LowerLayerDown,
|
||||
NotPresent,
|
||||
Testing,
|
||||
Other,
|
||||
}
|
||||
|
||||
impl OperState {
|
||||
pub fn parse_lossy(value: &str) -> Self {
|
||||
match value.to_ascii_uppercase().as_str() {
|
||||
"UP" => Self::Up,
|
||||
"DOWN" => Self::Down,
|
||||
"UNKNOWN" => Self::Unknown,
|
||||
"DORMANT" => Self::Dormant,
|
||||
"LOWERLAYERDOWN" | "LOWER_LAYER_DOWN" | "LOWERLAYER_DOWN" => Self::LowerLayerDown,
|
||||
"NOTPRESENT" | "NOT_PRESENT" => Self::NotPresent,
|
||||
"TESTING" => Self::Testing,
|
||||
_ => Self::Other,
|
||||
}
|
||||
}
|
||||
|
||||
pub const fn as_str(self) -> &'static str {
|
||||
match self {
|
||||
Self::Up => "UP",
|
||||
Self::Down => "DOWN",
|
||||
Self::Unknown => "UNKNOWN",
|
||||
Self::Dormant => "DORMANT",
|
||||
Self::LowerLayerDown => "LOWERLAYERDOWN",
|
||||
Self::NotPresent => "NOTPRESENT",
|
||||
Self::Testing => "TESTING",
|
||||
Self::Other => "OTHER",
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_up(self) -> bool {
|
||||
matches!(self, Self::Up)
|
||||
}
|
||||
}
|
||||
|
||||
impl Serialize for OperState {
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: Serializer,
|
||||
{
|
||||
serializer.serialize_str(self.as_str())
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de> Deserialize<'de> for OperState {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
let value = String::deserialize(deserializer)?;
|
||||
Ok(Self::parse_lossy(&value))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(all(unix, feature = "experimental-nl"))]
|
||||
impl From<NetlinkOperState> for OperState {
|
||||
fn from(value: NetlinkOperState) -> Self {
|
||||
match value {
|
||||
NetlinkOperState::Up => Self::Up,
|
||||
NetlinkOperState::Down => Self::Down,
|
||||
NetlinkOperState::Unknown => Self::Unknown,
|
||||
NetlinkOperState::Dormant => Self::Dormant,
|
||||
NetlinkOperState::LowerLayerDown => Self::LowerLayerDown,
|
||||
NetlinkOperState::NotPresent => Self::NotPresent,
|
||||
NetlinkOperState::Testing => Self::Testing,
|
||||
_ => Self::Other,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn get(dev: Option<&str>) -> anyhow::Result<Vec<LinkOutput>> {
|
||||
get_with_backend(Backend::Json, dev).await
|
||||
}
|
||||
@@ -33,3 +114,36 @@ pub async fn get_with_backend(
|
||||
Backend::Netlink => nl::get(dev).await,
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{LinkOutput, OperState};
|
||||
|
||||
#[test]
|
||||
fn operstate_parses_json_and_netlink_spellings() {
|
||||
assert_eq!(OperState::parse_lossy("UP"), OperState::Up);
|
||||
assert_eq!(OperState::parse_lossy("Up"), OperState::Up);
|
||||
assert_eq!(
|
||||
OperState::parse_lossy("LOWERLAYERDOWN"),
|
||||
OperState::LowerLayerDown
|
||||
);
|
||||
assert_eq!(
|
||||
OperState::parse_lossy("LowerLayerDown"),
|
||||
OperState::LowerLayerDown
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn link_output_deserializes_typed_operstate() {
|
||||
let link: LinkOutput = serde_json::from_str(
|
||||
r#"{"ifindex":1,"ifname":"eth0","operstate":"UP","address":"aa:bb:cc:dd:ee:ff"}"#,
|
||||
)
|
||||
.expect("link json should deserialize");
|
||||
|
||||
assert_eq!(link.operstate, Some(OperState::Up));
|
||||
assert_eq!(
|
||||
link.address.map(|mac| mac.to_string()).as_deref(),
|
||||
Some("aa:bb:cc:dd:ee:ff")
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#![cfg(unix)]
|
||||
|
||||
use futures::TryStreamExt;
|
||||
use rtnetlink::Handle;
|
||||
use rtnetlink::packet_route::link::LinkAttribute;
|
||||
|
||||
use super::LinkOutput;
|
||||
@@ -9,6 +10,13 @@ pub async fn get(dev: Option<&str>) -> anyhow::Result<Vec<LinkOutput>> {
|
||||
let (conn, handle, _) = rtnetlink::new_connection()?;
|
||||
tokio::spawn(conn);
|
||||
|
||||
get_with_handle(&handle, dev).await
|
||||
}
|
||||
|
||||
pub async fn get_with_handle(
|
||||
handle: &Handle,
|
||||
dev: Option<&str>,
|
||||
) -> anyhow::Result<Vec<LinkOutput>> {
|
||||
let mut req = handle.link().get();
|
||||
if let Some(dev) = dev {
|
||||
req = req.match_name(dev.to_owned());
|
||||
@@ -32,7 +40,7 @@ pub async fn get(dev: Option<&str>) -> anyhow::Result<Vec<LinkOutput>> {
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
LinkAttribute::OperState(state) => operstate = Some(format!("{state:?}")),
|
||||
LinkAttribute::OperState(state) => operstate = Some(state.into()),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ use std::{
|
||||
|
||||
use futures::TryStreamExt;
|
||||
use macaddr::MacAddr;
|
||||
use rtnetlink::Handle;
|
||||
use rtnetlink::packet_route::{
|
||||
AddressFamily,
|
||||
neighbour::{NeighbourAddress, NeighbourAttribute, NeighbourState},
|
||||
@@ -27,6 +28,16 @@ pub async fn get(
|
||||
let (conn, handle, _) = rtnetlink::new_connection()?;
|
||||
tokio::spawn(conn);
|
||||
|
||||
get_with_handle(&handle, ips, devs, nuds, macs).await
|
||||
}
|
||||
|
||||
pub async fn get_with_handle(
|
||||
handle: &Handle,
|
||||
ips: &[IpAddr],
|
||||
devs: &[impl AsRef<str>],
|
||||
nuds: &[NUDState],
|
||||
macs: &[MacAddr],
|
||||
) -> anyhow::Result<Vec<NeighborItem>> {
|
||||
let mut neighbor_data = handle.neighbours().get().execute();
|
||||
|
||||
// Build filter sets (empty = match all)
|
||||
@@ -36,7 +47,7 @@ pub async fn get(
|
||||
let mac_set: HashSet<MacAddr> = macs.iter().copied().collect();
|
||||
|
||||
// Prefetch all links once; link lookups were the ugliest and most expensive part.
|
||||
let ifname_cache: HashMap<u32, String> = link::nl::get(None)
|
||||
let ifname_cache: HashMap<u32, String> = link::nl::get_with_handle(handle, None)
|
||||
.await?
|
||||
.into_iter()
|
||||
.map(|link| (link.ifindex, link.ifname))
|
||||
|
||||
Reference in New Issue
Block a user