about as good as i want idfk yet

This commit is contained in:
lda
2026-04-06 02:39:10 +07:00 Unverified
parent 7eee1e0bab
commit 310b0b462d
7 changed files with 375 additions and 26 deletions
+1 -1
View File
@@ -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"]
+135 -6
View File
@@ -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);
}
}
+101 -14
View File
@@ -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)
}
+116 -2
View File
@@ -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")
);
}
}
+9 -1
View File
@@ -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()),
_ => {}
}
}
+12 -1
View File
@@ -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))