22 Commits
53 changed files with 1647 additions and 528 deletions

No files matched your search

+7 -2
View File
@@ -1,5 +1,10 @@
[target.armv7-unknown-linux-musleabihf]
linker = "rust-lld"
[build] [build]
target = "armv7-unknown-linux-musleabihf" target = "armv7-unknown-linux-musleabihf"
[target.armv7-unknown-linux-musleabihf] [alias]
linker = "rust-lld" ldabr = "b -r --target=target.armv7-unknown-linux-musleabihf"
t = "test -- --nocapture --test-threads=1"
tdebug = "test -- --nocapture --test-threads=1 --show-output"
File renamed without changes.
+5 -2
View File
@@ -1,5 +1,8 @@
you can use block code as so: you should use block code to refence code, like so:
```rust ```rust
hello_world!(println); hello_world!(println);
// indented lines
``` ```
in the chat. in the chat. Else it fucks up formatting.
We work towards clean, maintainable design. im not the best at this thing.
+2 -1
View File
@@ -1,3 +1,4 @@
{ {
"rust-analyzer.cargo.target": "armv7-unknown-linux-musleabihf" "rust-analyzer.cargo.target": "armv7-unknown-linux-musleabihf",
"rust-analyzer.diagnostics.disabled": ["unlinked-file"]
} }
Generated
+134 -3
View File
@@ -32,6 +32,12 @@ dependencies = [
"libc", "libc",
] ]
[[package]]
name = "anyhow"
version = "1.0.100"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61"
[[package]] [[package]]
name = "atomic-waker" name = "atomic-waker"
version = "1.1.2" version = "1.1.2"
@@ -189,6 +195,12 @@ version = "1.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2fd1289c04a9ea8cb22300a459a72a385d7c73d3259e2ed7dcb2af674838cfa9" checksum = "2fd1289c04a9ea8cb22300a459a72a385d7c73d3259e2ed7dcb2af674838cfa9"
[[package]]
name = "cfg_aliases"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724"
[[package]] [[package]]
name = "chrono" name = "chrono"
version = "0.4.41" version = "0.4.41"
@@ -610,6 +622,22 @@ version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
[[package]]
name = "lda-ipjs"
version = "0.0.1"
dependencies = [
"anyhow",
"futures",
"macaddr",
"rtnetlink",
"serde",
"serde_json",
"serde_with",
"strum",
"thiserror 2.0.16",
"tokio",
]
[[package]] [[package]]
name = "libc" name = "libc"
version = "0.2.175" version = "0.2.175"
@@ -669,6 +697,66 @@ dependencies = [
"windows-sys", "windows-sys",
] ]
[[package]]
name = "netlink-packet-core"
version = "0.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3463cbb78394cb0141e2c926b93fc2197e473394b761986eca3b9da2c63ae0f4"
dependencies = [
"paste",
]
[[package]]
name = "netlink-packet-route"
version = "0.25.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3ec2f5b6839be2a19d7fa5aab5bc444380f6311c2b693551cb80f45caaa7b5ef"
dependencies = [
"bitflags",
"libc",
"log",
"netlink-packet-core",
]
[[package]]
name = "netlink-proto"
version = "0.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b65d130ee111430e47eed7896ea43ca693c387f097dd97376bffafbf25812128"
dependencies = [
"bytes",
"futures",
"log",
"netlink-packet-core",
"netlink-sys",
"thiserror 2.0.16",
]
[[package]]
name = "netlink-sys"
version = "0.8.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "16c903aa70590cb93691bf97a767c8d1d6122d2cc9070433deb3bbf36ce8bd23"
dependencies = [
"bytes",
"futures",
"libc",
"log",
"tokio",
]
[[package]]
name = "nix"
version = "0.29.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46"
dependencies = [
"bitflags",
"cfg-if",
"cfg_aliases",
"libc",
]
[[package]] [[package]]
name = "num-conv" name = "num-conv"
version = "0.1.0" version = "0.1.0"
@@ -705,6 +793,12 @@ version = "4.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "48dd4f4a2c8405440fd0462561f0e5806bd0f77e86f51c761481bdd4018b545e" checksum = "48dd4f4a2c8405440fd0462561f0e5806bd0f77e86f51c761481bdd4018b545e"
[[package]]
name = "paste"
version = "1.0.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a"
[[package]] [[package]]
name = "percent-encoding" name = "percent-encoding"
version = "2.3.1" version = "2.3.1"
@@ -767,6 +861,23 @@ dependencies = [
"syn", "syn",
] ]
[[package]]
name = "rtnetlink"
version = "0.18.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "08fd15aa4c64c34d0b3178e45ec6dad313a9f02b193376d501668a7950264bb7"
dependencies = [
"futures",
"log",
"netlink-packet-core",
"netlink-packet-route",
"netlink-proto",
"netlink-sys",
"nix",
"thiserror 1.0.69",
"tokio",
]
[[package]] [[package]]
name = "rustc-demangle" name = "rustc-demangle"
version = "0.1.26" version = "0.1.26"
@@ -999,13 +1110,33 @@ version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263"
[[package]]
name = "thiserror"
version = "1.0.69"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52"
dependencies = [
"thiserror-impl 1.0.69",
]
[[package]] [[package]]
name = "thiserror" name = "thiserror"
version = "2.0.16" version = "2.0.16"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3467d614147380f2e4e374161426ff399c91084acd2363eaf549172b3d5e60c0" checksum = "3467d614147380f2e4e374161426ff399c91084acd2363eaf549172b3d5e60c0"
dependencies = [ dependencies = [
"thiserror-impl", "thiserror-impl 2.0.16",
]
[[package]]
name = "thiserror-impl"
version = "1.0.69"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1"
dependencies = [
"proc-macro2",
"quote",
"syn",
] ]
[[package]] [[package]]
@@ -1179,7 +1310,7 @@ checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65"
[[package]] [[package]]
name = "wakey" name = "wakey"
version = "0.1.3" version = "0.1.5"
dependencies = [ dependencies = [
"axum", "axum",
"axum-extra", "axum-extra",
@@ -1191,7 +1322,7 @@ dependencies = [
"serde_json", "serde_json",
"serde_with", "serde_with",
"strum", "strum",
"thiserror", "thiserror 2.0.16",
"tokio", "tokio",
"urlencoding", "urlencoding",
] ]
+5 -2
View File
@@ -1,6 +1,6 @@
[package] [package]
name = "wakey" name = "wakey"
version = "0.1.3" version = "0.1.5"
edition = "2024" edition = "2024"
publish = ["gitea"] publish = ["gitea"]
@@ -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]
@@ -26,3 +26,6 @@ strip = true
[features] [features]
default = ["very-smart-parsing"] default = ["very-smart-parsing"]
very-smart-parsing = [] # this is the a-bit-redundant parse thing that copilot made very-smart-parsing = [] # this is the a-bit-redundant parse thing that copilot made
[workspace]
members = ["ipjs"]
+18
View File
@@ -0,0 +1,18 @@
[package]
name = "lda-ipjs"
description = "ip -j show schemas"
version = "0.0.1"
edition = "2024"
[dependencies]
macaddr = { version = "1.0.1", features = ["serde", "serde_std"] }
strum = { version = "0.27.2", features = ["derive", "strum_macros"] }
serde_json = "1.0.143"
serde = { version = "1.0.219", features = ["derive"] }
serde_with = { version = "3.14.0", features = ["json"] }
thiserror = "2.0.16"
anyhow = "1.0.100"
tokio = { version = "1.47.1", features = ["fs", "process", "rt-multi-thread", "io-util", "macros"] }
rtnetlink = "0.18.1"
futures = "0.3.31"
+3
View File
@@ -0,0 +1,3 @@
trait IpCommand {
}
View File
Whitespace-only changes.
+20
View File
@@ -0,0 +1,20 @@
//! low
//!
//! # lda-ipjs
//!
//! this package will represent all my needs with the all the subcommands of ip -j.
//!
//! ## what i need
//!
//! ```console
//! ip -j neigh show
//! ```
//!
//! i also need to see devices and idk MAYBE maybe not MAYBE UHHHHHH maybe broadcast
//!
//! LOWK if this were to be calls to kernel or some bullshit then PLEASE because doing ts parsing its hell cuh
pub mod subcommands;
pub mod utils;
// i want a generalized way to build and call
+28
View File
@@ -0,0 +1,28 @@
// ip address [ show [ dev IFNAME ] [ scope SCOPE-ID ] [ master DEVICE ]
// [ type TYPE ] [ to PREFIX ] [ FLAG-LIST ]
// [ label LABEL ] [up] [ vrf NAME ] ]
// fuck is this mean
// do i need all this? do i need anything but `ip -j a show dev br-lan`?
// TYPE := { vlan | veth | vcan | vxcan | dummy | ifb | macvlan | macvtap |
// bridge | bond | ipoib | ip6tnl | ipip | sit | vxlan | lowpan |
// gre | gretap | erspan | ip6gre | ip6gretap | ip6erspan | vti |
// nlmon | can | bond_slave | ipvlan | geneve | bridge_slave |
// hsr | macsec | netdevsim }
// FLAG-LIST := [ FLAG-LIST ] FLAG
// FLAG := [ permanent | dynamic | secondary | primary |
// [-]tentative | [-]deprecated | [-]dadfailed | temporary |
// CONFFLAG-LIST ]
// CONFFLAG-LIST := [ CONFFLAG-LIST ] CONFFLAG
// CONFFLAG := [ home | nodad | mngtmpaddr | noprefixroute | autojoin ]
// prefix seems to be a cidr. both 6 and 4 works. idfk dog
use super::AddrInfo;
pub async fn get() -> Vec<AddrInfo> {
todo!()
}
+41
View File
@@ -0,0 +1,41 @@
//! ts
//!
//! deals with both ip a (addroutput) and ip l (commonoutput)
//!
//! lowk why its free but its indirection and its ass
pub mod json;
use crate::utils::serialize::mac::option_mac;
use macaddr::MacAddr;
use serde::{Deserialize, Serialize};
/// i dont include what i dont know about (almost all ts)
#[derive(Serialize, Debug, Deserialize)]
pub struct AddrOutput {
pub ifindex: u32,
pub ifname: String,
/// i imagine UP or DOWN, unknown
pub operstate: String,
// 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>,
#[serde(default)] // i wish we have intellisense for this... fuck you metaprogramming
pub addr_info: Vec<AddrInfo>,
}
// i be copying
// Raw JSON shape from ip -j -4 address show
#[derive(Debug, Deserialize, Serialize)]
pub struct AddrInfo {
pub family: Option<String>,
pub local: Option<String>,
pub prefixlen: Option<u8>,
pub broadcast: Option<String>,
pub scope: Option<String>,
pub label: Option<String>,
// many more exist; we only take what we need
}
+2
View File
@@ -0,0 +1,2 @@
pub mod address;
pub mod neighbor;
+41
View File
@@ -0,0 +1,41 @@
//! idk what to put here
use std::net::IpAddr;
use anyhow::bail;
use super::{NUDState, NeighborItem};
// loose translation of [wakey::utils::query::macs::get_mac]
// i think ill write tokio::process every time tho (for this if let thing) because iterate through all ts youll have to as str and all the hooplas.
// it all turns to live osstr tho so ts just for my own sanity
// thiserror? anyhow
pub async fn get(
ip: Option<IpAddr>,
dev: Option<&str>,
nud: &[NUDState],
) -> anyhow::Result<Vec<NeighborItem>> {
let mut cmd = tokio::process::Command::new("ip");
cmd.args(["-j", "neigh", "show"]);
if let Some(ip) = ip {
cmd.arg(ip.to_canonical().to_string());
};
if let Some(dev) = dev {
cmd.arg("dev");
cmd.arg(dev);
}
for nud in nud {
cmd.arg("nud");
cmd.arg(nud.to_string());
}
let output = cmd.output().await?;
if !output.status.success() {
bail!(String::from_utf8_lossy(&output.stderr).into_owned())
} else {
Ok(serde_json::from_slice(&output.stdout)?)
}
}
+81
View File
@@ -0,0 +1,81 @@
//! ```bash
//! ip -j n s
//! ```
//!
//! yes. this is a real call.
pub mod json;
pub mod nl;
use crate::utils::serialize::mac::option_mac;
use std::net::IpAddr;
use macaddr::MacAddr;
use serde::{Deserialize, Serialize};
use strum::{Display, EnumString};
#[derive(Debug, PartialEq, Eq, Clone, Hash, Serialize, Deserialize)]
pub struct NeighborInput {
/// supports only the last item (ignore), `to` keyword is optional
pub to: Option<IpAddr>,
/// supports only one item (it complains if multiple)
pub dev: Option<String>, // im all for simplicity
/// takes multiple, has to have `nud` before bro or it will think you `to`
pub nud: Vec<NUDState>,
}
// as input this must be lowercase. as output it is uppercase
/// docs for items come from a random ahh man website idk
#[derive(
Debug, PartialEq, Eq, EnumString, Display, Clone, Copy, Hash, Serialize, Deserialize, Default,
)]
#[strum(serialize_all = "lowercase", ascii_case_insensitive)]
#[serde(rename_all = "UPPERCASE")]
pub enum NUDState {
/// the neighbour entry is valid forever and can
/// be only be removed administratively.
Permanent,
/// the neighbour entry is valid. No attempts to
/// validate this entry will be made but it can
/// be removed when its lifetime expires.
Noarp,
/// the neighbour entry is valid until the
/// reachability timeout expires.
Reachable,
/// the neighbour entry is valid but suspicious.
/// This option to ip neigh does not change the
/// neighbour state if it was valid and the
/// address is not changed by this command.
Stale,
/// this is a pseudo state used when initially
/// creating a neighbour entry or after trying to
/// remove it before it becomes free to do so.
#[default]
None,
/// the neighbour entry has not (yet) been
/// validated/resolved.
Incomplete,
/// neighbor entry validation is currently
/// delayed.
Delay,
/// neighbor is being probed.
Probe,
/// max number of probes exceeded without
/// success, neighbor validation has ultimately
/// failed.
Failed,
Other(u16)
}
/// everything i see
#[derive(Debug, PartialEq, Eq, Clone, Hash, Serialize, Deserialize)]
pub struct NeighborItem {
#[serde(rename(deserialize = "dst"))]
pub ip: IpAddr,
pub dev: String,
#[serde(with = "option_mac", default, rename(deserialize = "lladdr"))]
pub mac: Option<MacAddr>,
#[serde(default)]
pub state: Vec<NUDState>,
}
+143
View File
@@ -0,0 +1,143 @@
// hallo
use std::{
collections::{HashMap, HashSet},
net::IpAddr,
};
use futures::TryStreamExt;
use macaddr::MacAddr;
use rtnetlink::packet_route::{
AddressFamily,
link::LinkAttribute,
neighbour::{NeighbourAddress, NeighbourAttribute, NeighbourState},
};
use super::{NUDState, NeighborItem};
// dont you love https://github.com/rust-netlink/rtnetlink/blob/main/examples/get_neighbours.rs
pub async fn get(
ip: Option<IpAddr>,
dev: Option<&str>,
nud: &[NUDState],
) -> anyhow::Result<Vec<NeighborItem>> {
let (gip, gdev, gnud) = (ip, dev, nud);
let (conn, handle, _) = rtnetlink::new_connection()?;
tokio::spawn(conn); // every time?
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![];
'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)
// copilot says this to match ip -j n s
{
continue 'big;
}
let state = vec![
neighbour_message_item
.header
.state
.try_into()
.unwrap_or_default(),
]; // ONE ITEM. why tf ts design json.
let mut ip = None;
let mut mac = None;
for neigh_attr in neighbour_message_item.attributes {
match neigh_attr {
NeighbourAttribute::Destination(neighbour_address) => match neighbour_address {
NeighbourAddress::Inet(ipv4_addr) => ip = Some(ipv4_addr.into()),
NeighbourAddress::Inet6(ipv6_addr) => ip = Some(ipv6_addr.into()),
_ => continue 'big,
},
NeighbourAttribute::LinkLocalAddress(items) => {
mac = match items.len() {
6 => items.first_chunk::<6>().map(|&e| MacAddr::from(e)),
8 => items.first_chunk::<8>().map(|&e| MacAddr::from(e)),
_ => continue 'big,
}
}
_ => 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 {
ip,
dev,
mac,
state,
});
}
Ok(result) // now i need another pass to filter out the uh.
}
impl TryFrom<NeighbourState> for NUDState {
fn try_from(value: NeighbourState) -> Result<Self, Self::Error> {
match value {
NeighbourState::Incomplete => Ok(Self::Incomplete),
NeighbourState::Reachable => Ok(Self::Reachable),
NeighbourState::Stale => Ok(Self::Stale),
NeighbourState::Delay => Ok(Self::Delay),
NeighbourState::Probe => Ok(Self::Probe),
NeighbourState::Failed => Ok(Self::Failed),
NeighbourState::Noarp => Ok(Self::Noarp),
NeighbourState::Permanent => Ok(Self::Permanent),
NeighbourState::None => Ok(Self::None),
NeighbourState::Other(e) => Ok(Self::Other(e)),
_ => Err(u16::MAX), // idk
}
}
type Error = u16;
}
+35
View File
@@ -0,0 +1,35 @@
use std::iter::Fuse;
/// wrap ts into a [Fuse][std::iter::Fuse] or something
pub struct Real<A: Clone, B: Iterator<Item = A>> {
prepend: A,
iter: B,
my_turn: bool,
}
impl<A: Clone, B: Iterator<Item = A>> Real<A, B> {
pub fn new(prepend: A, iter: B) -> Self {
Self {
prepend,
iter,
my_turn: true,
}
}
pub fn fuse(prepend: A, iter: B) -> Fuse<Self> {
Self::new(prepend, iter).fuse()
}
}
impl<A: Clone, B: Iterator<Item = A>> Iterator for Real<A, B> {
type Item = A;
fn next(&mut self) -> Option<Self::Item> {
let a = if self.my_turn {
Some(self.prepend.clone())
} else {
self.iter.next()
};
self.my_turn = !self.my_turn;
a
}
}
+2
View File
@@ -0,0 +1,2 @@
pub mod iter;
pub mod serialize;
+46
View File
@@ -0,0 +1,46 @@
use macaddr::MacAddr;
use serde::{self, Deserialize, Deserializer, de::Error as DeError};
use serde::{Serialize, Serializer, de};
pub fn serialize_macs<S>(macs: &[MacAddr], serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
let strings: Vec<String> = macs.iter().map(|m| m.to_string()).collect();
serde::Serialize::serialize(&strings, serializer)
}
/// Serialize a MacAddr as a string
pub fn serialize<S>(mac: &MacAddr, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
serializer.serialize_str(&mac.to_string())
}
/// Deserialize a MacAddr from a string
pub fn deserialize<'de, D>(deserializer: D) -> Result<MacAddr, D::Error>
where
D: Deserializer<'de>,
{
let s = <String as serde::Deserialize>::deserialize(deserializer)?;
s.parse::<MacAddr>().map_err(DeError::custom)
}
pub mod option_mac {
use super::*;
/// serialize an [`Option<MacAddr>`]
pub fn serialize<S: Serializer>(bro: &Option<MacAddr>, ser: S) -> Result<S::Ok, S::Error> {
Option::<String>::serialize(&bro.as_ref().map(ToString::to_string), ser)
}
/// deserialize an [`Option<MacAddr>`]
pub fn deserialize<'de, D>(des: D) -> Result<Option<MacAddr>, D::Error>
where
D: serde::Deserializer<'de>,
{
Option::<&str>::deserialize(des)?
.map(str::parse)
.transpose()
.map_err(de::Error::custom)
}
}
+2
View File
@@ -0,0 +1,2 @@
pub mod mac;
pub mod vec;
+37
View File
@@ -0,0 +1,37 @@
use serde::Deserialize;
use serde::de;
#[derive(Deserialize)]
#[serde(untagged)]
enum OneOrMany<T> {
One(T),
Many(Vec<T>),
}
pub fn vec_from_strs<'de, D, T>(des: D) -> Result<Vec<T>, D::Error>
where
D: serde::Deserializer<'de>,
T: std::str::FromStr,
T::Err: std::fmt::Display,
{
let raw: OneOrMany<String> = OneOrMany::<String>::deserialize(des)?;
let mut out = Vec::new();
match raw {
OneOrMany::One(s) => {
let t = s.trim();
if !t.is_empty() {
out.push(t.parse().map_err(de::Error::custom)?);
}
}
OneOrMany::Many(vs) => {
for s in vs {
let t = s.trim();
if t.is_empty() {
continue;
}
out.push(t.parse().map_err(de::Error::custom)?);
}
}
}
Ok(out)
}
+120
View File
@@ -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(())
}
+60
View File
@@ -0,0 +1,60 @@
# taking back my code AHHHH
this is going to be lengthy. ci is spotless but idk how it works cuz powershell. this is good but i cant afford VPN on the router; we will need to do some bullshit.
## overview
we need a:
- small ahh bin in the router.
- forwards a shell and some functions.
- may be shell calling fns maybe some unsafe c. i cant unsafe c.
- big ahh bin in the vps.
- connect to the small ahh bin thru the wide ass internet.
- forwards some APIs.
- static site
- now need no hosts; apis can be called anywhere anyway.
- do offer these in the big ahh bins tho lets not miss out some shit.
- automation
- gitea
- on the vps or the uh idk.
- ABSOLUTELY can auto build main:latest every day / other day.
- and releases.
- router
- a script to pull the tar. from actions artifacts or from releases. (lua or shell yo)
- a script for the bin and the init.d scripts for these two.
- vps
- a systemd service (ima use ai for ts ngl)
- an auto updater. maybe we overkill and dnf. maybe go easy and git clone + cargo install. maybe we pull from release or nightly. idk
i think idk
## router
### current
everything.
we have some functions that call ip neigh a bajillion times.
we have some other functions that read the same file/folder in /sys or sum shit.
we have a function that broadcast to a set ahh address.
we have some routes; and some data structures acting as schema for them ahh. we dont do posts with json but gets with query strings for some ahh reasons.
these are carryover from the early days.
also from the early days are the js.
static was 100% ai with 0 intervention.
we have a search bar of sort that hooks through a smart endpoint that searches for everything.
we have some bullshit. and two buttons to check and wake. that pings `/api/smart/{query}` and `/wake?name=query`. why.
we have the result of the wake and or status and the errors in the first text box.
the list of cliets with names connected to the router the second box.
thats cool.
### changes?
idk changes.
+173
View File
@@ -0,0 +1,173 @@
use macaddr::MacAddr6;
use serde::Deserialize;
use std::{net::IpAddr, process::Command};
// Raw JSON shape from ip -j -4 address show
#[derive(Debug, Deserialize)]
struct IpJAddrInfo {
family: Option<String>,
local: Option<String>,
broadcast: Option<String>,
scope: Option<String>,
label: Option<String>,
prefixlen: Option<u8>,
// many more exist; we only take what we need
}
#[derive(Debug, Deserialize)]
struct IpJAddrEntry {
ifname: String,
// interface MAC address (present on non-loopback):
address: Option<String>,
addr_info: Option<Vec<IpJAddrInfo>>,
}
#[derive(Debug, Clone)]
pub struct AddrInfo {
pub local: IpAddr,
pub broadcast: Option<IpAddr>,
pub scope: Option<String>,
pub label: Option<String>,
pub prefixlen: Option<u8>,
}
#[derive(Debug, Clone)]
pub struct IpAEntry {
pub ifname: String,
pub mac: Option<MacAddr6>,
pub addr_info: Vec<AddrInfo>,
// IPv4 only (inet)
}
// Run ip -j -4 address show and deserialize
fn read_ip_json() -> Result<Vec<IpJAddrEntry>, Box<dyn std::error::Error>> {
let out = Command::new("ip")
.args(["-j", "-4", "address", "show"])
.output()?;
if !out.status.success() {
return Err(format!("ip exited with {}", out.status).into());
}
let entries: Vec<IpJAddrEntry> = serde_json::from_slice(&out.stdout)?;
Ok(entries)
}
fn parse_ip(s: &str) -> Option<IpAddr> {
s.parse::<IpAddr>().ok()
}
fn parse_mac(s: &str) -> Option<MacAddr6> {
// ip outputs lowercase aa:bb:..., which MacAddr6 can parse
s.parse::<MacAddr6>().ok()
}
// Public: read and convert to a cleaned model
pub fn get_ip_addr_entries() -> Result<Vec<IpAEntry>, Box<dyn std::error::Error>> {
let raw = read_ip_json()?;
let mut out = Vec::new();
for e in raw {
let mac = e.address.as_deref().and_then(parse_mac);
let mut infos = Vec::new();
if let Some(list) = e.addr_info {
for ai in list {
// keep only IPv4
if ai.family.as_deref() != Some("inet") {
continue;
}
if let Some(local) = ai.local.as_deref().and_then(parse_ip) {
let broadcast = ai.broadcast.as_deref().and_then(parse_ip);
infos.push(AddrInfo {
local,
broadcast,
scope: ai.scope,
label: ai.label,
prefixlen: ai.prefixlen,
});
}
}
}
out.push(IpAEntry {
ifname: e.ifname,
mac,
addr_info: infos,
});
}
Ok(out)
}
// Helper: first “good” (global) broadcast on an interface
pub fn broadcast_for_ifname(ifname: &str) -> Result<Option<IpAddr>, Box<dyn std::error::Error>> {
let entries = get_ip_addr_entries()?;
let dev = entries.into_iter().find(|e| e.ifname == ifname);
if let Some(dev) = dev {
// Prefer scope=global with a broadcast; else any broadcast
if let Some(b) = dev
.addr_info
.iter()
.filter(|ai| ai.scope.as_deref() == Some("global"))
.filter_map(|ai| ai.broadcast)
.next()
{
return Ok(Some(b));
}
if let Some(b) = dev.addr_info.iter().filter_map(|ai| ai.broadcast).next() {
return Ok(Some(b));
}
}
Ok(None)
}
// Helper: broadcast for a given interface MAC
pub fn broadcast_for_mac(
mac: MacAddr6,
) -> Result<Option<(String, IpAddr)>, Box<dyn std::error::Error>> {
let entries = get_ip_addr_entries()?;
for e in entries {
if let Some(m) = e.mac {
if m == mac {
// prefer global broadcast
if let Some(b) = e
.addr_info
.iter()
.filter(|ai| ai.scope.as_deref() == Some("global"))
.filter_map(|ai| ai.broadcast)
.next()
{
return Ok(Some((e.ifname, b)));
}
if let Some(b) = e.addr_info.iter().filter_map(|ai| ai.broadcast).next() {
return Ok(Some((e.ifname, b)));
}
}
}
}
Ok(None)
}
// Fallback: if nothing else, use limited broadcast (may be filtered on some networks)
pub fn fallback_broadcast() -> IpAddr {
// 255.255.255.255
IpAddr::from([255, 255, 255, 255])
}
// Convenience: all broadcasts grouped by interface (IPv4)
pub fn all_broadcasts() -> Result<Vec<(String, IpAddr)>, Box<dyn std::error::Error>> {
let Some(r) = Some(12) else {
e.addr_info
.iter()
.filter(|ai| ai.scope.as_deref() == Some("global"))
.filter_map(|ai| ai.broadcast)
};
let entries = get_ip_addr_entries()?;
let mut out = Vec::new();
for e in entries {
for ai in e.addr_info {
if let Some(b) = ai.broadcast {
out.push((e.ifname.clone(), b));
}
}
}
Ok(out)
}
+1 -1
View File
@@ -15,7 +15,7 @@ start() {
RETRY_COUNT=0 RETRY_COUNT=0
# shellcheck disable=SC2016 # shellcheck disable=SC2016
nohup sh -c ' sh -c '
while [ "$RETRY_COUNT" -lt "$MAX_RETRIES" ]; do while [ "$RETRY_COUNT" -lt "$MAX_RETRIES" ]; do
if fn; then if fn; then
echo "[update_tailscale] Success at $(date)" >>"$LOGFILE" echo "[update_tailscale] Success at $(date)" >>"$LOGFILE"
+5 -5
View File
@@ -2,12 +2,12 @@
START=66 START=66
start() { start() {
LOGFILE=/var/log/update_wakey.log LOGFILE=/var/log/update_wakey.log
MAX_RETRIES=10 MAX_RETRIES=10
RETRY_COUNT=0 RETRY_COUNT=0
# shellcheck disable=SC2016 # shellcheck disable=SC2016
nohup sh -c ' sh -c '
while [ "$RETRY_COUNT" -lt "$MAX_RETRIES" ]; do while [ "$RETRY_COUNT" -lt "$MAX_RETRIES" ]; do
if /etc/ldlda_help/update_wakey.sh; then if /etc/ldlda_help/update_wakey.sh; then
echo "[update_wakey] Success at $(date)" >>"$LOGFILE" echo "[update_wakey] Success at $(date)" >>"$LOGFILE"
+61
View File
@@ -0,0 +1,61 @@
#!/usr/bin/env pwsh
# Test ipjs on remote ARM device
param(
[string]$Package = "lda-ipjs",
[string]$TestName = "",
[ValidateSet("debug", "release")]
[string]$BuildProfile = "debug",
[string]$password,
[switch]$Verbose,
[string]$RemoteTestPath = "/root/.bin/test",
[string]$RemoteHost = "[email protected]"
)
$ErrorActionPreference = "Stop"
# Build tests and capture output
Write-Host "Building tests for $Package..." -ForegroundColor Cyan
# Stream cargo output anc convert to text
$cargoOutput = cargo test --no-run -p $Package --target armv7-unknown-linux-musleabihf $(if ($BuildProfile -eq "release") { "-r" }) 2>&1 |
ForEach-Object {
$line = if ($_ -is [System.Management.Automation.ErrorRecord]) { $_.ToString() } else { $_ }
if ($Verbose) {
Write-Host $line
}
$line # Pass through to capture
}
# Parse test binary paths from cargo output
$testBinaries = $cargoOutput |
Select-String -Pattern "Executable.*\((.+)\)" |
ForEach-Object { $_.Matches.Groups[1].Value } |
Get-Item
if ($testBinaries.Count -eq 0) {
Write-Error "No test binaries found!"
Write-Host "Cargo output:" -ForegroundColor Yellow
$cargoOutput | ForEach-Object { Write-Host $_ }
exit 1
}
Write-Host "Found $($testBinaries.Count) test $($testBinaries.Count -eq 1 ? "binary" : "binaries")" -ForegroundColor Green
# Run each test binary
foreach ($testBinary in $testBinaries) {
Write-Host "`nTesting: $($testBinary.Name)" -ForegroundColor Cyan
# Copy to target
pscp.exe -batch -scp -pw $password $testBinary.FullName ${RemoteHost}:$RemoteTestPath | Out-Null
# Run on target
$testArgs = "$(if ($Verbose) {"--nocapture --show-output"})"
if ($TestName) {
$testArgs = "$TestName $testArgs"
}
plink -batch -ssh $RemoteHost -pw $password "chmod +x $RemoteTestPath && $RemoteTestPath $testArgs"
}
Write-Host "`nDone!" -ForegroundColor Green
+9 -5
View File
@@ -41,7 +41,7 @@ pub struct IpNeighLine {
// NUDState custom Deserialize now lives in arpparse/impl.rs; use serde_with OneOrMany for Vec // NUDState custom Deserialize now lives in arpparse/impl.rs; use serde_with OneOrMany for Vec
#[derive(Debug, PartialEq, Eq, EnumString, Display, Clone, Copy, Hash, serde::Serialize)] #[derive(Debug, PartialEq, Eq, EnumString, Display, Clone, Copy, Hash, serde::Serialize, Default)]
#[strum(serialize_all = "UPPERCASE", ascii_case_insensitive)] #[strum(serialize_all = "UPPERCASE", ascii_case_insensitive)]
#[serde(rename_all = "UPPERCASE")] #[serde(rename_all = "UPPERCASE")]
pub enum NUDState { pub enum NUDState {
@@ -63,10 +63,7 @@ pub enum NUDState {
/// neighbour state if it was valid and the /// neighbour state if it was valid and the
/// address is not changed by this command. /// address is not changed by this command.
Stale, Stale,
/// this is a pseudo state used when initially
/// creating a neighbour entry or after trying to
/// remove it before it becomes free to do so.
None,
/// the neighbour entry has not (yet) been /// the neighbour entry has not (yet) been
/// validated/resolved. /// validated/resolved.
@@ -80,6 +77,13 @@ pub enum NUDState {
/// success, neighbor validation has ultimately /// success, neighbor validation has ultimately
/// failed. /// failed.
Failed, Failed,
/// this is a pseudo state used when initially
/// creating a neighbour entry or after trying to
/// remove it before it becomes free to do so.
#[serde(other)]
#[default]
None,
} }
impl NUDState { impl NUDState {
+2 -1
View File
@@ -2,7 +2,8 @@
const MAC_NAME_CACHE: &str = "/tmp/wakey_mac_names.json"; const MAC_NAME_CACHE: &str = "/tmp/wakey_mac_names.json";
/// Load MAC->name cache from disk /// Load MAC->name cache from disk
async fn load_mac_name_cache() -> io::Result<std::collections::BTreeMap<String, String>> { pub(crate) async fn load_mac_name_cache() -> io::Result<std::collections::BTreeMap<String, String>>
{
match tokio::fs::read_to_string(MAC_NAME_CACHE).await { match tokio::fs::read_to_string(MAC_NAME_CACHE).await {
Ok(s) => serde_json::from_str(&s).map_err(io::Error::other), Ok(s) => serde_json::from_str(&s).map_err(io::Error::other),
Err(e) if e.kind() == ErrorKind::NotFound => Ok(Default::default()), Err(e) if e.kind() == ErrorKind::NotFound => Ok(Default::default()),
+13 -8
View File
@@ -1,7 +1,14 @@
use axum::{ //! braindead version v0.1.x
Router, //!
routing::{get, post}, //! # whats next
}; //!
//! for version 2 i hope to have:
//!
//! 1. idk reworked frontend;
//! 2. incorporate ip -j;
//! 3. small 1-5 second caching;
use axum::{Router, routing::get};
use tokio::net::TcpListener; use tokio::net::TcpListener;
mod arpparse; mod arpparse;
pub mod assets; pub mod assets;
@@ -10,18 +17,15 @@ mod route;
mod utils; mod utils;
use std::io; use std::io;
const MACHINE_NAME: &str = "lda.lan";
#[cfg(target_os = "linux")] #[cfg(target_os = "linux")]
#[tokio::main] #[tokio::main]
async fn entry() -> io::Result<()> { async fn entry() -> io::Result<()> {
use crate::route::{api_router, home_2, home_2_route, wake_handler}; use crate::route::{api_router, home_2, home_2_route};
let app = Router::new() let app = Router::new()
// .route("/home", get(home)) // .route("/home", get(home))
.route("/", get(home_2)) .route("/", get(home_2))
.merge(home_2_route()) .merge(home_2_route())
.route("/wake", post(wake_handler))
// .route("/status", get(get_status_2)) // .route("/status", get(get_status_2))
.nest("/api", api_router()); .nest("/api", api_router());
@@ -32,6 +36,7 @@ async fn entry() -> io::Result<()> {
#[cfg(not(target_os = "linux"))] #[cfg(not(target_os = "linux"))]
fn main() -> color_eyre::Result<()> { fn main() -> color_eyre::Result<()> {
use std::net::ToSocketAddrs;
color_eyre::install()?; color_eyre::install()?;
// use crate::arpparse::NUDState; // use crate::arpparse::NUDState;
// println!("{}", NUDState::Reachable.to_string().to_lowercase()); // println!("{}", NUDState::Reachable.to_string().to_lowercase());
-99
View File
@@ -1,99 +0,0 @@
pub mod api;
pub mod devs;
pub mod dhcp;
pub mod status;
pub mod wake;
pub use crate::route::api::{DeviceQuery, api_router};
use crate::{
assets::{self},
utils::{ping::_ping_ip, wake::wake},
};
use axum::{
Router,
http::{StatusCode, header},
response::{Html, IntoResponse},
routing::get,
};
use axum_extra::extract::Query;
use crate::utils::route::serve_js;
use crate::{MACHINE_NAME, utils::_status_build};
pub async fn wake_handler(
Query(DeviceQuery { name, .. }): Query<DeviceQuery>,
) -> axum::response::Result<impl IntoResponse> {
match wake(name.as_deref().unwrap_or(MACHINE_NAME)).await {
Ok(0) => Err((StatusCode::NOT_FOUND, "No packets sent!").into()),
Ok(x) => Ok((
StatusCode::ACCEPTED,
format!("{x} packet{s} sent!", s = if x > 1 { "s" } else { "" }),
)),
_ => Err((StatusCode::GATEWAY_TIMEOUT, "Wake failed").into()),
}
}
pub async fn _get_status_2(q: Query<DeviceQuery>) -> Html<String> {
let name = match q {
Query(DeviceQuery {
name: Some(name), ..
}) => name,
_ => MACHINE_NAME.to_string(),
};
Html(_status_build(&name).await)
}
pub async fn _home() -> Html<String> {
Html(format!(
r#"
<html>
<body>
<p><a href="/home_2">Alternate UI</a></p>
<p>the machine is {}! <a href="/status">Status</a></p>
<form method="POST" action="/wake">
<button type="submit">Wake LDA</button>
</form>
</body>
</html>
"#,
if _ping_ip((MACHINE_NAME, 22)).await {
"on"
} else {
"off"
} // match get_ips(MACHINE_NAME).await {
// Ok(ips) => {
// // let addrs: Vec<SocketAddr> = ips.into_iter().map(|ip|(ip, 22).into()).collect();
// }
// Err(_) => "off",
// }
))
}
pub async fn home_2() -> Html<&'static str> {
Html(assets::HOME_2_HTML)
}
pub fn home_2_route() -> Router {
use assets::*;
Router::new()
.route("/home_2", get(|| async { Html(HOME_2_HTML) }))
.route("/home_2/", get(|| async { Html(HOME_2_HTML) }))
.route("/home_2.html", get(|| async { Html(HOME_2_HTML) }))
.route(
"/home_2/styles.css",
get(|| async {
(
[
(header::CONTENT_TYPE, "text/css; charset=utf-8"),
(header::CACHE_CONTROL, "public, max-age=300"),
],
home_2::STYLES_CSS,
)
}),
)
.route("/home_2/main.js", get(|| serve_js(home_2::MAIN_JS)))
.route("/home_2/leases.js", get(|| serve_js(home_2::LEASES_JS)))
.route("/home_2/status.js", get(|| serve_js(home_2::STATUS_JS)))
.route("/home_2/utils.js", get(|| serve_js(home_2::UTILS_JS)))
.route("/home_2/wake.js", get(|| serve_js(home_2::WAKE_JS)))
.route("/home_2/dom.js", get(|| serve_js(home_2::DOM_JS)))
}
+50 -24
View File
@@ -1,32 +1,61 @@
use crate::route::error::ApiError;
use crate::utils::query_parser::{QueryType, parse_query};
use axum::Json; use axum::Json;
use axum::http::StatusCode; use axum::http::StatusCode;
use axum::response::IntoResponse; use axum::response::IntoResponse;
use axum::routing::post; use axum::{extract::Path, response::Redirect};
use axum::{Router, extract::Path, response::Redirect, routing::get};
use crate::utils::route; use crate::route::status::{DeviceQuery, Filters, NamePath};
use crate::utils::query::get_ips;
// Smart redirect: accept IP, MAC, dev, or NUD state and redirect to /api/status accordingly // Smart redirect: accept IP, MAC, dev, or NUD state and redirect to /api/status accordingly
pub async fn status_smart_redirect( pub async fn status_smart_redirect(
Path(q): Path<String>, Path(q): Path<String>,
) -> axum::response::Result<Redirect, impl IntoResponse> { ) -> axum::response::Result<Redirect, impl IntoResponse> {
match serde_html_form::to_string(route::status_smart_redirect(q).await) { // no less bullshit
Ok(e) => Ok(Redirect::to(&format!("/api/status?{e}"))), let query = match parse_query(q) {
Err(e) => Err(( QueryType::Ip(ip_addr) => DeviceQuery {
StatusCode::BAD_GATEWAY, filter: Filters {
Json(StatusError { ips: vec![ip_addr],
error: e.to_string(),
..Default::default() ..Default::default()
}), },
)), ..Default::default()
},
QueryType::Mac(mac_addr) => DeviceQuery {
filter: Filters {
macs: vec![mac_addr],
..Default::default()
},
..Default::default()
},
QueryType::Dev(s) => DeviceQuery {
filter: Filters {
devs: vec![s],
..Default::default()
},
..Default::default()
},
QueryType::Nud(nudstate) => DeviceQuery {
filter: Filters {
nuds: vec![nudstate],
..Default::default()
},
..Default::default()
},
QueryType::Unknown(n) => DeviceQuery {
name: Some(n),
..Default::default()
},
};
match serde_html_form::to_string(query) {
Ok(e) => Ok(Redirect::to(&format!("/api/status?{e}"))),
Err(e) => Err(ApiError {
error: e.to_string(),
code: StatusCode::BAD_GATEWAY,
}),
} }
} }
pub use crate::route::devs::*;
pub use crate::route::dhcp::*;
pub use crate::route::status::*;
pub use crate::route::wake::*;
pub async fn status_redirect(Path(NamePath { name }): Path<NamePath>) -> Redirect { pub async fn status_redirect(Path(NamePath { name }): Path<NamePath>) -> Redirect {
Redirect::permanent(&format!( Redirect::permanent(&format!(
"/api/status?name={name}", "/api/status?name={name}",
@@ -34,12 +63,9 @@ pub async fn status_redirect(Path(NamePath { name }): Path<NamePath>) -> Redirec
)) ))
} }
pub fn api_router() -> Router { pub async fn ip(Path(name): Path<String>) -> impl IntoResponse {
Router::new() get_ips(&name).await.map_or_else(
.route("/status/{name}", get(status_redirect)) |e| ApiError::ise(e.to_string()).into_response(),
.route("/status", get(get_status_json)) |ips| Json(ips.collect::<Vec<_>>()).into_response(),
.route("/dhcp_leases", get(get_dhcp_leases)) )
.route("/smart/{q}", get(status_smart_redirect))
.route("/devs", get(devs_router))
.route("/wake", post(wake_multi))
} }
+12 -11
View File
@@ -1,4 +1,8 @@
use crate::{dhcpparse, route::api::StatusError, utils::parse::boolish_str}; use crate::{
dhcpparse::read_dhcp_leases_with_names,
route::error::ApiError,
utils::{parse::boolish_str, query::enrich_leases_with_nud_state},
};
use axum::{Json, extract::Query, http::StatusCode, response::IntoResponse}; use axum::{Json, extract::Query, http::StatusCode, response::IntoResponse};
// DHCP lease endpoints // DHCP lease endpoints
@@ -14,21 +18,18 @@ pub async fn get_dhcp_leases(Query(raw): Query<DhcpLeasesQueryRaw>) -> impl Into
.map(boolish_str) .map(boolish_str)
.unwrap_or(false); .unwrap_or(false);
match dhcpparse::read_dhcp_leases_with_names().await { match read_dhcp_leases_with_names().await {
Ok(leases_with_names) => { Ok(leases_with_names) => {
if !include_state { if !include_state {
return (StatusCode::OK, Json(leases_with_names)).into_response(); return (StatusCode::OK, Json(leases_with_names)).into_response();
} }
let out = crate::utils::query::enrich_leases_with_nud_state(leases_with_names).await; let out = enrich_leases_with_nud_state(leases_with_names).await;
(StatusCode::OK, Json(out)).into_response() (StatusCode::OK, Json(out)).into_response()
} }
Err(e) => ( Err(e) => ApiError {
StatusCode::BAD_GATEWAY, error: e.to_string(),
Json(StatusError { code: StatusCode::BAD_GATEWAY,
error: e.to_string(), }
..Default::default() .into_response(),
}),
)
.into_response(),
} }
} }
+29
View File
@@ -0,0 +1,29 @@
use axum::{
Json,
http::StatusCode,
response::{IntoResponse, Response},
};
use serde::Serialize;
#[derive(Debug, Serialize)]
pub struct ApiError {
#[serde(skip_serializing, skip_deserializing)]
pub code: StatusCode,
pub error: String,
}
impl ApiError {
/// [StatusCode::INTERNAL_SERVER_ERROR]
pub const fn ise(error: String) -> Self {
Self {
code: StatusCode::INTERNAL_SERVER_ERROR,
error,
}
}
}
impl IntoResponse for ApiError {
fn into_response(self) -> Response {
(self.code, Json(self)).into_response()
}
}
+60
View File
@@ -0,0 +1,60 @@
pub mod api;
pub mod devs;
pub mod dhcp;
pub mod error;
pub mod status;
pub mod wake;
use crate::assets;
use crate::route::api::ip;
use crate::route::api::status_redirect;
use crate::route::api::status_smart_redirect;
use crate::route::devs::devs_router;
use crate::route::dhcp::get_dhcp_leases;
use crate::route::status::get_status_json;
use crate::route::wake::wake_multi;
use axum::routing::post;
use axum::{Router, http::header, response::Html, routing::get};
use crate::utils::route::serve_js;
pub async fn home_2() -> Html<&'static str> {
Html(assets::HOME_2_HTML)
}
pub fn home_2_route() -> Router {
use assets::*;
Router::new()
.route("/home_2", get(|| async { Html(HOME_2_HTML) }))
.route("/home_2/", get(|| async { Html(HOME_2_HTML) }))
.route("/home_2.html", get(|| async { Html(HOME_2_HTML) }))
.route(
"/home_2/styles.css",
get(|| async {
(
[
(header::CONTENT_TYPE, "text/css; charset=utf-8"),
(header::CACHE_CONTROL, "public, max-age=300"),
],
home_2::STYLES_CSS,
)
}),
)
.route("/home_2/main.js", get(|| serve_js(home_2::MAIN_JS)))
.route("/home_2/leases.js", get(|| serve_js(home_2::LEASES_JS)))
.route("/home_2/status.js", get(|| serve_js(home_2::STATUS_JS)))
.route("/home_2/utils.js", get(|| serve_js(home_2::UTILS_JS)))
.route("/home_2/wake.js", get(|| serve_js(home_2::WAKE_JS)))
.route("/home_2/dom.js", get(|| serve_js(home_2::DOM_JS)))
}
pub fn api_router() -> Router {
Router::new()
.route("/status/{name}", get(status_redirect))
.route("/status", get(get_status_json))
.route("/dhcp_leases", get(get_dhcp_leases))
.route("/smart/{q}", get(status_smart_redirect))
.route("/devs", get(devs_router))
.route("/wake", post(wake_multi))
.route("/ips/{name}", get(ip))
}
+46 -78
View File
@@ -1,30 +1,21 @@
use crate::{ use crate::route::error::ApiError;
arpparse::NUDState,
utils::parse::{de_many, serialize_macs},
};
use axum::{Json, http::StatusCode, response::IntoResponse}; use axum::{Json, http::StatusCode, response::IntoResponse};
use axum_extra::extract::Query; use axum_extra::extract::Query;
use macaddr::MacAddr; use macaddr::MacAddr;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use serde_with::skip_serializing_none; use serde_with::skip_serializing_none;
use std::collections::HashSet;
use std::net::IpAddr; use std::net::IpAddr;
use crate::arpparse::NUDState;
use crate::utils::parse::de_many;
use crate::utils::parse::serialize_macs;
use crate::utils::query::get_macs;
#[derive(Debug, Default, Clone, Hash, Deserialize, Serialize)] #[derive(Debug, Default, Clone, Hash, Deserialize, Serialize)]
pub struct DeviceQuery { pub struct DeviceQuery {
pub name: Option<String>, pub name: Option<String>,
#[serde(default, deserialize_with = "de_many::vec_from_strs")] #[serde(flatten)]
pub ip: Vec<IpAddr>, pub filter: Filters,
#[serde(
default,
deserialize_with = "de_many::vec_from_strs",
serialize_with = "serialize_macs"
)]
pub mac: Vec<MacAddr>,
#[serde(default, deserialize_with = "de_many::vec_from_strs")]
pub dev: Vec<String>,
#[serde(default, deserialize_with = "de_many::vec_from_strs")]
pub nud: Vec<NUDState>,
} }
#[derive(Debug, Default, Clone, Hash, Deserialize)] #[derive(Debug, Default, Clone, Hash, Deserialize)]
@@ -40,21 +31,23 @@ pub struct Status<T> {
pub filters: Filters, pub filters: Filters,
} }
#[derive(Debug, Default, Serialize)] #[derive(Debug, Default, Clone, Hash, Serialize, Deserialize)]
pub struct Filters { pub struct Filters {
#[serde(skip_serializing_if = "Vec::is_empty")] #[serde(default, deserialize_with = "de_many::vec_from_strs")]
pub ip: Vec<IpAddr>, pub ips: Vec<IpAddr>,
#[serde(skip_serializing_if = "Vec::is_empty")] #[serde(default, deserialize_with = "de_many::vec_from_strs")]
pub dev: Vec<String>, pub devs: Vec<String>,
#[serde(skip_serializing_if = "Vec::is_empty")] #[serde(default, deserialize_with = "de_many::vec_from_strs")]
pub nud: Vec<NUDState>, pub nuds: Vec<NUDState>,
#[serde( #[serde(
skip_serializing_if = "Vec::is_empty", default,
deserialize_with = "de_many::vec_from_strs",
serialize_with = "serialize_macs" serialize_with = "serialize_macs"
)] )]
pub mac: Vec<MacAddr>, pub macs: Vec<MacAddr>,
} }
#[deprecated = "use ApiError"]
#[skip_serializing_none] #[skip_serializing_none]
#[derive(Debug, Serialize, Default)] #[derive(Debug, Serialize, Default)]
pub struct StatusError { pub struct StatusError {
@@ -65,54 +58,32 @@ pub struct StatusError {
pub async fn get_status_json( pub async fn get_status_json(
Query(DeviceQuery { Query(DeviceQuery {
name, name,
ip, filter:
dev, Filters {
nud, ips,
mac, devs,
nuds,
macs,
},
.. ..
}): Query<DeviceQuery>, }): Query<DeviceQuery>,
) -> impl IntoResponse { ) -> impl IntoResponse {
fn to_opts<T: Clone>(slice: &[T]) -> Vec<Option<T>> { match get_macs(
if slice.is_empty() { &name.iter().collect::<Vec<_>>(),
vec![None] &ips,
} else { &devs.iter().collect::<Vec<_>>(),
slice.iter().cloned().map(Some).collect() &nuds,
} &macs,
} )
let ips_opt = if ip.is_empty() { .await
None
} else {
Some(ip.clone())
};
let dev_opts: Vec<Option<String>> = to_opts(&dev);
let nud_opts: Vec<Option<NUDState>> = to_opts(&nud);
let filters = Filters {
ip,
dev,
nud,
mac: mac.clone(),
};
let mut tasks = Vec::new();
for d in &dev_opts {
for n in &nud_opts {
tasks.push(crate::utils::query::get_macs(
name.as_deref(),
ips_opt.as_deref(),
d.as_deref(),
*n,
));
}
}
// why try join all?
match futures::future::try_join_all(tasks)
.await
.map(|v| v.into_iter().flatten().collect::<Vec<_>>())
{ {
Ok(mut table) => { Ok(table) => {
if !mac.is_empty() { let filters = Filters {
let wanted: HashSet<MacAddr> = mac.into_iter().collect(); ips,
table.retain(|row| row.mac.map(|m| wanted.contains(&m)).unwrap_or(false)); devs,
} nuds,
macs,
};
( (
StatusCode::OK, StatusCode::OK,
Json(Status { Json(Status {
@@ -123,14 +94,11 @@ pub async fn get_status_json(
) )
.into_response() .into_response()
} }
Err(error) => ( Err(error) => ApiError {
StatusCode::BAD_GATEWAY, code: StatusCode::BAD_GATEWAY,
Json(StatusError { error: error.to_string(),
name, }
error: error.to_string(), .into_response(),
}),
)
.into_response(),
} }
} }
// Status endpoints // Status endpoints
+13 -20
View File
@@ -23,9 +23,8 @@ pub struct WakeResult {
#[skip_serializing_none] #[skip_serializing_none]
#[derive(Debug, Serialize, Clone, Copy)] #[derive(Debug, Serialize, Clone, Copy)]
pub struct WakeTargetResult { pub struct WakeTargetResult {
pub ip: Option<IpAddr>, #[serde(flatten)]
#[serde(serialize_with = "ser_opm")] pub target: WakeTarget,
pub mac: Option<MacAddr>,
pub status: WakeTargetStatus, pub status: WakeTargetStatus,
} }
@@ -41,11 +40,11 @@ pub enum WakeTargetStatus {
} }
#[skip_serializing_none] #[skip_serializing_none]
#[derive(Debug, Deserialize, Clone, Copy)] #[derive(Debug, Serialize, Deserialize, Clone, Copy)]
pub struct WakeTarget { pub struct WakeTarget {
#[serde(default)] #[serde(default)]
pub ip: Option<IpAddr>, pub ip: Option<IpAddr>,
#[serde(default, deserialize_with = "des_opm")] #[serde(default, serialize_with = "ser_opm", deserialize_with = "des_opm")]
pub mac: Option<MacAddr>, pub mac: Option<MacAddr>,
} }
@@ -82,21 +81,15 @@ pub async fn wake_multi_split(
.await?; .await?;
sock.set_broadcast(true)?; sock.set_broadcast(true)?;
Ok( let iter = targets.into_iter().map(async |c| {
futures::future::join_all(targets.into_iter().map(async |c| { if c.is_incomplete() {
if c.is_incomplete() { c.to_incomplete()
c.to_incomplete() } else {
} else { let t = c.try_into().expect("complete struct failed to try_into");
wake_one( wake_one(&sock, t).await.into()
&sock, }
c.try_into().expect("complete struct failed to try_into"), });
) Ok(futures::future::join_all(iter).await)
.await
.into()
}
}))
.await,
)
} }
/* #[derive(Debug, Serialize)] /* #[derive(Debug, Serialize)]
pub struct WakeStatusLine { pub struct WakeStatusLine {
-64
View File
@@ -1,64 +0,0 @@
// pub const LDA_MACS: [[u8; 6]; 2] = [
// // ether
// [0x04, 0x7c, 0x16, 0x79, 0x6d, 0xee],
// // wifi
// [0xbc, 0x09, 0x1b, 0xec, 0x65, 0xd0],
// ];
/// is it time to lookup host lda.lan for this...
// pub static LDA_MACS_2: LazyLock<[MacAddr; 2]> = LazyLock::new(|| LDA_MACS.map(MacAddr::from));
pub mod wake;
use crate::utils::query::_get_macs_2_1;
pub mod cmd;
pub mod error;
/// generic so you can do "123.45.67.89:22" or "lda.lan:22" as an input
// this is so bad
pub mod ping;
pub mod query;
pub mod route;
// no custom ip deserializer needed when using axum_extra::extract::Query
// but we add a generic one to ignore blanks and accept OneOrMany
pub(crate) mod parse;
pub async fn _status_build(machine_name: &str) -> String {
let formatted_macs = match _get_macs_2_1(machine_name).await {
Ok(table) => {
let the: String = table
.iter()
.map(|(ip, mac, state)| {
let mac_str = // if let Some(mac) = mac {
mac.to_string()
// } else {
// "None".into()
// }
;
format!(
"<tr><td>{ip}</td><td>{mac_str}</td><td>{state}</td></tr>",
state = state._dumber_state()
)
})
.collect();
format!(
r#"<p>info of {machine_name}:</p>
<table>
<tr><th>IP</th><th>MAC</th><th>State</th></tr>
{the}
</table>"#
)
}
Err(e) => format!("<p>errors getting table for {machine_name}: {e}</p>"),
};
format!(
r#"
<html>
<body>
{formatted_macs}
</body>
</html>
"#,
)
}
+23
View File
@@ -0,0 +1,23 @@
// pub const LDA_MACS: [[u8; 6]; 2] = [
// // ether
// [0x04, 0x7c, 0x16, 0x79, 0x6d, 0xee],
// // wifi
// [0xbc, 0x09, 0x1b, 0xec, 0x65, 0xd0],
// ];
/// is it time to lookup host lda.lan for this...
// pub static LDA_MACS_2: LazyLock<[MacAddr; 2]> = LazyLock::new(|| LDA_MACS.map(MacAddr::from));
pub mod wake;
pub mod cmd;
pub mod error;
/// generic so you can do "123.45.67.89:22" or "lda.lan:22" as an input
// this is so bad
pub mod ping;
pub mod query;
pub mod query_parser;
pub mod route;
// no custom ip deserializer needed when using axum_extra::extract::Query
// but we add a generic one to ignore blanks and accept OneOrMany
pub(crate) mod parse;
+2 -2
View File
@@ -7,7 +7,7 @@ use tokio::{
time::timeout, time::timeout,
}; };
use crate::{arpparse::NUDState, utils::query::get_macs}; use crate::{arpparse::NUDState, utils::query::get_mac};
pub async fn _ping_ip<T: ToSocketAddrs>(addr: T) -> bool { pub async fn _ping_ip<T: ToSocketAddrs>(addr: T) -> bool {
timeout(Duration::from_secs(1), TcpStream::connect(addr)) timeout(Duration::from_secs(1), TcpStream::connect(addr))
@@ -19,7 +19,7 @@ pub async fn _ping_ip_2<T: ToSocketAddrs>(_addr: T) -> bool {
} }
pub async fn _ping_ip_3<T: Into<IpAddr>>(addr: T) -> u8 { pub async fn _ping_ip_3<T: Into<IpAddr>>(addr: T) -> u8 {
match get_macs(None, Some(&[addr.into()]), None, None).await { match get_mac(Some(addr.into()), None, &[] as &[NUDState]).await {
Err(_) => 0, Err(_) => 0,
Ok(l) => l Ok(l) => l
.into_iter() .into_iter()
+12 -2
View File
@@ -1,10 +1,20 @@
use std::collections::HashSet; use std::{collections::HashSet, fs};
pub fn get_dev() -> HashSet<String> { pub fn get_dev() -> HashSet<String> {
let mut devs: HashSet<String> = HashSet::new(); let mut devs: HashSet<String> = HashSet::new();
if let Ok(rd) = std::fs::read_dir("/sys/class/net") { if let Ok(rd) = std::fs::read_dir("/sys/class/net") {
for e in rd.flatten() { for e in rd.flatten() {
if let Ok(name) = e.file_name().into_string() if e.file_type()
.map(|ft| {
if ft.is_symlink() {
// true
fs::metadata(e.path()).map(|m| m.is_dir()).unwrap_or(false)
} else {
ft.is_dir()
}
})
.unwrap_or(false)
&& let Ok(name) = e.file_name().into_string()
&& name != "lo" && name != "lo"
&& !name.is_empty() && !name.is_empty()
{ {
+2 -2
View File
@@ -1,5 +1,6 @@
use crate::arpparse::NUDState; use crate::arpparse::NUDState;
use crate::dhcpparse::DhcpLeaseLine; use crate::dhcpparse::DhcpLeaseLine;
use crate::utils::query::get_macs;
use serde_with::skip_serializing_none; use serde_with::skip_serializing_none;
use std::net::IpAddr; use std::net::IpAddr;
@@ -14,11 +15,10 @@ pub struct DhcpLeaseOut {
/// Enrich DHCP leases with NUD state and rank using get_macs /// Enrich DHCP leases with NUD state and rank using get_macs
pub async fn enrich_leases_with_nud_state(leases: Vec<DhcpLeaseLine>) -> Vec<DhcpLeaseOut> { pub async fn enrich_leases_with_nud_state(leases: Vec<DhcpLeaseLine>) -> Vec<DhcpLeaseOut> {
use crate::utils::query::macs::get_macs;
let ips: Vec<IpAddr> = leases.iter().map(|l| l.ip).collect(); let ips: Vec<IpAddr> = leases.iter().map(|l| l.ip).collect();
let mut map: std::collections::HashMap<IpAddr, (NUDState, u8)> = let mut map: std::collections::HashMap<IpAddr, (NUDState, u8)> =
std::collections::HashMap::new(); std::collections::HashMap::new();
if let Ok(rows) = get_macs(None, Some(&ips), None, None).await { if let Ok(rows) = get_macs(&[] as &[&str], &ips, &[] as &[&str], &[], &[]).await {
for row in rows { for row in rows {
let state = row.state; let state = row.state;
let r = state.rank(); let r = state.rank();
+95 -82
View File
@@ -1,13 +1,14 @@
use macaddr::MacAddr;
use crate::arpparse::{self, IpNeighLine, NUDState}; use crate::arpparse::{self, IpNeighLine, NUDState};
use crate::utils::{ use crate::utils::{
cmd::exec_command, cmd::exec_command,
error::{self, Error, Result}, error::{self, Result},
}; };
use macaddr::MacAddr;
use std::collections::HashSet; use std::collections::HashSet;
use std::net::IpAddr; use std::net::IpAddr;
pub async fn get_ips(machine_name: &str) -> error::Result<impl Iterator<Item = IpAddr>> { pub async fn get_ips(machine_name: &str) -> Result<impl Iterator<Item = IpAddr>> {
Ok(tokio::net::lookup_host((machine_name, 0)) Ok(tokio::net::lookup_host((machine_name, 0))
.await .await
.map_err(|e| error::Error::DnsResolve { .map_err(|e| error::Error::DnsResolve {
@@ -17,92 +18,102 @@ pub async fn get_ips(machine_name: &str) -> error::Result<impl Iterator<Item = I
.map(|c| c.ip())) .map(|c| c.ip()))
} }
pub async fn _get_macs_2_1(machine_name: &str) -> Result<HashSet<(IpAddr, MacAddr, NUDState)>> { // #[deprecated(
Ok(get_macs_1(machine_name) // since = "0.1.5",
.await? // note = "just call once everything with get mac
.into_iter() // and then filter it bro WHY DO YOU EVEN DO TS"
.filter_map( // )]
|IpNeighLine { // good now
ip, //
dev: _, // Current logic: When filtering by exactly 1 dev/mac, exclude entries missing that field.
mac, // This is because missing dev/mac usually means the entry is incomplete/transient.
state, //
}| mac.map(|mac| (ip, mac, state)), // when there is only one MACs (getmac got some), the result will not have them fields.
) // so there are three cases:
.collect()) //
} // 1. dont got nothing: take all of them (macset.is_empty())
// 2. exactly one: pre-filtered by ip, everything matches,
pub async fn get_macs_2_mac(machine_name: &str) -> Result<HashSet<MacAddr>> { // devset.len() != 1 returns false, but then it works????
Ok(get_macs_1(machine_name) // OH THIS fuckass code i added it in the get_mac
.await? // 3. devset.len() > 1. if none then absolutely not match,
.into_iter() // if some then check with the set; thats normal
.filter_map(
|IpNeighLine {
ip: _,
dev: _,
mac,
state: _,
}| mac,
)
.collect())
}
pub async fn get_macs_1(machine_name: &str) -> Result<Vec<arpparse::IpNeighLine>> {
let dev = "br-lan";
let ips = get_ips(machine_name).await?;
let futures = ips.map(|ip| {
let ip = ip.to_canonical();
async move {
let cmd = "ip";
let args = ["neigh", "show", "to", &ip.to_string(), "dev", dev];
let o = exec_command(cmd, args).await?;
if !o.status.success() {
return Err(Error::CommandFailed {
cmd,
args: args.iter().map(ToString::to_string).collect(),
status: o.status.code(),
stderr: String::from_utf8_lossy(&o.stderr).into(),
});
};
Ok(String::from_utf8_lossy(&o.stdout)
.lines()
.flat_map(arpparse::parse_ip_neigh_line)
.collect::<Vec<_>>())
}
});
let res = futures::future::try_join_all(futures).await?;
Ok(res.into_iter().flatten().collect())
}
pub async fn get_macs( pub async fn get_macs(
machine_name: Option<&str>, machine_names: &[impl AsRef<str>],
ips: Option<&[IpAddr]>, ips: &[IpAddr],
dev: Option<&str>, devs: &[impl AsRef<str>],
state: Option<NUDState>, state: &[NUDState],
macs: &[MacAddr],
) -> Result<Vec<IpNeighLine>> { ) -> Result<Vec<IpNeighLine>> {
let ip_list: Option<Vec<IpAddr>> = let mut ip_set: HashSet<IpAddr> = ips.iter().map(|ip| ip.to_canonical()).collect();
ips.map(|slice| slice.iter().copied().map(|ip| ip.to_canonical()).collect()); let ip_m: HashSet<IpAddr> = futures::future::try_join_all(
let ip_list = match (ip_list, machine_name) { machine_names
(Some(list), _) => list, .iter()
(None, Some(name)) => get_ips(name).await?.collect(), .map(|c| get_ips(c.as_ref())),
(None, None) => Vec::new(), )
}; .await?
let run_one = |to_ip: Option<IpAddr>| get_mac(to_ip, dev, state); .into_iter()
if !ip_list.is_empty() { .flatten()
let futures = ip_list.into_iter().map(|ip| run_one(Some(ip))); .collect();
let res = futures::future::try_join_all(futures).await?; let ip_all = if ip_set.is_empty() && ip_m.is_empty() {
Ok(res.into_iter().flatten().collect()) None
} else if ip_set.is_empty() {
Some(ip_m)
} else if ip_m.is_empty() {
Some(ip_set)
} else { } else {
run_one(None).await Some({
} ip_set.retain(|c| ip_m.contains(c)); // inline AHHH
ip_set
})
};
let opt_dev = if devs.len() > 1 {
None
} else {
devs.iter().next().map(AsRef::as_ref)
};
let run_one = |to_ip: Option<IpAddr>| get_mac(to_ip, opt_dev, state);
let mut ip_filtered = if let Some(something) = ip_all {
if something.len() == 1 {
run_one(something.into_iter().next()).await?
} else {
run_one(None)
.await?
.into_iter()
.filter(|c| something.contains(&c.ip))
.collect()
}
} else {
run_one(None).await?
};
// Apply additional filters if any were provided
if !devs.is_empty() || !macs.is_empty() || !state.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. /// the atomic get_macs. handle ONE thing only.
pub async fn get_mac( pub async fn get_mac(
ip: Option<IpAddr>, ip: Option<IpAddr>,
dev: Option<&str>, dev: Option<&str>,
state: Option<NUDState>, state: &[NUDState],
) -> error::Result<Vec<IpNeighLine>> { ) -> Result<Vec<IpNeighLine>> {
let mut args: Vec<String> = vec!["neigh".into(), "show".into()]; let mut args: Vec<String> = vec!["neigh".into(), "show".into()];
if let Some(ip) = ip { if let Some(ip) = ip {
args.push("to".into()); args.push("to".into());
@@ -112,7 +123,7 @@ pub async fn get_mac(
args.push("dev".into()); args.push("dev".into());
args.push(d.to_string()); args.push(d.to_string());
} }
if let Some(nud) = state { for nud in state {
args.push("nud".into()); args.push("nud".into());
args.push(nud.as_ip_neigh_arg().into()); args.push(nud.as_ip_neigh_arg().into());
} }
@@ -136,7 +147,9 @@ pub async fn get_mac(
Ok(rows) Ok(rows)
} }
/* pub async fn _get_macs_good( /*
/// get macs where you just run ip neigh then rust handles the filtering (faster than get mac)
pub async fn get_macs_rust(
machine_names: Option<&str>, machine_names: Option<&str>,
ips: Option<&[IpAddr]>, ips: Option<&[IpAddr]>,
devs: Option<&[&str]>, devs: Option<&[&str]>,
File renamed without changes.
+44
View File
@@ -0,0 +1,44 @@
use std::net::IpAddr;
use macaddr::MacAddr;
use crate::{arpparse::NUDState, utils::query::dev::has_dev};
pub enum QueryType {
Ip(IpAddr),
Mac(MacAddr),
Dev(String),
Nud(NUDState),
Unknown(String),
}
pub fn parse_query(q: String) -> QueryType {
let s = if cfg!(feature = "very-smart-parsing") {
crate::utils::parse::extract_host(&q)
} else {
q.trim()
};
// 1) IP
let ip = if cfg!(feature = "very-smart-parsing") {
crate::utils::parse::parse_numeric_ipv4(s).or_else(|| s.parse::<IpAddr>().ok())
} else {
s.parse::<IpAddr>().ok()
};
if let Some(ip) = ip {
return QueryType::Ip(ip);
}
// 2) MAC
if let Ok(mac) = s.parse::<MacAddr>() {
return QueryType::Mac(mac);
}
// 3) NUD state (reachable, stale, ...)
if let Ok(state) = s.parse::<NUDState>() {
return QueryType::Nud(state);
}
// 4) Known device? prefer dev first
if has_dev(s) {
return QueryType::Dev(s.to_string());
}
// Default: name last // it will fail also
QueryType::Unknown(s.to_string())
}
-61
View File
@@ -1,9 +1,4 @@
use std::net::IpAddr;
use axum::{http::header, response::IntoResponse}; use axum::{http::header, response::IntoResponse};
use macaddr::MacAddr;
use crate::{arpparse::NUDState, route::DeviceQuery, utils::query::dev::has_dev};
pub async fn serve_js(content: &'static str) -> impl IntoResponse { pub async fn serve_js(content: &'static str) -> impl IntoResponse {
( (
@@ -14,59 +9,3 @@ pub async fn serve_js(content: &'static str) -> impl IntoResponse {
content, content,
) )
} }
pub async fn status_smart_redirect(q: String) -> DeviceQuery {
let s = if cfg!(feature = "very-smart-parsing") {
crate::utils::parse::extract_host(&q)
} else {
q.trim()
};
// 1) IP
let ip = if cfg!(feature = "very-smart-parsing") {
crate::utils::parse::parse_numeric_ipv4(s).or_else(|| s.parse::<IpAddr>().ok())
} else {
s.parse::<IpAddr>().ok()
};
if let Some(ip) = ip {
let ip = vec![ip];
return DeviceQuery {
ip,
..Default::default()
};
}
// 2) MAC
if let Ok(mac) = s.parse::<MacAddr>() {
let mac = vec![mac];
return DeviceQuery {
mac,
..Default::default()
};
}
// 3) NUD state (reachable, stale, ...)
if let Ok(state) = s.parse::<NUDState>() {
let nud = vec![state];
return DeviceQuery {
nud,
..Default::default()
};
}
// 4) Known device? prefer dev first
if has_dev(s) {
return DeviceQuery {
dev: vec![s.to_string()],
..Default::default()
};
}
// 5) Try DNS: if it resolves, treat as name
if tokio::net::lookup_host((s, 0)).await.is_ok() {
return DeviceQuery {
name: Some(s.to_string()),
..Default::default()
};
}
// Default: name last // it will fail also
DeviceQuery {
name: Some(s.to_string()),
..Default::default()
}
}
+13 -6
View File
@@ -22,25 +22,32 @@ impl TryFrom<RouteWakeTarget> for WakeTarget {
} }
} }
impl From<WakeTargetResult> for RouteWakeResult { impl From<WakeTarget> for RouteWakeTarget {
fn from(WakeTargetResult { ip, mac, status }: WakeTargetResult) -> Self { fn from(WakeTarget { ip, mac }: WakeTarget) -> Self {
Self { Self {
ip: Some(ip), ip: Some(ip),
mac: Some(mac), mac: Some(mac),
}
}
}
impl From<WakeTargetResult> for RouteWakeResult {
fn from(WakeTargetResult { target, status }: WakeTargetResult) -> Self {
Self {
target: target.into(),
status: status.into(), status: status.into(),
} }
} }
} }
impl RouteWakeTarget { impl RouteWakeTarget {
pub fn to_incomplete(self) -> RouteWakeResult { pub const fn to_incomplete(self) -> RouteWakeResult {
RouteWakeResult { RouteWakeResult {
ip: self.ip, target: self,
mac: self.mac,
status: RouteWakeStatus::Incomplete, status: RouteWakeStatus::Incomplete,
} }
} }
pub fn is_incomplete(&self) -> bool { pub const fn is_incomplete(&self) -> bool {
!matches!( !matches!(
self, self,
Self { Self {
+10 -40
View File
@@ -5,35 +5,6 @@ use futures::TryFutureExt;
use macaddr::MacAddr; use macaddr::MacAddr;
use tokio::net::UdpSocket; use tokio::net::UdpSocket;
use crate::utils::query::get_macs_2_mac;
pub async fn wake(machine_name: &str) -> io::Result<u32> {
let suh = UdpSocket::bind("0.0.0.0:0").await?;
suh.set_broadcast(true)?;
let /* mut */ macs = get_macs_2_mac(machine_name).await.unwrap_or_default();
// macs.extend(*LDA_MACS_2);
let mut sent_ok = 0;
for mac in macs {
let mb = mac.as_bytes();
let mut pac = [0; 6 + 6 * 16]; // 6x FF + 16x mac6
pac[..6].fill(0xff);
for i in 1..=16 {
pac[i * 6..(i + 1) * 6].copy_from_slice(mb);
}
match suh
.send_to(&pac, (IpAddr::from([192, 168, 100, 255]), 9))
.await
{
Ok(n) if n == pac.len() => sent_ok += 1,
Ok(n) => eprintln!("partial send ({n}/{})", pac.len()),
Err(e) => eprintln!("send error: {e}"),
}
}
Ok(sent_ok)
}
#[derive(Debug, Clone, Copy, Hash)] #[derive(Debug, Clone, Copy, Hash)]
pub struct WakeTarget { pub struct WakeTarget {
pub ip: IpAddr, pub ip: IpAddr,
@@ -41,8 +12,7 @@ pub struct WakeTarget {
} }
#[derive(Debug, Clone, Copy, Hash)] #[derive(Debug, Clone, Copy, Hash)]
pub struct WakeTargetResult { pub struct WakeTargetResult {
pub ip: IpAddr, pub target: WakeTarget,
pub mac: MacAddr,
pub status: WakeStatus, pub status: WakeStatus,
} }
#[derive(Debug, Clone, Copy, Hash)] #[derive(Debug, Clone, Copy, Hash)]
@@ -52,22 +22,22 @@ pub enum WakeStatus {
WrongSize, WrongSize,
} }
impl WakeTarget { impl WakeTarget {
fn _new(ip: IpAddr, mac: MacAddr) -> Self { const fn _new(ip: IpAddr, mac: MacAddr) -> Self {
Self { ip, mac } Self { ip, mac }
} }
fn good(self) -> WakeTargetResult { const fn good(self) -> WakeTargetResult {
WakeTargetResult::new(self.ip, self.mac, WakeStatus::Success) WakeTargetResult::new(self, WakeStatus::Success)
} }
fn bad(self) -> WakeTargetResult { const fn bad(self) -> WakeTargetResult {
WakeTargetResult::new(self.ip, self.mac, WakeStatus::WrongSize) WakeTargetResult::new(self, WakeStatus::WrongSize)
} }
fn errored(self) -> WakeTargetResult { const fn errored(self) -> WakeTargetResult {
WakeTargetResult::new(self.ip, self.mac, WakeStatus::NonexistentAddress) WakeTargetResult::new(self, WakeStatus::NonexistentAddress)
} }
} }
impl WakeTargetResult { impl WakeTargetResult {
fn new(ip: IpAddr, mac: MacAddr, status: WakeStatus) -> Self { const fn new(target: WakeTarget, status: WakeStatus) -> Self {
Self { ip, mac, status } Self { target, status }
} }
} }
+1 -1
View File
@@ -27,7 +27,7 @@
<div class="row muted" style="gap: 16px"> <div class="row muted" style="gap: 16px">
<span <span
>Uses query >Uses query
<span title="available keys: name, ip, mac, dev, nud" <span title="available keys: name, ips, macs, devs, nuds"
>(?name=...)</span >(?name=...)</span
> >
on this page to view the status.<!-- and header (X-Target-Name) so either extractor on this page to view the status.<!-- and header (X-Target-Name) so either extractor
+12
View File
@@ -1,5 +1,17 @@
import { elLeases } from "./dom.js"; import { elLeases } from "./dom.js";
/**
*
* @param {{
* expires_epoch: Number
* rank?: Number
* ip: String
* mac: String
* nud_state?: String
* name?: String
* }[]} leases
* @returns
*/
export function renderLeases(leases) { export function renderLeases(leases) {
if (!elLeases) return; if (!elLeases) return;
if (!Array.isArray(leases) || leases.length === 0) { if (!Array.isArray(leases) || leases.length === 0) {
+89 -3
View File
@@ -2,6 +2,7 @@ import { elHtml, elLog, setPill, qs, pill } from "./dom.js";
import { rankState } from "./utils.js"; import { rankState } from "./utils.js";
import { merge_wake_data, translate_wake_message } from "./wake.js"; import { merge_wake_data, translate_wake_message } from "./wake.js";
// IpNeighLine
const status_map = { const status_map = {
ip: "ip", ip: "ip",
mac: "mac", mac: "mac",
@@ -9,8 +10,14 @@ const status_map = {
dev: "interface", dev: "interface",
}; };
export const status_array = Object.keys(status_map); export const status_array = Object.keys(status_map);
export const filter_array = ["ip", "dev", "nud", "mac"]; // Filters
export const filter_array = ["ips", "devs", "nuds", "macs"];
/**
*
* @param {String} name
* @returns {URL}
*/
function buildStatusUrl(name) { function buildStatusUrl(name) {
const hasExtraFilters = filter_array.some((k) => qs.getAll(k).length); const hasExtraFilters = filter_array.some((k) => qs.getAll(k).length);
if (name && !hasExtraFilters) { if (name && !hasExtraFilters) {
@@ -25,6 +32,25 @@ function buildStatusUrl(name) {
return u; return u;
} }
/**
*
* @param {{
* has_wake?: true
* table: {
* wake_status?: Boolean
* ips: String
* dev: String
* mac: String
* state: String
* }[]
* filters: {
* ips?: String[]
* devs?: String[]
* nuds?: String[]
* macs?: String[]
* }
* }} data
*/
export function renderStatus(data) { export function renderStatus(data) {
const tbl = document.createElement("table"); const tbl = document.createElement("table");
tbl.className = "table"; tbl.className = "table";
@@ -82,12 +108,51 @@ export function renderStatus(data) {
if (r >= 5) setPill("ok", "online"); if (r >= 5) setPill("ok", "online");
else if (r >= 2) setPill("warn", "maybe"); else if (r >= 2) setPill("warn", "maybe");
else setPill("bad", "offline"); else setPill("bad", "offline");
if (data.filters.nud?.length > 0) pill.textContent += " (filtered)"; if (data.filters.nuds?.length > 0) pill.textContent += " (filtered)";
} else { } else {
setPill("warn", "unknown"); setPill("warn", "unknown");
} }
} }
/**
* @param {String} name
* @param {Boolean} render
* @param {{
* ip: String,
* mac: String,
* status:
* "incomplete" | "succeed" | "nonexistent_address" | "wrong_size"
* }} data_wake
* @returns {{
* has_wake: false
* table: {
* ip: String
* dev: String
* mac: String
* state: String
* }[]
* filters: {
* ips?: String[]
* devs?: String[]
* nuds?: String[]
* macs?: String[]
* }
* } | {
* has_wake: true
* table: {
* wake_status: Boolean
* ips: String
* dev: String
* mac: String
* state: String
* }[]
* filters: {
* ips?: String[]
* devs?: String[]
* nuds?: String[]
* macs?: String[]
* }
* }}
*/
export async function fetchStatus(name, render = true, data_wake) { export async function fetchStatus(name, render = true, data_wake) {
setPill("warn", "checking…"); setPill("warn", "checking…");
const u = buildStatusUrl(name); const u = buildStatusUrl(name);
@@ -97,6 +162,7 @@ export async function fetchStatus(name, render = true, data_wake) {
if (!r.ok) { if (!r.ok) {
let msg = String(r.status); let msg = String(r.status);
try { try {
/** @type {{error: string}} */
const err = await r.clone().json(); const err = await r.clone().json();
msg = err.error || JSON.stringify(err); msg = err.error || JSON.stringify(err);
} catch { } catch {
@@ -106,6 +172,26 @@ export async function fetchStatus(name, render = true, data_wake) {
setPill("bad", "error"); setPill("bad", "error");
return; return;
} }
/**
* this one does not have data wake
* @type {{
* table: {
* ip: String
* dev: String
* mac: String
* state: String
* wake_status?: String
* }[]
* filters: {
* ips?: String[]
* devs?: String[]
* nuds?: String[]
* macs?: String[]
* }
* has_wake?: true
* }}
*/
const data = await r.json(); const data = await r.json();
if (data_wake) { if (data_wake) {
+7
View File
@@ -15,6 +15,13 @@ body {
padding: 0; padding: 0;
font-family: system-ui, Segoe UI, Roboto, Helvetica, Arial, sans-serif; font-family: system-ui, Segoe UI, Roboto, Helvetica, Arial, sans-serif;
} }
@media screen and (max-width: 768px) {
wrap {
margin-inline: 8px;
}
}
body { body {
display: flex; display: flex;
min-height: 100dvh; min-height: 100dvh;
+31 -3
View File
@@ -1,6 +1,9 @@
import { elHtml, elLog, setPill } from "./dom.js"; import { elHtml, elLog, setPill } from "./dom.js";
import { fetchStatus, renderStatus } from "./status.js"; import { fetchStatus, renderStatus } from "./status.js";
/**
* @param {String} name just plain name
*/
export async function sendWake(name) { export async function sendWake(name) {
const data = await fetchStatus(name, false); const data = await fetchStatus(name, false);
if (!data) return; // can not proceed; theres nothing. if (!data) return; // can not proceed; theres nothing.
@@ -51,8 +54,29 @@ export async function sendWake(name) {
/** /**
* *
* @param {Array} table * @param {{
* @param {Array} wake * ip: String
* dev: String
* mac: String
* state: String
* }[]} table
* @param {{
* ip: String,
* mac: String,
* status: "incomplete" | "succeed" | "nonexistent_address" | "wrong_size"
* }[]} wake
* @returns {{
* ip: String
* dev: String
* mac: String
* state: String
* }[] | {
* ip: String
* dev: String
* mac: String
* state: String
* wake_status: boolean
* }[]}
*/ */
export function merge_wake_data(table, wake) { export function merge_wake_data(table, wake) {
if (!wake) return table; if (!wake) return table;
@@ -90,7 +114,11 @@ export function merge_wake_data(table, wake) {
return return_array; return return_array;
} }
/**
*
* @param {"incomplete" | "succeed" | "nonexistent_address" | "wrong_size" | any} wake_msg
* @returns {string}
*/
export function translate_wake_message(wake_msg) { export function translate_wake_message(wake_msg) {
switch (wake_msg) { switch (wake_msg) {
case "incomplete": case "incomplete":