just like how i like it
This commit is contained in:
@@ -0,0 +1,60 @@
|
||||
# lda-ipjs
|
||||
|
||||
Typed Rust wrappers around Linux `ip -j ...` output, with optional experimental rtnetlink backends.
|
||||
|
||||
## Purpose
|
||||
|
||||
`lda-ipjs` exists so `wakey` can ask Linux networking questions in typed Rust instead of:
|
||||
|
||||
- parsing plain-text shell output
|
||||
- scattering `tokio::process::Command::new("ip")` everywhere
|
||||
- mixing product logic with Linux networking trivia
|
||||
|
||||
## Current contract
|
||||
|
||||
Stable default behavior:
|
||||
|
||||
- `address::get(...)` uses JSON (`ip -j address show`)
|
||||
- `link::get(...)` uses JSON (`ip -j link show`)
|
||||
- `neighbor::get(...)` uses JSON (`ip -j neigh show`)
|
||||
|
||||
Optional experimental behavior:
|
||||
|
||||
- feature: `experimental-nl`
|
||||
- enables rtnetlink-backed implementations
|
||||
- intended for places where one-pass kernel queries are materially better than repeated `ip -j` calls
|
||||
|
||||
This means the public API is:
|
||||
|
||||
- `get(...)` for the default backend
|
||||
- `get_with_backend(Backend::Json | Backend::Netlink)` when backend choice matters
|
||||
|
||||
## Modules
|
||||
|
||||
- `subcommands::address`
|
||||
- typed address/interface-address data
|
||||
- good place for subnet/broadcast derivation later
|
||||
- `subcommands::link`
|
||||
- typed link/interface data
|
||||
- useful for `ifindex -> ifname` mapping and interface metadata
|
||||
- `subcommands::neighbor`
|
||||
- typed neighbor-table data
|
||||
- currently the most useful experimental netlink surface
|
||||
|
||||
## Backend policy
|
||||
|
||||
JSON is the normal path.
|
||||
|
||||
Use netlink only when:
|
||||
|
||||
- the call is hot enough to matter
|
||||
- repeated shelling out is obviously wasteful
|
||||
- the netlink implementation is at least as coherent as the JSON one
|
||||
|
||||
Today that mainly applies to `neighbor::nl`.
|
||||
|
||||
## Relationship to wakey
|
||||
|
||||
`wakey` is the product.
|
||||
|
||||
`lda-ipjs` is a Linux networking adapter crate underneath it.
|
||||
@@ -0,0 +1,26 @@
|
||||
use std::{io, process::Output};
|
||||
|
||||
use anyhow::Context;
|
||||
|
||||
use super::LinkOutput;
|
||||
|
||||
pub async fn get(dev: Option<&str>) -> anyhow::Result<Vec<LinkOutput>> {
|
||||
let output = _get(dev).await.context("Can not run command")?;
|
||||
|
||||
if !output.status.success() {
|
||||
anyhow::bail!(String::from_utf8_lossy(&output.stderr).into_owned());
|
||||
}
|
||||
|
||||
serde_json::from_slice(&output.stdout).context("Deserialize failed")
|
||||
}
|
||||
|
||||
pub async fn _get(dev: Option<&str>) -> io::Result<Output> {
|
||||
let mut cmd = tokio::process::Command::new("ip");
|
||||
cmd.args(["-j", "link", "show"]);
|
||||
|
||||
if let Some(d) = dev {
|
||||
cmd.arg(d);
|
||||
}
|
||||
|
||||
cmd.output().await
|
||||
}
|
||||
@@ -1 +1,35 @@
|
||||
//! this is for link. You need link; at least to build an index -> name map. i Need It. sometimes.
|
||||
//! Typed wrappers for `ip -j link show`.
|
||||
|
||||
pub mod json;
|
||||
#[cfg(all(unix, feature = "experimental-nl"))]
|
||||
pub mod nl;
|
||||
|
||||
pub use crate::subcommands::Backend;
|
||||
use crate::utils::serialize::mac::option_mac;
|
||||
use macaddr::MacAddr;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct LinkOutput {
|
||||
pub ifindex: u32,
|
||||
pub ifname: String,
|
||||
#[serde(default)]
|
||||
pub operstate: Option<String>,
|
||||
#[serde(default, with = "option_mac")]
|
||||
pub address: Option<MacAddr>,
|
||||
}
|
||||
|
||||
pub async fn get(dev: Option<&str>) -> anyhow::Result<Vec<LinkOutput>> {
|
||||
get_with_backend(Backend::Json, dev).await
|
||||
}
|
||||
|
||||
pub async fn get_with_backend(
|
||||
backend: Backend,
|
||||
dev: Option<&str>,
|
||||
) -> anyhow::Result<Vec<LinkOutput>> {
|
||||
match backend {
|
||||
Backend::Json => json::get(dev).await,
|
||||
#[cfg(all(unix, feature = "experimental-nl"))]
|
||||
Backend::Netlink => nl::get(dev).await,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
#![cfg(unix)]
|
||||
|
||||
use futures::TryStreamExt;
|
||||
use rtnetlink::packet_route::link::LinkAttribute;
|
||||
|
||||
use super::LinkOutput;
|
||||
|
||||
pub async fn get(dev: Option<&str>) -> anyhow::Result<Vec<LinkOutput>> {
|
||||
let (conn, handle, _) = rtnetlink::new_connection()?;
|
||||
tokio::spawn(conn);
|
||||
|
||||
let mut req = handle.link().get();
|
||||
if let Some(dev) = dev {
|
||||
req = req.match_name(dev.to_owned());
|
||||
}
|
||||
|
||||
let mut stream = req.execute();
|
||||
let mut out = Vec::new();
|
||||
|
||||
while let Some(link) = stream.try_next().await? {
|
||||
let mut ifname = None;
|
||||
let mut operstate = None;
|
||||
let mut address = None;
|
||||
|
||||
for attr in link.attributes {
|
||||
match attr {
|
||||
LinkAttribute::IfName(name) => ifname = Some(name),
|
||||
LinkAttribute::Address(bytes) => {
|
||||
address = match bytes.len() {
|
||||
6 => bytes.first_chunk::<6>().map(|&b| macaddr::MacAddr::from(b)),
|
||||
8 => bytes.first_chunk::<8>().map(|&b| macaddr::MacAddr::from(b)),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
LinkAttribute::OperState(state) => operstate = Some(format!("{state:?}")),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(ifname) = ifname {
|
||||
out.push(LinkOutput {
|
||||
ifindex: link.header.index,
|
||||
ifname,
|
||||
operstate,
|
||||
address,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Ok(out)
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
pub mod address;
|
||||
pub mod link;
|
||||
pub mod neighbor;
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
|
||||
@@ -9,11 +9,11 @@ use futures::TryStreamExt;
|
||||
use macaddr::MacAddr;
|
||||
use rtnetlink::packet_route::{
|
||||
AddressFamily,
|
||||
link::LinkAttribute,
|
||||
neighbour::{NeighbourAddress, NeighbourAttribute, NeighbourState},
|
||||
};
|
||||
|
||||
use super::{NUDState, NeighborItem};
|
||||
use crate::subcommands::link;
|
||||
|
||||
/// Fetch neighbors via rtnetlink. Empty slice = no filter (match all).
|
||||
/// Non-empty slice = match ANY in the set.
|
||||
@@ -35,8 +35,12 @@ pub async fn get(
|
||||
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();
|
||||
// Prefetch all links once; link lookups were the ugliest and most expensive part.
|
||||
let ifname_cache: HashMap<u32, String> = link::nl::get(None)
|
||||
.await?
|
||||
.into_iter()
|
||||
.map(|link| (link.ifindex, link.ifname))
|
||||
.collect();
|
||||
let mut result = vec![];
|
||||
|
||||
'row: while let Some(msg) = neighbor_data.try_next().await? {
|
||||
@@ -72,28 +76,7 @@ pub async fn get(
|
||||
}
|
||||
|
||||
// Resolve ifindex -> name (cached)
|
||||
let dev = match ifname_cache.get(&msg.header.ifindex) {
|
||||
Some(name) => Some(name.clone()),
|
||||
None => {
|
||||
let name = handle
|
||||
.link()
|
||||
.get()
|
||||
.match_index(msg.header.ifindex)
|
||||
.execute()
|
||||
.try_next()
|
||||
.await?
|
||||
.and_then(|link| {
|
||||
link.attributes.into_iter().find_map(|a| match a {
|
||||
LinkAttribute::IfName(n) => Some(n),
|
||||
_ => None,
|
||||
})
|
||||
});
|
||||
if let Some(ref n) = name {
|
||||
ifname_cache.insert(msg.header.ifindex, n.clone());
|
||||
}
|
||||
name
|
||||
}
|
||||
};
|
||||
let dev = ifname_cache.get(&msg.header.ifindex).cloned();
|
||||
|
||||
let (Some(ip), Some(dev)) = (ip, dev) else {
|
||||
continue 'row;
|
||||
|
||||
Reference in New Issue
Block a user