move things around do some BullShit

This commit is contained in:
lda
2025-08-27 16:15:36 +07:00 Unverified
parent f40c7e4b5c
commit 5782f7f315
35 changed files with 1321 additions and 1001 deletions
+49 -14
View File
@@ -58,8 +58,6 @@ pub fn extract_host(input: &str) -> &str {
s.trim()
}
use macaddr::MacAddr;
use serde::Serializer;
/// key for yes: "1" | "true" | "yes" | "on" | "y"
///
/// frfr
@@ -102,18 +100,6 @@ pub fn boolish_str(s: &str) -> bool {
&& t.parse::<u64>().map(|n| n != 0).unwrap_or(false))
}
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)
}
pub fn serialize_mac<S: serde::Serializer>(m: &macaddr::MacAddr, s: S) -> Result<S::Ok, S::Error> {
s.serialize_str(&m.to_string())
}
pub mod de_many {
use serde::Deserialize;
use serde::de;
@@ -153,3 +139,52 @@ pub mod de_many {
Ok(out)
}
}
pub mod mac {
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_mac<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_mac<'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)
}
/// serialize an [`Option<MacAddr>`]
pub fn ser_opm<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 des_opm<'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)
}
}
pub use mac::*;
+5 -239
View File
@@ -1,240 +1,6 @@
use crate::arpparse::NUDState;
use crate::dhcpparse::DhcpLeaseLine;
use crate::utils::parse::serialize_mac;
use std::net::IpAddr;
pub mod dev;
pub mod leases;
pub mod macs;
#[skip_serializing_none]
#[derive(Debug, Clone, serde::Serialize)]
pub struct DhcpLeaseOut {
pub expires_epoch: u64,
pub ip: IpAddr,
#[serde(serialize_with = "serialize_mac")]
pub mac: macaddr::MacAddr,
pub name: Option<String>,
pub nud_state: Option<NUDState>,
pub rank: Option<u8>,
}
/// Enrich DHCP leases with NUD state and rank using get_macs
pub async fn enrich_leases_with_nud_state(leases: Vec<DhcpLeaseLine>) -> Vec<DhcpLeaseOut> {
use crate::utils::query::get_macs;
let ips: Vec<IpAddr> = leases.iter().map(|l| l.ip).collect();
let mut map: std::collections::HashMap<IpAddr, (NUDState, u8)> =
std::collections::HashMap::new();
if let Ok(rows) = get_macs(None, Some(&ips), None, None).await {
for row in rows {
let state = row.state;
let r = state.rank();
map.entry(row.ip)
.and_modify(|e| {
if r > e.1 {
*e = (state, r)
}
})
.or_insert((state, r));
}
}
leases
.into_iter()
.map(|l| DhcpLeaseOut {
expires_epoch: l.expires_epoch,
ip: l.ip,
mac: l.mac,
name: l.name,
nud_state: map.get(&l.ip).map(|(s, _)| *s),
rank: map.get(&l.ip).map(|(_, r)| *r),
})
.collect()
}
use std::collections::HashSet;
use macaddr::MacAddr;
use serde_with::skip_serializing_none;
// use tokio::io;
use crate::{
arpparse::{self, IpNeighLine},
utils::{
cmd::exec_command,
error::{self, Error, Result},
},
};
/// this is because i like [`IpAddr`] more than [`SocketAddr`](std::net::SocketAddr)
pub async fn get_ips(machine_name: &str) -> error::Result<Vec<IpAddr>> {
let it = tokio::net::lookup_host((machine_name, 0))
.await
.map_err(|e| error::Error::DnsResolve {
name: machine_name.to_string(),
source: e,
})?;
Ok(it.map(|c| c.ip()).collect())
}
pub async fn _get_macs_2_1(machine_name: &str) -> Result<HashSet<(IpAddr, MacAddr, NUDState)>> {
Ok(get_macs_1(machine_name)
.await?
.into_iter()
.filter_map(
|IpNeighLine {
ip,
dev: _,
mac,
state,
}| mac.map(|mac| (ip, mac, state)),
)
.collect())
}
pub async fn get_macs_2_mac(machine_name: &str) -> Result<HashSet<MacAddr>> {
Ok(get_macs_1(machine_name)
.await?
.into_iter()
.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.iter().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)
// .map(IpNeighLine::with_dev(dev)) // this could be after flatmap up there
.collect::<Vec<_>>())
}
});
let res = futures::future::try_join_all(futures).await?; // async move block errs.
Ok(res
.into_iter()
.flatten() /* resolve double vec */
// .flatten() /* drop parse errors (flat_map cleared) */
.collect())
}
pub async fn get_macs(
machine_name: Option<&str>,
ips: Option<&[IpAddr]>,
dev: Option<&str>,
state: Option<NUDState>,
) -> Result<Vec<IpNeighLine>> {
// Collect IPs early (before any await) to avoid holding generics across await points
let ip_list: Option<Vec<IpAddr>> =
ips.map(|slice| slice.iter().copied().map(|ip| ip.to_canonical()).collect());
// Resolve by machine name if no IPs provided but we have a name
let ip_list = match (ip_list, machine_name) {
(Some(list), _) => list,
(None, Some(name)) => get_ips(name).await?.into_iter().collect(),
(None, None) => Vec::new(),
};
// Helper to convert NUDState to the string expected by `ip neigh`
let nud_arg = state.map(NUDState::as_ip_neigh_arg);
// let nud_arg = state.map(|s| s.to_string().to_lowercase());
// Build a closure to run one `ip neigh` invocation and parse results
let run_one = |to_ip: Option<IpAddr>| async move {
let mut args: Vec<String> = vec!["neigh".into(), "show".into()];
if let Some(ip) = to_ip {
args.push("to".into());
args.push(ip.to_string());
}
if let Some(d) = dev {
args.push("dev".into());
args.push(d.to_string());
}
if let Some(nud) = nud_arg {
args.push("nud".into());
args.push(nud.to_string());
}
let o = exec_command("ip", args.iter().map(String::as_str).collect::<Vec<_>>()).await?; // hope to rustc that it knows how to unfuck ts
if !o.status.success() {
return Err(Error::CommandFailed {
cmd: "ip",
args,
status: o.status.code(),
stderr: String::from_utf8_lossy(&o.stderr).into(),
});
}
let lines = String::from_utf8_lossy(&o.stdout);
// Parse lines and, if a specific dev filter was used, stamp that dev onto rows
let parsed = lines.lines().flat_map(arpparse::parse_ip_neigh_line);
let rows: Vec<IpNeighLine> = if let Some(d) = dev {
parsed.map(IpNeighLine::with_dev(d)).collect()
} else {
parsed.collect()
};
Ok::<Vec<IpNeighLine>, error::Error>(rows)
};
// If we have specific IPs, query each; otherwise query the whole table once
if !ip_list.is_empty() {
let futures = ip_list.into_iter().map(|ip| run_one(Some(ip)));
let res = futures::future::try_join_all(futures).await?;
Ok(res.into_iter().flatten().collect())
} else {
run_one(None).await
}
}
pub mod dev {
use std::collections::HashSet;
pub fn get_dev() -> HashSet<String> {
// Prefer /sys/class/net, fallback to /proc/net/dev; filter out loopback
let mut devs: HashSet<String> = HashSet::new();
if let Ok(rd) = std::fs::read_dir("/sys/class/net") {
for e in rd.flatten() {
if let Ok(name) = e.file_name().into_string()
&& name != "lo"
&& !name.is_empty()
{
devs.insert(name);
}
}
} else if let Ok(txt) = std::fs::read_to_string("/proc/net/dev") {
for line in txt.lines().skip(2) {
// skip headers
if let Some((name, _rest)) = line.split_once(':') {
let n = name.trim().to_string();
if n != "lo" && !n.is_empty() {
devs.insert(n);
}
}
}
}
devs
}
pub fn devs_sorted() -> Vec<String> {
let mut v: Vec<String> = get_dev().into_iter().collect();
v.sort();
v
}
pub fn has_dev(name: &str) -> bool {
get_dev().contains(name)
}
}
pub use leases::*;
pub use macs::*;
+35
View File
@@ -0,0 +1,35 @@
use std::collections::HashSet;
pub fn get_dev() -> HashSet<String> {
let mut devs: HashSet<String> = HashSet::new();
if let Ok(rd) = std::fs::read_dir("/sys/class/net") {
for e in rd.flatten() {
if let Ok(name) = e.file_name().into_string()
&& name != "lo"
&& !name.is_empty()
{
devs.insert(name);
}
}
} else if let Ok(txt) = std::fs::read_to_string("/proc/net/dev") {
for line in txt.lines().skip(2) {
if let Some((name, _rest)) = line.split_once(':') {
let n = name.trim().to_string();
if n != "lo" && !n.is_empty() {
devs.insert(n);
}
}
}
}
devs
}
pub fn devs_sorted() -> Vec<String> {
let mut v: Vec<String> = get_dev().into_iter().collect();
v.sort();
v
}
pub fn has_dev(name: &str) -> bool {
get_dev().contains(name)
}
+49
View File
@@ -0,0 +1,49 @@
use crate::arpparse::NUDState;
use crate::dhcpparse::DhcpLeaseLine;
use crate::utils::parse::serialize_mac;
use serde_with::skip_serializing_none;
use std::net::IpAddr;
#[skip_serializing_none]
#[derive(Debug, Clone, serde::Serialize)]
pub struct DhcpLeaseOut {
pub expires_epoch: u64,
pub ip: IpAddr,
#[serde(serialize_with = "serialize_mac")]
pub mac: macaddr::MacAddr,
pub name: Option<String>,
pub nud_state: Option<NUDState>,
pub rank: Option<u8>,
}
/// Enrich DHCP leases with NUD state and rank using get_macs
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 mut map: std::collections::HashMap<IpAddr, (NUDState, u8)> =
std::collections::HashMap::new();
if let Ok(rows) = get_macs(None, Some(&ips), None, None).await {
for row in rows {
let state = row.state;
let r = state.rank();
map.entry(row.ip)
.and_modify(|e| {
if r > e.1 {
*e = (state, r)
}
})
.or_insert((state, r));
}
}
leases
.into_iter()
.map(|l| DhcpLeaseOut {
expires_epoch: l.expires_epoch,
ip: l.ip,
mac: l.mac,
name: l.name,
nud_state: map.get(&l.ip).map(|(s, _)| *s),
rank: map.get(&l.ip).map(|(_, r)| *r),
})
.collect()
}
+130
View File
@@ -0,0 +1,130 @@
use crate::arpparse::{self, IpNeighLine, NUDState};
use crate::utils::{
cmd::exec_command,
error::{self, Error, Result},
};
use macaddr::MacAddr;
use std::collections::HashSet;
use std::net::IpAddr;
pub async fn get_ips(machine_name: &str) -> error::Result<Vec<IpAddr>> {
let it = tokio::net::lookup_host((machine_name, 0))
.await
.map_err(|e| error::Error::DnsResolve {
name: machine_name.to_string(),
source: e,
})?;
Ok(it.map(|c| c.ip()).collect())
}
pub async fn _get_macs_2_1(machine_name: &str) -> Result<HashSet<(IpAddr, MacAddr, NUDState)>> {
Ok(get_macs_1(machine_name)
.await?
.into_iter()
.filter_map(
|IpNeighLine {
ip,
dev: _,
mac,
state,
}| mac.map(|mac| (ip, mac, state)),
)
.collect())
}
pub async fn get_macs_2_mac(machine_name: &str) -> Result<HashSet<MacAddr>> {
Ok(get_macs_1(machine_name)
.await?
.into_iter()
.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.iter().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(
machine_name: Option<&str>,
ips: Option<&[IpAddr]>,
dev: Option<&str>,
state: Option<NUDState>,
) -> Result<Vec<IpNeighLine>> {
let ip_list: Option<Vec<IpAddr>> =
ips.map(|slice| slice.iter().copied().map(|ip| ip.to_canonical()).collect());
let ip_list = match (ip_list, machine_name) {
(Some(list), _) => list,
(None, Some(name)) => get_ips(name).await?.into_iter().collect(),
(None, None) => Vec::new(),
};
let nud_arg = state.map(NUDState::as_ip_neigh_arg);
let run_one = |to_ip: Option<IpAddr>| async move {
let mut args: Vec<String> = vec!["neigh".into(), "show".into()];
if let Some(ip) = to_ip {
args.push("to".into());
args.push(ip.to_string());
}
if let Some(d) = dev {
args.push("dev".into());
args.push(d.to_string());
}
if let Some(nud) = nud_arg {
args.push("nud".into());
args.push(nud.to_string());
}
let o = exec_command("ip", args.iter().map(String::as_str).collect::<Vec<_>>()).await?;
if !o.status.success() {
return Err(Error::CommandFailed {
cmd: "ip",
args,
status: o.status.code(),
stderr: String::from_utf8_lossy(&o.stderr).into(),
});
}
let lines = String::from_utf8_lossy(&o.stdout);
let parsed = lines.lines().flat_map(arpparse::parse_ip_neigh_line);
let rows: Vec<IpNeighLine> = if let Some(d) = dev {
parsed.map(IpNeighLine::with_dev(d)).collect()
} else {
parsed.collect()
};
Ok::<Vec<IpNeighLine>, error::Error>(rows)
};
if !ip_list.is_empty() {
let futures = ip_list.into_iter().map(|ip| run_one(Some(ip)));
let res = futures::future::try_join_all(futures).await?;
Ok(res.into_iter().flatten().collect())
} else {
run_one(None).await
}
}
+75
View File
@@ -0,0 +1,75 @@
use std::net::IpAddr;
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 {
(
[
(header::CONTENT_TYPE, "application/javascript"),
(header::CACHE_CONTROL, "public, max-age=300"),
],
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()
}
}
+66
View File
@@ -1,5 +1,7 @@
pub mod impls;
use std::{io, net::IpAddr};
use macaddr::MacAddr;
use tokio::net::UdpSocket;
use crate::utils::query::get_macs_2_mac;
@@ -30,3 +32,67 @@ pub async fn wake(machine_name: &str) -> io::Result<u32> {
}
Ok(sent_ok)
}
#[derive(Debug, Clone, Copy, Hash)]
pub struct WakeTarget {
pub ip: IpAddr,
pub mac: MacAddr,
}
#[derive(Debug, Clone, Copy, Hash)]
pub struct WakeTargetResult {
pub ip: IpAddr,
pub mac: MacAddr,
pub status: WakeStatus,
}
#[derive(Debug, Clone, Copy, Hash)]
pub enum WakeStatus {
Success,
NonexistentAddress,
WrongSize,
}
impl WakeTarget {
fn _new(ip: IpAddr, mac: MacAddr) -> Self {
Self { ip, mac }
}
fn good(self) -> WakeTargetResult {
WakeTargetResult::new(self.ip, self.mac, WakeStatus::Success)
}
fn bad(self) -> WakeTargetResult {
WakeTargetResult::new(self.ip, self.mac, WakeStatus::WrongSize)
}
fn errored(self) -> WakeTargetResult {
WakeTargetResult::new(self.ip, self.mac, WakeStatus::NonexistentAddress)
}
}
impl WakeTargetResult {
fn new(ip: IpAddr, mac: MacAddr, status: WakeStatus) -> Self {
Self { ip, mac, status }
}
}
// its time. we have the ip; the macs. we dont need to send to the uh the broadcast anymore???
pub async fn _wake_multi(
targets: impl IntoIterator<Item = WakeTarget>,
) -> io::Result<Vec<WakeTargetResult>> {
let sock = UdpSocket::bind("0.0.0.0:0").await?;
sock.set_broadcast(true)?;
let fs = targets.into_iter().map(|t| wake_one(&sock, t));
Ok(futures::future::join_all(fs).await)
}
pub async fn wake_one(sock: &UdpSocket, t: WakeTarget) -> WakeTargetResult {
let mac = t.mac;
let mb = mac.as_bytes();
let mut pac = [0; 6 + 6 * 16];
pac[..6].fill(0xff);
for i in 1..=16 {
pac[i * 6..(i + 1) * 6].copy_from_slice(mb);
}
let ip = t.ip;
let port = 9;
match sock.send_to(&pac, (ip, port)).await {
Ok(n) if n == pac.len() => t.good(),
Ok(_) => t.bad(),
Err(_) => t.errored(),
}
}
+62
View File
@@ -0,0 +1,62 @@
use super::{WakeStatus, WakeTarget, WakeTargetResult};
use crate::route::wake::{
WakeTarget as RouteWakeTarget, WakeTargetResult as RouteWakeResult,
WakeTargetStatus as RouteWakeStatus,
};
#[derive(Debug, Clone, Copy)]
pub struct Incomplete;
impl TryFrom<RouteWakeTarget> for WakeTarget {
type Error = Incomplete;
fn try_from(value: RouteWakeTarget) -> Result<Self, Self::Error> {
if let RouteWakeTarget {
ip: Some(ip),
mac: Some(mac),
} = value
{
Ok(Self { ip, mac })
} else {
Err(Incomplete)
}
}
}
impl From<WakeTargetResult> for RouteWakeResult {
fn from(WakeTargetResult { ip, mac, status }: WakeTargetResult) -> Self {
Self {
ip: Some(ip),
mac: Some(mac),
status: status.into(),
}
}
}
impl RouteWakeTarget {
pub fn to_incomplete(self) -> RouteWakeResult {
RouteWakeResult {
ip: self.ip,
mac: self.mac,
status: RouteWakeStatus::Incomplete,
}
}
pub fn is_incomplete(&self) -> bool {
!matches!(
self,
Self {
ip: Some(_),
mac: Some(_)
}
)
}
}
impl From<WakeStatus> for RouteWakeStatus {
fn from(value: WakeStatus) -> Self {
match value {
WakeStatus::NonexistentAddress => Self::NonexistentAddress,
WakeStatus::Success => Self::Succeed,
WakeStatus::WrongSize => Self::WrongSize,
}
}
}