"claude do it" type shi
This commit is contained in:
lda
2026-02-05 03:28:10 +07:00 Unverified
parent 2831864d73
commit 59f5a893fc
7 changed files with 123 additions and 230 deletions
Generated
+1 -1
View File
@@ -627,7 +627,7 @@ checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
[[package]] [[package]]
name = "lda-ipjs" name = "lda-ipjs"
version = "0.0.1" version = "0.0.2"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"futures", "futures",
+1 -1
View File
@@ -1,7 +1,7 @@
[package] [package]
name = "lda-ipjs" name = "lda-ipjs"
description = "ip -j show schemas" description = "ip -j show schemas"
version = "0.0.1" version = "0.0.2"
edition = "2024" edition = "2024"
publish = ["gitea"] publish = ["gitea"]
+1
View File
@@ -0,0 +1 @@
//! this is for link. You need link; at least to build an index -> name map. i Need It. sometimes.
+76 -79
View File
@@ -1,6 +1,5 @@
//! this is purely experimental. im not doing ts no mo //! rtnetlink-based neighbor table query. One syscall, filter in userspace.
// hallo
use std::{ use std::{
collections::{HashMap, HashSet}, collections::{HashMap, HashSet},
net::IpAddr, net::IpAddr,
@@ -16,115 +15,113 @@ use rtnetlink::packet_route::{
use super::{NUDState, NeighborItem}; use super::{NUDState, NeighborItem};
// dont you love https://github.com/rust-netlink/rtnetlink/blob/main/examples/get_neighbours.rs /// Fetch neighbors via rtnetlink. Empty slice = no filter (match all).
// NeighborItem.state guarantees to be a single thing. /// Non-empty slice = match ANY in the set.
// https://github.com/rust-netlink/rtnetlink/blob/main/examples/get_neighbours.rs
pub async fn get( pub async fn get(
ip: Option<IpAddr>, ips: &[IpAddr],
dev: Option<&str>, devs: &[impl AsRef<str>],
nud: &[NUDState], nuds: &[NUDState],
macs: &[MacAddr],
) -> anyhow::Result<Vec<NeighborItem>> { ) -> anyhow::Result<Vec<NeighborItem>> {
let (gip, gdev, gnud) = (ip, dev, nud);
let (conn, handle, _) = rtnetlink::new_connection()?; let (conn, handle, _) = rtnetlink::new_connection()?;
tokio::spawn(conn); // every time? tokio::spawn(conn);
let mut neighbor_data = handle.neighbours().get().execute();
let nudset: HashSet<&NUDState> = HashSet::from_iter(gnud);
// map ifindex to name let mut neighbor_data = handle.neighbours().get().execute();
let mut ball: HashMap<u32, String> = HashMap::new();
// Build filter sets (empty = match all)
let ip_set: HashSet<IpAddr> = ips.iter().copied().collect();
let dev_set: HashSet<&str> = devs.iter().map(AsRef::as_ref).collect();
let nud_set: HashSet<&NUDState> = nuds.iter().collect();
let mac_set: HashSet<MacAddr> = macs.iter().copied().collect();
// Cache ifindex -> name
let mut ifname_cache: HashMap<u32, String> = HashMap::new();
let mut result = vec![]; let mut result = vec![];
'big: while let Some(neighbour_message_item) = neighbor_data.try_next().await? {
// Filter by address family 'row: while let Some(msg) = neighbor_data.try_next().await? {
// Only IPv4/IPv6, skip NOARP
if !matches!( if !matches!(
neighbour_message_item.header.family, msg.header.family,
AddressFamily::Inet | AddressFamily::Inet6 AddressFamily::Inet | AddressFamily::Inet6
) || matches!(neighbour_message_item.header.state, NeighbourState::Noarp) ) || matches!(msg.header.state, NeighbourState::Noarp)
// copilot says this to match ip -j n s
{ {
continue 'big; continue 'row;
} }
let state = vec![ let state: NUDState = msg.header.state.try_into().unwrap_or_default();
neighbour_message_item
.header
.state
.try_into()
.unwrap_or_default(),
]; // ONE ITEM. why tf ts design json.
let mut ip = None; let mut ip = None;
let mut mac = None; let mut mac = None;
for neigh_attr in neighbour_message_item.attributes { for attr in msg.attributes {
match neigh_attr { match attr {
NeighbourAttribute::Destination(neighbour_address) => match neighbour_address { NeighbourAttribute::Destination(addr) => match addr {
NeighbourAddress::Inet(ipv4_addr) => ip = Some(ipv4_addr.into()), NeighbourAddress::Inet(v4) => ip = Some(IpAddr::from(v4)),
NeighbourAddress::Inet6(ipv6_addr) => ip = Some(ipv6_addr.into()), NeighbourAddress::Inet6(v6) => ip = Some(IpAddr::from(v6)),
_ => continue 'big, _ => continue 'row,
}, },
NeighbourAttribute::LinkLocalAddress(items) => { NeighbourAttribute::LinkLocalAddress(bytes) => {
mac = match items.len() { mac = match bytes.len() {
6 => items.first_chunk::<6>().map(|&e| MacAddr::from(e)), 6 => bytes.first_chunk::<6>().map(|&b| MacAddr::from(b)),
8 => items.first_chunk::<8>().map(|&e| MacAddr::from(e)), 8 => bytes.first_chunk::<8>().map(|&b| MacAddr::from(b)),
_ => continue 'big, _ => continue 'row,
} }
} }
_ => continue, _ => {}
} }
} }
// exquisite // Resolve ifindex -> name (cached)
let dev = if let Some(cached) = ball.get(&neighbour_message_item.header.ifindex) { let dev = match ifname_cache.get(&msg.header.ifindex) {
Some(cached.clone()) Some(name) => Some(name.clone()),
} else { None => {
// Query and cache let name = handle
let name = handle .link()
.link() .get()
.get() .match_index(msg.header.ifindex)
.match_index(neighbour_message_item.header.ifindex) .execute()
.execute() .try_next()
.try_next() .await?
.await? .and_then(|link| {
.and_then(|a| { link.attributes.into_iter().find_map(|a| match a {
a.attributes.into_iter().find_map(|attr| match attr { LinkAttribute::IfName(n) => Some(n),
LinkAttribute::IfName(name) => Some(name), _ => None,
_ => None, })
}) });
}); if let Some(ref n) = name {
ifname_cache.insert(msg.header.ifindex, n.clone());
if let Some(ref n) = name { }
ball.insert(neighbour_message_item.header.ifindex, n.clone()); name
} }
name
}; };
let (Some(ip), Some(dev)) = (ip, dev) else { let (Some(ip), Some(dev)) = (ip, dev) else {
continue 'big; continue 'row;
}; };
{ // Apply filters (empty set = match all)
// low block if !ip_set.is_empty() && !ip_set.contains(&ip) {
if let Some(fip) = gip continue 'row;
&& fip != ip }
{ if !dev_set.is_empty() && !dev_set.contains(dev.as_str()) {
continue 'big; continue 'row;
} }
if let Some(fdev) = gdev if !nud_set.is_empty() && !nud_set.contains(&state) {
&& dev != fdev continue 'row;
{ }
continue 'big; if !mac_set.is_empty() && !mac.is_some_and(|m| mac_set.contains(&m)) {
} continue 'row;
if !nudset.is_empty() && !nudset.contains(&state[0]) {
continue 'big;
};
} }
result.push(NeighborItem { result.push(NeighborItem {
ip, ip,
dev: Some(dev), dev: Some(dev),
mac, mac,
state, state: vec![state],
}); });
} }
Ok(result) // now i need another pass to filter out the uh.
Ok(result)
} }
impl TryFrom<NeighbourState> for NUDState { impl TryFrom<NeighbourState> for NUDState {
@@ -140,7 +137,7 @@ impl TryFrom<NeighbourState> for NUDState {
NeighbourState::Permanent => Ok(Self::Permanent), NeighbourState::Permanent => Ok(Self::Permanent),
NeighbourState::None => Ok(Self::None), NeighbourState::None => Ok(Self::None),
NeighbourState::Other(e) => Ok(Self::Other(e)), NeighbourState::Other(e) => Ok(Self::Other(e)),
_ => Err(u16::MAX), // idk _ => Err(u16::MAX),
} }
} }
+2 -2
View File
@@ -4,7 +4,7 @@ use lda_ipjs::subcommands::{address, neighbor};
#[tokio::test] // ← Use tokio::test instead of manual #[tokio::main] #[tokio::test] // ← Use tokio::test instead of manual #[tokio::main]
async fn ball1() -> anyhow::Result<()> { async fn ball1() -> anyhow::Result<()> {
let result = neighbor::nl::get(None, None, &[]).await?; let result = neighbor::nl::get(&[], &[] as &[&str], &[], &[]).await?;
println!("netlink results: {:?}", result); println!("netlink results: {:?}", result);
Ok(()) // ← Don't force error, let it succeed Ok(()) // ← Don't force error, let it succeed
} }
@@ -95,7 +95,7 @@ async fn ball_compare_backends() -> anyhow::Result<()> {
println!("Got {} entries from JSON", json_result.len()); println!("Got {} entries from JSON", json_result.len());
println!("\n=== Netlink Backend ==="); println!("\n=== Netlink Backend ===");
let nl_result = neighbor::nl::get(None, None, &[]).await?; let nl_result = neighbor::nl::get(&[], &[] as &[&str], &[], &[]).await?;
println!("Got {} entries from netlink", nl_result.len()); println!("Got {} entries from netlink", nl_result.len());
// Compare counts // Compare counts
+3 -19
View File
@@ -6,25 +6,9 @@ use thiserror::Error;
#[derive(Debug, Display, Error)] #[derive(Debug, Display, Error)]
pub enum IPNeighParseError { pub enum IPNeighParseError {
IpWhere, // i never seen a ip neigh where the first thing aint an ip IpWhere, // i never seen a ip neigh where the first thing aint an ip
IpParseError(AddrParseError), IpParseError(#[from] AddrParseError),
// DevWhere, // DevWhere,
MacParseError(macaddr::ParseError), MacParseError(#[from] macaddr::ParseError),
StateWhere, // i never seen a ip neigh without the big FAILED at the end StateWhere, // i never seen a ip neigh without the big FAILED at the end
StateParseError(strum::ParseError), StateParseError(#[from] strum::ParseError),
}
impl From<AddrParseError> for IPNeighParseError {
fn from(value: AddrParseError) -> Self {
Self::IpParseError(value)
}
}
impl From<macaddr::ParseError> for IPNeighParseError {
fn from(value: macaddr::ParseError) -> Self {
Self::MacParseError(value)
}
}
impl From<strum::ParseError> for IPNeighParseError {
fn from(value: strum::ParseError) -> Self {
Self::StateParseError(value)
}
} }
+39 -128
View File
@@ -1,8 +1,8 @@
use lda_ipjs::subcommands::neighbor; use lda_ipjs::subcommands::neighbor;
use macaddr::MacAddr; use macaddr::MacAddr;
use crate::arpparse::{self, IpNeighLine, NUDState}; use crate::arpparse::{IpNeighLine, NUDState};
use anyhow::{Context, Result, bail}; use anyhow::{Context, Result};
use std::collections::HashSet; use std::collections::HashSet;
use std::net::IpAddr; use std::net::IpAddr;
@@ -13,20 +13,7 @@ pub async fn get_ips(machine_name: &str) -> Result<impl Iterator<Item = IpAddr>>
.map(|c| c.ip())) .map(|c| c.ip()))
} }
// good now /// Query neighbor table with multi-filters. Empty slice = no filter.
//
// Current logic: When filtering by exactly 1 dev/mac, exclude entries missing that field.
// This is because missing dev/mac usually means the entry is incomplete/transient.
//
// when there is only one MACs (getmac got some), the result will not have them fields.
// so there are three cases:
//
// 1. dont got nothing: take all of them (macset.is_empty())
// 2. exactly one: pre-filtered by ip, everything matches,
// devset.len() != 1 returns false, but then it works????
// OH THIS fuckass code i added it in the get_mac
// 3. devset.len() > 1. if none then absolutely not match,
// if some then check with the set; thats normal
pub async fn get_macs( pub async fn get_macs(
machine_names: &[impl AsRef<str>], machine_names: &[impl AsRef<str>],
ips: &[IpAddr], ips: &[IpAddr],
@@ -34,140 +21,64 @@ pub async fn get_macs(
state: &[NUDState], state: &[NUDState],
macs: &[MacAddr], macs: &[MacAddr],
) -> Result<Vec<IpNeighLine>> { ) -> Result<Vec<IpNeighLine>> {
let mut ip_set: HashSet<IpAddr> = ips.iter().map(|ip| ip.to_canonical()).collect(); // Resolve machine names to IPs
let ip_m: HashSet<IpAddr> = if !machine_names.is_empty() { let resolved_ips: HashSet<IpAddr> = if !machine_names.is_empty() {
futures::future::try_join_all(machine_names.iter().map(|c| get_ips(c.as_ref()))) futures::future::try_join_all(machine_names.iter().map(|n| get_ips(n.as_ref())))
.await? .await?
.into_iter() .into_iter()
.flatten() .flatten()
.collect() .collect()
} else { } else {
Default::default() HashSet::new()
}; };
let ip_all = if ip_set.is_empty() && ip_m.is_empty() {
None // Merge provided IPs with resolved IPs
} else if ip_set.is_empty() { let ip_filter: Vec<IpAddr> = if ips.is_empty() && resolved_ips.is_empty() {
Some(ip_m) vec![]
} else if ip_m.is_empty() { } else if ips.is_empty() {
Some(ip_set) resolved_ips.into_iter().collect()
} else if resolved_ips.is_empty() {
ips.iter().map(|ip| ip.to_canonical()).collect()
} else { } else {
Some({ // Intersection: only IPs that appear in both
ip_set.retain(|c| ip_m.contains(c)); // inline AHHH ips.iter()
ip_set .map(|ip| ip.to_canonical())
}) .filter(|ip| resolved_ips.contains(ip))
.collect()
}; };
let opt_dev = if devs.len() > 1 { // Convert state filter
None let nud_filter: Vec<neighbor::NUDState> = state.iter().copied().map(Into::into).collect();
} else {
devs.iter().next().map(AsRef::as_ref)
};
let run_one = |to_ip: Option<IpAddr>| get_mac(to_ip, opt_dev, state); // Convert devs to &str for nl::get
let dev_strs: Vec<&str> = devs.iter().map(AsRef::as_ref).collect();
let mut ip_filtered = if let Some(something) = ip_all { // Single rtnetlink call with all filters
if something.len() == 1 { let results: Vec<IpNeighLine> = neighbor::nl::get(&ip_filter, &dev_strs, &nud_filter, macs)
run_one(something.into_iter().next()).await? .await
} else { .context("rtnetlink failed")?
run_one(None) .into_iter()
.await? .map(Into::into)
.into_iter() .collect();
.filter(|c| something.contains(&c.ip))
.collect()
}
} else {
run_one(None).await?
};
// Apply additional filters if any were provided Ok(results)
if !devs.is_empty() || !macs.is_empty() {
let devset: HashSet<_> = devs.iter().map(AsRef::as_ref).collect();
let macset: HashSet<_> = macs.iter().collect();
ip_filtered.retain(|entry| {
// Dev filter: if we're filtering by dev, entry must have a dev AND it must be in the set
let dev_ok =
devset.is_empty() || entry.dev.as_deref().is_some_and(|d| devset.contains(d));
// MAC filter: if we're filtering by MAC, entry must have a MAC AND it must be in the set
let mac_ok = macset.is_empty() || entry.mac.is_some_and(|m| macset.contains(&m));
dev_ok && mac_ok
})
};
Ok(ip_filtered)
} }
// /// the atomic get_macs. handle ONE thing only. /// Legacy single-filter wrapper. Use get_macs for multi-filter.
// // this one sucks shit #[allow(dead_code)]
// pub async fn get_mac(
// ip: Option<IpAddr>,
// dev: Option<&str>,
// state: &[NUDState],
// ) -> Result<Vec<IpNeighLine>> {
// use lda_ipjs::subcommands::neighbor as ipjs_neigh;
// let ipjs_states: Vec<ipjs_neigh::NUDState> = state.iter().copied().map(Into::into).collect();
// let items = ipjs_neigh::json::get(ip, dev, &ipjs_states)
// .await
// .context("Calling ip -j neigh failed")?;
// let lines = items.into_iter().map(Into::into).collect();
// Ok(lines)
// }
/// the atomic get_macs. handle ONE thing only.
// 17 - 25 ms full
pub async fn _get_mac(
ip: Option<IpAddr>,
dev: Option<&str>,
state: &[NUDState],
) -> Result<Vec<IpNeighLine>> {
let mut args: Vec<String> = vec!["neigh".into(), "show".into()];
if let Some(ip) = ip {
args.push("to".into());
args.push(ip.to_string());
}
if let Some(d) = dev {
args.push("dev".into());
args.push(d.to_string());
}
for nud in state {
args.push("nud".into());
args.push(nud.as_ip_neigh_arg().into());
}
let cmd = "ip";
let mut u = tokio::process::Command::new(cmd);
u.args(args);
let out = u.output().await?;
if !out.status.success() {
bail!(String::from_utf8_lossy(&out.stderr).into_owned());
}
let lines = String::from_utf8_lossy(&out.stdout);
let parsed = lines.lines().flat_map(arpparse::parse_ip_neigh_line);
let rows: Vec<IpNeighLine> = if let Some(d) = dev {
parsed.map(IpNeighLine::_with_dev(d)).collect()
} else {
parsed.collect()
};
Ok(rows)
}
// 15 - 20 ms full
pub async fn get_mac( pub async fn get_mac(
ip: Option<IpAddr>, ip: Option<IpAddr>,
dev: Option<&str>, dev: Option<&str>,
state: &[NUDState], state: &[NUDState],
) -> Result<Vec<IpNeighLine>> { ) -> Result<Vec<IpNeighLine>> {
let state2: Vec<neighbor::NUDState> = state.iter().copied().map(Into::into).collect(); let ips: Vec<IpAddr> = ip.into_iter().collect();
Ok(neighbor::nl::get(ip, dev, &state2) let devs: Vec<&str> = dev.into_iter().collect();
let nud: Vec<neighbor::NUDState> = state.iter().copied().map(Into::into).collect();
Ok(neighbor::nl::get(&ips, &devs, &nud, &[])
.await .await
.context("rtnetlink failed")? .context("rtnetlink failed")?
.into_iter() .into_iter()
.map(Into::into) .map(Into::into)
.collect()) .collect())
// how did i just do that
} }