ball knowledge
(claude handholding)
This commit is contained in:
@@ -1,5 +1,10 @@
|
|||||||
[target.armv7-unknown-linux-musleabihf]
|
[target.armv7-unknown-linux-musleabihf]
|
||||||
linker = "rust-lld"
|
linker = "rust-lld"
|
||||||
|
|
||||||
|
[build]
|
||||||
|
target = "armv7-unknown-linux-musleabihf"
|
||||||
|
|
||||||
[alias]
|
[alias]
|
||||||
ldabr = "b -r --target=target.armv7-unknown-linux-musleabihf"
|
ldabr = "b -r --target=target.armv7-unknown-linux-musleabihf"
|
||||||
|
t = "test -- --nocapture --test-threads=1"
|
||||||
|
tdebug = "test -- --nocapture --test-threads=1 --show-output"
|
||||||
+1
-1
@@ -16,7 +16,7 @@ serde_json = "1.0.143"
|
|||||||
serde_with = { version = "3.14.0", features = ["json"] }
|
serde_with = { version = "3.14.0", features = ["json"] }
|
||||||
strum = { version = "0.27.2", features = ["derive", "strum_macros"] }
|
strum = { version = "0.27.2", features = ["derive", "strum_macros"] }
|
||||||
thiserror = "2.0.16"
|
thiserror = "2.0.16"
|
||||||
tokio = { version = "1.47.1", features = ["fs", "process", "rt-multi-thread", "io-util"] }
|
tokio = { version = "1.47.1", features = ["fs", "process", "rt-multi-thread", "io-util", "macros"] }
|
||||||
urlencoding = "2.1.3"
|
urlencoding = "2.1.3"
|
||||||
|
|
||||||
[profile.release]
|
[profile.release]
|
||||||
|
|||||||
+1
-1
@@ -11,7 +11,7 @@ serde_json = "1.0.143"
|
|||||||
serde = { version = "1.0.219", features = ["derive"] }
|
serde = { version = "1.0.219", features = ["derive"] }
|
||||||
thiserror = "2.0.16"
|
thiserror = "2.0.16"
|
||||||
anyhow = "1.0.100"
|
anyhow = "1.0.100"
|
||||||
tokio = { version = "1.47.1", features = ["fs", "process", "rt-multi-thread", "io-util"] }
|
tokio = { version = "1.47.1", features = ["fs", "process", "rt-multi-thread", "io-util", "macros"] }
|
||||||
rtnetlink = "0.18.1"
|
rtnetlink = "0.18.1"
|
||||||
futures = "0.3.31"
|
futures = "0.3.31"
|
||||||
|
|
||||||
|
|||||||
@@ -69,9 +69,16 @@ pub enum NUDState {
|
|||||||
/// everything i see
|
/// everything i see
|
||||||
#[derive(Debug, PartialEq, Eq, Clone, Hash, Serialize, Deserialize)]
|
#[derive(Debug, PartialEq, Eq, Clone, Hash, Serialize, Deserialize)]
|
||||||
pub struct NeighborItem {
|
pub struct NeighborItem {
|
||||||
dst: IpAddr,
|
#[serde(rename(deserialize = "dst"))]
|
||||||
|
ip: IpAddr,
|
||||||
dev: String,
|
dev: String,
|
||||||
#[serde(deserialize_with = "des_opm", serialize_with = "ser_opm")]
|
#[serde(
|
||||||
lladdr: Option<MacAddr>,
|
deserialize_with = "des_opm",
|
||||||
|
serialize_with = "ser_opm",
|
||||||
|
default,
|
||||||
|
rename(deserialize = "lladdr")
|
||||||
|
)]
|
||||||
|
mac: Option<MacAddr>,
|
||||||
|
#[serde(default)]
|
||||||
state: Vec<NUDState>,
|
state: Vec<NUDState>,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,13 @@
|
|||||||
// hallo
|
// hallo
|
||||||
use std::net::IpAddr;
|
use std::{
|
||||||
|
collections::{HashMap, HashSet},
|
||||||
|
net::IpAddr,
|
||||||
|
};
|
||||||
|
|
||||||
use futures::TryStreamExt;
|
use futures::TryStreamExt;
|
||||||
use macaddr::MacAddr;
|
use macaddr::MacAddr;
|
||||||
use rtnetlink::packet_route::{
|
use rtnetlink::packet_route::{
|
||||||
|
AddressFamily,
|
||||||
link::LinkAttribute,
|
link::LinkAttribute,
|
||||||
neighbour::{NeighbourAddress, NeighbourAttribute, NeighbourState},
|
neighbour::{NeighbourAddress, NeighbourAttribute, NeighbourState},
|
||||||
};
|
};
|
||||||
@@ -16,47 +20,42 @@ pub async fn get(
|
|||||||
dev: Option<&str>,
|
dev: Option<&str>,
|
||||||
nud: &[NUDState],
|
nud: &[NUDState],
|
||||||
) -> 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); // every time?
|
||||||
let mut neighbor_data = handle.neighbours().get().execute(); // can i change header with message_mut? what even is header.
|
let mut neighbor_data = handle.neighbours().get().execute();
|
||||||
|
let nudset: HashSet<&NUDState> = HashSet::from_iter(gnud);
|
||||||
|
let mut ball: 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? {
|
'big: while let Some(neighbour_message_item) = neighbor_data.try_next().await? {
|
||||||
|
// Filter by address family
|
||||||
|
if !matches!(
|
||||||
|
neighbour_message_item.header.family,
|
||||||
|
AddressFamily::Inet | AddressFamily::Inet6
|
||||||
|
) || matches!(neighbour_message_item.header.state, NeighbourState::Noarp)
|
||||||
|
{
|
||||||
|
continue 'big;
|
||||||
|
}
|
||||||
|
|
||||||
let state = vec![
|
let state = vec![
|
||||||
neighbour_message_item
|
neighbour_message_item
|
||||||
.header
|
.header
|
||||||
.state
|
.state
|
||||||
.try_into()
|
.try_into()
|
||||||
.unwrap_or_default(),
|
.unwrap_or_default(),
|
||||||
];
|
]; // ONE ITEM. why tf ts design json.
|
||||||
let mut dst = None;
|
let mut ip = None;
|
||||||
let mut lladdr = None;
|
let mut mac = None;
|
||||||
|
|
||||||
// a hassle and a half to get the name
|
|
||||||
let dev = handle
|
|
||||||
.link()
|
|
||||||
.get()
|
|
||||||
.match_index(neighbour_message_item.header.ifindex)
|
|
||||||
.execute()
|
|
||||||
.try_next()
|
|
||||||
.await?
|
|
||||||
.and_then(|a| {
|
|
||||||
for link_attr in a.attributes {
|
|
||||||
match link_attr {
|
|
||||||
LinkAttribute::IfName(name) => return Some(name),
|
|
||||||
_ => continue,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
None
|
|
||||||
});
|
|
||||||
for neigh_attr in neighbour_message_item.attributes {
|
for neigh_attr in neighbour_message_item.attributes {
|
||||||
match neigh_attr {
|
match neigh_attr {
|
||||||
NeighbourAttribute::Destination(neighbour_address) => match neighbour_address {
|
NeighbourAttribute::Destination(neighbour_address) => match neighbour_address {
|
||||||
NeighbourAddress::Inet(ipv4_addr) => dst = Some(ipv4_addr.into()),
|
NeighbourAddress::Inet(ipv4_addr) => ip = Some(ipv4_addr.into()),
|
||||||
NeighbourAddress::Inet6(ipv6_addr) => dst = Some(ipv6_addr.into()),
|
NeighbourAddress::Inet6(ipv6_addr) => ip = Some(ipv6_addr.into()),
|
||||||
_ => continue 'big,
|
_ => continue 'big,
|
||||||
},
|
},
|
||||||
NeighbourAttribute::LinkLocalAddress(items) => {
|
NeighbourAttribute::LinkLocalAddress(items) => {
|
||||||
lladdr = match items.len() {
|
mac = match items.len() {
|
||||||
6 => items.first_chunk::<6>().map(|&e| MacAddr::from(e)),
|
6 => items.first_chunk::<6>().map(|&e| MacAddr::from(e)),
|
||||||
8 => items.first_chunk::<8>().map(|&e| MacAddr::from(e)),
|
8 => items.first_chunk::<8>().map(|&e| MacAddr::from(e)),
|
||||||
_ => continue 'big,
|
_ => continue 'big,
|
||||||
@@ -65,13 +64,57 @@ pub async fn get(
|
|||||||
_ => continue,
|
_ => continue,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
let Some((dst, dev)) = dst.zip(dev) else {
|
|
||||||
continue;
|
// exquisite
|
||||||
|
let dev = if let Some(cached) = ball.get(&neighbour_message_item.header.ifindex) {
|
||||||
|
Some(cached.clone())
|
||||||
|
} else {
|
||||||
|
// Query and cache
|
||||||
|
let name = handle
|
||||||
|
.link()
|
||||||
|
.get()
|
||||||
|
.match_index(neighbour_message_item.header.ifindex)
|
||||||
|
.execute()
|
||||||
|
.try_next()
|
||||||
|
.await?
|
||||||
|
.and_then(|a| {
|
||||||
|
a.attributes.into_iter().find_map(|attr| match attr {
|
||||||
|
LinkAttribute::IfName(name) => Some(name),
|
||||||
|
_ => None,
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
if let Some(ref n) = name {
|
||||||
|
ball.insert(neighbour_message_item.header.ifindex, n.clone());
|
||||||
|
}
|
||||||
|
name
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let Some((ip, dev)) = ip.zip(dev) else {
|
||||||
|
continue 'big;
|
||||||
|
};
|
||||||
|
|
||||||
|
{
|
||||||
|
// low block
|
||||||
|
if let Some(fip) = gip
|
||||||
|
&& fip != ip
|
||||||
|
{
|
||||||
|
continue 'big;
|
||||||
|
}
|
||||||
|
if let Some(fdev) = gdev
|
||||||
|
&& dev != fdev
|
||||||
|
{
|
||||||
|
continue 'big;
|
||||||
|
}
|
||||||
|
if !nudset.is_empty() && !nudset.contains(&state[0]) {
|
||||||
|
continue 'big;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
result.push(NeighborItem {
|
result.push(NeighborItem {
|
||||||
dst,
|
ip,
|
||||||
dev,
|
dev,
|
||||||
lladdr,
|
mac,
|
||||||
state,
|
state,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,120 @@
|
|||||||
|
use std::collections::HashSet;
|
||||||
|
|
||||||
|
use lda_ipjs::subcommands::neighbor;
|
||||||
|
|
||||||
|
#[tokio::test] // ← Use tokio::test instead of manual #[tokio::main]
|
||||||
|
async fn ball1() -> anyhow::Result<()> {
|
||||||
|
let result = neighbor::nl::get(None, None, &[]).await?;
|
||||||
|
println!("netlink results: {:?}", result);
|
||||||
|
Ok(()) // ← Don't force error, let it succeed
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn ball2() -> anyhow::Result<()> {
|
||||||
|
let result = neighbor::json::get(None, None, &[]).await?;
|
||||||
|
println!("json results: {:?}", result);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add this to debug the raw JSON
|
||||||
|
#[tokio::test]
|
||||||
|
async fn ball_raw_json() -> anyhow::Result<()> {
|
||||||
|
let output = tokio::process::Command::new("ip")
|
||||||
|
.args(["-j", "neigh", "show"])
|
||||||
|
.output()
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
let json = String::from_utf8_lossy(&output.stdout);
|
||||||
|
println!("Raw JSON:\n{}", json);
|
||||||
|
|
||||||
|
// Try to parse it
|
||||||
|
let parsed: Result<Vec<neighbor::NeighborItem>, _> = serde_json::from_slice(&output.stdout);
|
||||||
|
match parsed {
|
||||||
|
Ok(items) => println!("Parsed {} items", items.len()),
|
||||||
|
Err(e) => println!("Parse error: {}", e),
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check what fields actually exist in the JSON
|
||||||
|
#[tokio::test]
|
||||||
|
async fn ball_field_analysis() -> anyhow::Result<()> {
|
||||||
|
let output = tokio::process::Command::new("ip")
|
||||||
|
.args(["-j", "neigh", "show"])
|
||||||
|
.output()
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
let raw: Vec<serde_json::Value> = serde_json::from_slice(&output.stdout)?;
|
||||||
|
|
||||||
|
println!("Found {} neighbor entries", raw.len());
|
||||||
|
|
||||||
|
// Collect all unique field names across all entries
|
||||||
|
let mut all_fields = std::collections::HashSet::new();
|
||||||
|
for (i, entry) in raw.iter().enumerate() {
|
||||||
|
if let Some(obj) = entry.as_object() {
|
||||||
|
println!("\nEntry {}: {} fields", i, obj.len());
|
||||||
|
for (key, value) in obj {
|
||||||
|
all_fields.insert(key.clone());
|
||||||
|
println!(" {}: {} = {:?}", key, value.type_name(), value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
println!("\n=== All unique fields seen ===");
|
||||||
|
for field in &all_fields {
|
||||||
|
println!(" - {}", field);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper trait to get type name for JSON values
|
||||||
|
trait TypeName {
|
||||||
|
fn type_name(&self) -> &str;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TypeName for serde_json::Value {
|
||||||
|
fn type_name(&self) -> &str {
|
||||||
|
match self {
|
||||||
|
serde_json::Value::Null => "null",
|
||||||
|
serde_json::Value::Bool(_) => "bool",
|
||||||
|
serde_json::Value::Number(_) => "number",
|
||||||
|
serde_json::Value::String(_) => "string",
|
||||||
|
serde_json::Value::Array(_) => "array",
|
||||||
|
serde_json::Value::Object(_) => "object",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test filtering logic
|
||||||
|
#[tokio::test]
|
||||||
|
async fn ball_compare_backends() -> anyhow::Result<()> {
|
||||||
|
println!("=== JSON Backend ===");
|
||||||
|
let json_result = neighbor::json::get(None, None, &[]).await?;
|
||||||
|
println!("Got {} entries from JSON", json_result.len());
|
||||||
|
|
||||||
|
println!("\n=== Netlink Backend ===");
|
||||||
|
let nl_result = neighbor::nl::get(None, None, &[]).await?;
|
||||||
|
println!("Got {} entries from netlink", nl_result.len());
|
||||||
|
|
||||||
|
// Compare counts
|
||||||
|
if json_result.len() != nl_result.len() {
|
||||||
|
println!(
|
||||||
|
"\n⚠️ Count mismatch! JSON: {}, Netlink: {}",
|
||||||
|
json_result.len(),
|
||||||
|
nl_result.len()
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
println!("\n✅ Both backends returned same count");
|
||||||
|
}
|
||||||
|
let a: HashSet<neighbor::NeighborItem> = HashSet::from_iter(json_result);
|
||||||
|
let b: HashSet<neighbor::NeighborItem> = HashSet::from_iter(nl_result);
|
||||||
|
println!(
|
||||||
|
"istg {len1} == {len2} or else",
|
||||||
|
len1 = a.len(),
|
||||||
|
len2 = b.len()
|
||||||
|
);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
#!/usr/bin/env pwsh
|
||||||
|
# Test ipjs on remote ARM device
|
||||||
|
|
||||||
|
param(
|
||||||
|
[string]$Package = "lda-ipjs",
|
||||||
|
[string]$TestName = "",
|
||||||
|
[string]$BuildProfile = "debug",
|
||||||
|
[string]$password,
|
||||||
|
[switch]$Quiet
|
||||||
|
|
||||||
|
)
|
||||||
|
|
||||||
|
$ErrorActionPreference = "Stop"
|
||||||
|
|
||||||
|
# Build tests
|
||||||
|
Write-Host "Building tests for $Package..." -ForegroundColor Cyan
|
||||||
|
cargo test --no-run -p $Package --target armv7-unknown-linux-musleabihf $(if ($BuildProfile -eq "release") { "-r" }) --test test
|
||||||
|
|
||||||
|
# Find the test binary
|
||||||
|
$testBinary = Get-ChildItem -Path "target\armv7-unknown-linux-musleabihf\$BuildProfile\deps\test-*" -File |
|
||||||
|
Where-Object { $_.Name -match '^test-[a-f0-9]+$' } |
|
||||||
|
Sort-Object LastWriteTime -Descending |
|
||||||
|
Select-Object -First 1
|
||||||
|
|
||||||
|
if (-not $testBinary) {
|
||||||
|
Write-Error "Test binary not found!"
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Host "Found test binary: $($testBinary.Name)" -ForegroundColor Green
|
||||||
|
|
||||||
|
# Copy to target
|
||||||
|
Write-Host "Copying to target..." -ForegroundColor Cyan
|
||||||
|
pscp.exe -l root -batch -scp -pw $password $testBinary.FullName root@192.168.100.1:/tmp/test
|
||||||
|
|
||||||
|
# Run on target
|
||||||
|
Write-Host "Running tests on target..." -ForegroundColor Cyan
|
||||||
|
$testArgs = "--nocapture --show-output"
|
||||||
|
if ($TestName) {
|
||||||
|
$testArgs = "$TestName$(if (!$Quiet) {" $testArgs"})"
|
||||||
|
}
|
||||||
|
|
||||||
|
plink -batch -ssh root@192.168.100.1 -pw $password "chmod +x /tmp/test && /tmp/test$(if (!$Quiet) {" $testArgs"})"
|
||||||
|
|
||||||
|
Write-Host "Done!" -ForegroundColor Green
|
||||||
@@ -8,11 +8,14 @@
|
|||||||
//! 2. incorporate ip -j;
|
//! 2. incorporate ip -j;
|
||||||
//! 3. small 1-5 second caching;
|
//! 3. small 1-5 second caching;
|
||||||
|
|
||||||
|
use axum::{Router, routing::get};
|
||||||
|
use tokio::net::TcpListener;
|
||||||
mod arpparse;
|
mod arpparse;
|
||||||
pub mod assets;
|
pub mod assets;
|
||||||
mod dhcpparse;
|
mod dhcpparse;
|
||||||
mod route;
|
mod route;
|
||||||
mod utils;
|
mod utils;
|
||||||
|
use std::io;
|
||||||
|
|
||||||
#[cfg(target_os = "linux")]
|
#[cfg(target_os = "linux")]
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
|
|||||||
Reference in New Issue
Block a user