the Chat sweep
This commit is contained in:
Generated
+26
-3
@@ -1389,12 +1389,35 @@ dependencies = [
|
|||||||
"serde",
|
"serde",
|
||||||
"serde_html_form",
|
"serde_html_form",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"serde_with",
|
|
||||||
"strum",
|
|
||||||
"thiserror 2.0.18",
|
|
||||||
"tokio",
|
"tokio",
|
||||||
"tower-http",
|
"tower-http",
|
||||||
"urlencoding",
|
"urlencoding",
|
||||||
|
"wakey-core",
|
||||||
|
"wakey-linux",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "wakey-core"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"macaddr",
|
||||||
|
"serde",
|
||||||
|
"serde_with",
|
||||||
|
"strum",
|
||||||
|
"thiserror 2.0.18",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "wakey-linux"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"anyhow",
|
||||||
|
"futures",
|
||||||
|
"lda-ipjs",
|
||||||
|
"macaddr",
|
||||||
|
"serde_json",
|
||||||
|
"tokio",
|
||||||
|
"wakey-core",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|||||||
+3
-4
@@ -14,9 +14,6 @@ macaddr = { version = "1", features = ["serde", "serde_std"] }
|
|||||||
serde = { version = "1", features = ["derive"] }
|
serde = { version = "1", features = ["derive"] }
|
||||||
serde_html_form = "0"
|
serde_html_form = "0"
|
||||||
serde_json = "1"
|
serde_json = "1"
|
||||||
serde_with = { version = "3", features = ["json"] }
|
|
||||||
strum = { version = "0", features = ["derive", "strum_macros"] }
|
|
||||||
thiserror = "2"
|
|
||||||
tokio = { version = "1", features = [
|
tokio = { version = "1", features = [
|
||||||
"fs",
|
"fs",
|
||||||
"process",
|
"process",
|
||||||
@@ -26,6 +23,8 @@ tokio = { version = "1", features = [
|
|||||||
] }
|
] }
|
||||||
tower-http = { version = "0", features = ["fs"] }
|
tower-http = { version = "0", features = ["fs"] }
|
||||||
urlencoding = "2"
|
urlencoding = "2"
|
||||||
|
wakey-core = { path = "wakey-core" }
|
||||||
|
wakey-linux = { path = "wakey-linux" }
|
||||||
|
|
||||||
[profile.release]
|
[profile.release]
|
||||||
opt-level = "z"
|
opt-level = "z"
|
||||||
@@ -37,7 +36,7 @@ very-smart-parsing = [
|
|||||||
] # this is the a-bit-redundant parse thing that copilot made
|
] # this is the a-bit-redundant parse thing that copilot made
|
||||||
|
|
||||||
[workspace]
|
[workspace]
|
||||||
members = ["ipjs"]
|
members = ["ipjs", "wakey-core", "wakey-linux"]
|
||||||
|
|
||||||
[dependencies.lda-ipjs]
|
[dependencies.lda-ipjs]
|
||||||
path = "ipjs"
|
path = "ipjs"
|
||||||
|
|||||||
@@ -1,14 +0,0 @@
|
|||||||
use std::net::AddrParseError;
|
|
||||||
|
|
||||||
use strum::Display;
|
|
||||||
use thiserror::Error;
|
|
||||||
|
|
||||||
#[derive(Debug, Display, Error)]
|
|
||||||
pub enum IPNeighParseError {
|
|
||||||
IpWhere, // i never seen a ip neigh where the first thing aint an ip
|
|
||||||
IpParseError(#[from] AddrParseError),
|
|
||||||
// DevWhere,
|
|
||||||
MacParseError(#[from] macaddr::ParseError),
|
|
||||||
StateWhere, // i never seen a ip neigh without the big FAILED at the end
|
|
||||||
StateParseError(#[from] strum::ParseError),
|
|
||||||
}
|
|
||||||
@@ -1,75 +0,0 @@
|
|||||||
//! r#impl AHHHHHH
|
|
||||||
|
|
||||||
use serde::{Deserialize, Deserializer, de};
|
|
||||||
|
|
||||||
use super::NUDState;
|
|
||||||
|
|
||||||
// Case-insensitive parsing for NUDState via manual Deserialize
|
|
||||||
impl<'de> Deserialize<'de> for NUDState {
|
|
||||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
|
||||||
where
|
|
||||||
D: Deserializer<'de>,
|
|
||||||
{
|
|
||||||
let s: &str = <&str as Deserialize>::deserialize(deserializer)?;
|
|
||||||
s.parse().map_err(de::Error::custom)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
use crate::arpparse::IpNeighLine;
|
|
||||||
|
|
||||||
use lda_ipjs::subcommands::neighbor::{self as ipjs_neigh, NeighborItem};
|
|
||||||
|
|
||||||
impl From<ipjs_neigh::NUDState> for NUDState {
|
|
||||||
fn from(value: ipjs_neigh::NUDState) -> Self {
|
|
||||||
match value {
|
|
||||||
ipjs_neigh::NUDState::Permanent => NUDState::Permanent,
|
|
||||||
ipjs_neigh::NUDState::Noarp => NUDState::Noarp,
|
|
||||||
ipjs_neigh::NUDState::Reachable => NUDState::Reachable,
|
|
||||||
ipjs_neigh::NUDState::Stale => NUDState::Stale,
|
|
||||||
ipjs_neigh::NUDState::None => NUDState::None,
|
|
||||||
ipjs_neigh::NUDState::Incomplete => NUDState::Incomplete,
|
|
||||||
ipjs_neigh::NUDState::Delay => NUDState::Delay,
|
|
||||||
ipjs_neigh::NUDState::Probe => NUDState::Probe,
|
|
||||||
ipjs_neigh::NUDState::Failed => NUDState::Failed,
|
|
||||||
ipjs_neigh::NUDState::Other(_) => NUDState::None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<NUDState> for ipjs_neigh::NUDState {
|
|
||||||
fn from(value: NUDState) -> Self {
|
|
||||||
match value {
|
|
||||||
NUDState::Permanent => ipjs_neigh::NUDState::Permanent,
|
|
||||||
NUDState::Noarp => ipjs_neigh::NUDState::Noarp,
|
|
||||||
NUDState::Reachable => ipjs_neigh::NUDState::Reachable,
|
|
||||||
NUDState::Stale => ipjs_neigh::NUDState::Stale,
|
|
||||||
NUDState::None => ipjs_neigh::NUDState::None,
|
|
||||||
NUDState::Incomplete => ipjs_neigh::NUDState::Incomplete,
|
|
||||||
NUDState::Delay => ipjs_neigh::NUDState::Delay,
|
|
||||||
NUDState::Probe => ipjs_neigh::NUDState::Probe,
|
|
||||||
NUDState::Failed => ipjs_neigh::NUDState::Failed,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<NeighborItem> for IpNeighLine {
|
|
||||||
fn from(
|
|
||||||
NeighborItem {
|
|
||||||
ip,
|
|
||||||
dev,
|
|
||||||
mac,
|
|
||||||
state,
|
|
||||||
}: NeighborItem,
|
|
||||||
) -> Self {
|
|
||||||
IpNeighLine {
|
|
||||||
ip,
|
|
||||||
dev,
|
|
||||||
mac,
|
|
||||||
state: state
|
|
||||||
.into_iter()
|
|
||||||
.map(Into::into)
|
|
||||||
.max()
|
|
||||||
.unwrap_or(NUDState::None),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
+2
-250
@@ -1,250 +1,2 @@
|
|||||||
// struct arp;
|
pub use wakey_core::{NeighborEntry as IpNeighLine, NeighborParseError as IPNeighParseError};
|
||||||
|
pub use wakey_core::{NeighborState as NUDState, parse_neighbor_line};
|
||||||
// async fn read_arp() -> io::Result<()> {
|
|
||||||
// let arp_file = tokio::fs::File::open("/proc/net/arp").await?;
|
|
||||||
// let arp_read = BufReader::new(arp_file);
|
|
||||||
// Ok(())
|
|
||||||
// }
|
|
||||||
|
|
||||||
//! ip neigh pass
|
|
||||||
|
|
||||||
use std::{net::IpAddr, str::FromStr};
|
|
||||||
|
|
||||||
use crate::utils::parse::mac;
|
|
||||||
use macaddr::MacAddr;
|
|
||||||
use serde_with::skip_serializing_none;
|
|
||||||
use strum::{Display, EnumString};
|
|
||||||
|
|
||||||
use crate::arpparse::error::IPNeighParseError;
|
|
||||||
mod error;
|
|
||||||
mod impls; // custom (de)serialization impls
|
|
||||||
|
|
||||||
/// ip neigh has some cool shit.
|
|
||||||
///
|
|
||||||
/// IP
|
|
||||||
/// dev DEV | None
|
|
||||||
/// lladdr MAC | None
|
|
||||||
/// status { permanent | noarp | stale | reachable | none | incomplete | delay | probe | failed } (ip neigh help)
|
|
||||||
///
|
|
||||||
/// so you can see its damn good
|
|
||||||
#[skip_serializing_none]
|
|
||||||
#[derive(Debug, PartialEq, Eq, Clone, Hash, serde::Serialize)]
|
|
||||||
pub struct IpNeighLine {
|
|
||||||
pub ip: IpAddr,
|
|
||||||
pub dev: Option<String>,
|
|
||||||
/// link layer address
|
|
||||||
#[serde(with = "mac::option_mac")]
|
|
||||||
pub mac: Option<MacAddr>,
|
|
||||||
/// Neighbour Unreachability Detection
|
|
||||||
pub state: NUDState,
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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, Default,
|
|
||||||
)]
|
|
||||||
#[strum(serialize_all = "UPPERCASE", 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,
|
|
||||||
|
|
||||||
/// 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,
|
|
||||||
|
|
||||||
/// 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 {
|
|
||||||
/// Argument form expected by `ip neigh ... nud <state>` (lowercase)
|
|
||||||
pub const fn as_ip_neigh_arg(self) -> &'static str {
|
|
||||||
match self {
|
|
||||||
NUDState::Permanent => "permanent",
|
|
||||||
NUDState::Reachable => "reachable",
|
|
||||||
NUDState::Stale => "stale",
|
|
||||||
NUDState::Delay => "delay",
|
|
||||||
NUDState::Probe => "probe",
|
|
||||||
NUDState::Incomplete => "incomplete",
|
|
||||||
NUDState::Noarp => "noarp",
|
|
||||||
NUDState::None => "none",
|
|
||||||
NUDState::Failed => "failed",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
/// dumb UI label
|
|
||||||
pub fn _dumber_state(&self) -> &'static str {
|
|
||||||
match self {
|
|
||||||
NUDState::Permanent | NUDState::Reachable => "online",
|
|
||||||
NUDState::Stale => "maybe online",
|
|
||||||
NUDState::Delay | NUDState::Probe | NUDState::Incomplete => "resolving",
|
|
||||||
NUDState::Noarp => "static",
|
|
||||||
NUDState::None => "unknown",
|
|
||||||
NUDState::Failed => "offline",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
/// dumb boolean: Some(true)=on, Some(false)=off, None=shrug
|
|
||||||
pub fn _dumber_state_this_way(&self) -> Option<bool> {
|
|
||||||
match self {
|
|
||||||
NUDState::Permanent | NUDState::Reachable => Some(true),
|
|
||||||
NUDState::Failed => Some(false),
|
|
||||||
_ => None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// thanks copilot for the PEAK
|
|
||||||
pub fn parse_ip_neigh_line(s: &str) -> Result<IpNeighLine, IPNeighParseError> {
|
|
||||||
let mut it = s.split_whitespace();
|
|
||||||
let ip: IpAddr = it.next().ok_or(IPNeighParseError::IpWhere)?.parse()?;
|
|
||||||
|
|
||||||
let mut dev: Option<String> = None;
|
|
||||||
let mut mac: Option<MacAddr> = None;
|
|
||||||
let mut state: Option<NUDState> = None;
|
|
||||||
let mut last_tok: Option<&str> = None;
|
|
||||||
|
|
||||||
while let Some(tok) = it.next() {
|
|
||||||
match tok {
|
|
||||||
"dev" => dev = it.next().map(str::to_string),
|
|
||||||
"lladdr" => {
|
|
||||||
mac = it.next().map(|m| m.parse()).transpose()?;
|
|
||||||
}
|
|
||||||
"nud" => {
|
|
||||||
// we know this aint happening
|
|
||||||
state = it.next().map(|st| st.parse()).transpose()?;
|
|
||||||
}
|
|
||||||
other => last_tok = Some(other),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// If no explicit "nud", many outputs end with STATE
|
|
||||||
if state.is_none()
|
|
||||||
&& let Some(st) = last_tok
|
|
||||||
{
|
|
||||||
state = Some(st.parse()?);
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(IpNeighLine {
|
|
||||||
ip,
|
|
||||||
dev,
|
|
||||||
mac,
|
|
||||||
state: state.ok_or(IPNeighParseError::StateWhere)?,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
impl FromStr for IpNeighLine {
|
|
||||||
type Err = IPNeighParseError;
|
|
||||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
|
||||||
parse_ip_neigh_line(s)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
/// pls dont touch ts
|
|
||||||
impl IpNeighLine {
|
|
||||||
/* // these are some not needed fns
|
|
||||||
pub fn set_ip(&mut self, ip: IpAddr) {
|
|
||||||
self.ip = ip;
|
|
||||||
}
|
|
||||||
pub fn set_state(&mut self, state: NUDState) {
|
|
||||||
self.state = state;
|
|
||||||
}
|
|
||||||
pub fn ip(self, ip: IpAddr) -> Self {
|
|
||||||
Self { ip, ..self }
|
|
||||||
}
|
|
||||||
pub fn state(self, state: NUDState) -> Self {
|
|
||||||
Self { state, ..self }
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
pub fn _with_dev(dev: impl Into<String>) -> impl FnMut(Self) -> Self {
|
|
||||||
let dev = dev.into();
|
|
||||||
move |self_| Self {
|
|
||||||
dev: Some(dev.clone()),
|
|
||||||
..self_
|
|
||||||
}
|
|
||||||
}
|
|
||||||
pub fn _with_mac(mac: MacAddr) -> impl FnMut(Self) -> Self {
|
|
||||||
move |self_| Self {
|
|
||||||
mac: Some(mac),
|
|
||||||
..self_
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ideas from copilot:
|
|
||||||
|
|
||||||
impl NUDState {
|
|
||||||
// higher is "better"/more online
|
|
||||||
pub const fn rank(self) -> u8 {
|
|
||||||
match self {
|
|
||||||
NUDState::Permanent | NUDState::Reachable => 5,
|
|
||||||
NUDState::Stale => 4,
|
|
||||||
NUDState::Delay | NUDState::Probe | NUDState::Incomplete => 3,
|
|
||||||
NUDState::Noarp => 2,
|
|
||||||
NUDState::None => 1,
|
|
||||||
NUDState::Failed => 0,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
impl PartialOrd for NUDState {
|
|
||||||
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
|
|
||||||
Some(self.cmp(other))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
impl Ord for NUDState {
|
|
||||||
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
|
|
||||||
self.rank().cmp(&other.rank())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl IpNeighLine {
|
|
||||||
// score for “local and online”: state, has-mac, v4, iface preference
|
|
||||||
pub fn _score(&self) -> (u8, u8, u8, u8) {
|
|
||||||
let iface = self
|
|
||||||
.dev
|
|
||||||
.as_deref()
|
|
||||||
.map(|d| {
|
|
||||||
if d.starts_with("br") || d.starts_with("lan") || d.starts_with("eth") {
|
|
||||||
2
|
|
||||||
} else if d.starts_with("wlan") || d.starts_with("wl") {
|
|
||||||
1
|
|
||||||
} else {
|
|
||||||
0
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.unwrap_or(0);
|
|
||||||
(
|
|
||||||
self.state.rank(),
|
|
||||||
self.mac.is_some() as u8,
|
|
||||||
matches!(self.ip, IpAddr::V4(_)) as u8,
|
|
||||||
iface,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
+4
-84
@@ -1,84 +1,4 @@
|
|||||||
/// MAC->name cache location (ephemeral)
|
pub use wakey_core::DhcpLease as DhcpLeaseLine;
|
||||||
const MAC_NAME_CACHE: &str = "/tmp/wakey_mac_names.json";
|
pub use wakey_linux::dhcp::{
|
||||||
|
load_mac_name_cache, parse_dhcp_lease_line, read_dhcp_leases, read_dhcp_leases_with_names,
|
||||||
/// Load MAC->name cache from disk
|
};
|
||||||
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 {
|
|
||||||
Ok(s) => serde_json::from_str(&s).map_err(io::Error::other),
|
|
||||||
Err(e) if e.kind() == ErrorKind::NotFound => Ok(Default::default()),
|
|
||||||
Err(e) => Err(e),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Save MAC->name cache to disk
|
|
||||||
async fn save_mac_name_cache(map: &std::collections::BTreeMap<String, String>) -> io::Result<()> {
|
|
||||||
let s = serde_json::to_string(map).map_err(io::Error::other)?;
|
|
||||||
let _ = tokio::fs::write(MAC_NAME_CACHE, s).await;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Read all leases, filling names from MAC->name cache if missing
|
|
||||||
pub async fn read_dhcp_leases_with_names() -> io::Result<Vec<DhcpLeaseLine>> {
|
|
||||||
let leases = read_dhcp_leases().await?;
|
|
||||||
let mut cache = load_mac_name_cache().await.unwrap_or_default();
|
|
||||||
let mut changed = false;
|
|
||||||
let mut leases_with_names = Vec::with_capacity(leases.len());
|
|
||||||
for mut l in leases {
|
|
||||||
let mac_s = l.mac.to_string();
|
|
||||||
if let Some(ref name) = l.name {
|
|
||||||
// if no name in cache file or it changed
|
|
||||||
if cache.get(&mac_s).map(|v| v != name).unwrap_or(true) {
|
|
||||||
cache.insert(mac_s, name.clone());
|
|
||||||
changed = true;
|
|
||||||
}
|
|
||||||
} else if let Some(prev) = cache.get(&mac_s) {
|
|
||||||
l.name = Some(prev.clone());
|
|
||||||
}
|
|
||||||
leases_with_names.push(l);
|
|
||||||
}
|
|
||||||
if changed {
|
|
||||||
let _ = save_mac_name_cache(&cache).await;
|
|
||||||
}
|
|
||||||
Ok(leases_with_names)
|
|
||||||
}
|
|
||||||
use crate::utils::parse::mac;
|
|
||||||
use macaddr::MacAddr;
|
|
||||||
use std::io::{self, ErrorKind};
|
|
||||||
use std::net::IpAddr;
|
|
||||||
|
|
||||||
/// A single line from /tmp/dhcp.leases
|
|
||||||
#[derive(Debug, Clone, serde::Serialize)]
|
|
||||||
pub struct DhcpLeaseLine {
|
|
||||||
/// Epoch seconds when the lease expires
|
|
||||||
pub expires_epoch: u64,
|
|
||||||
pub ip: IpAddr,
|
|
||||||
#[serde(with = "mac")]
|
|
||||||
pub mac: MacAddr,
|
|
||||||
pub name: Option<String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Parse one line of /tmp/dhcp.leases
|
|
||||||
pub fn parse_dhcp_lease_line(line: &str) -> Option<DhcpLeaseLine> {
|
|
||||||
let mut c = line.split_whitespace();
|
|
||||||
let expires_epoch: u64 = c.next()?.parse().ok()?;
|
|
||||||
let mac = c.next()?.parse().ok()?;
|
|
||||||
let ip = c.next()?.parse().ok()?;
|
|
||||||
let name = c.next().filter(|c| *c != "*").map(str::to_string);
|
|
||||||
// ignore any remaining columns (e.g., client-id)
|
|
||||||
Some(DhcpLeaseLine {
|
|
||||||
expires_epoch,
|
|
||||||
ip,
|
|
||||||
mac,
|
|
||||||
name,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Read all leases from /tmp/dhcp.leases (simple and fast)
|
|
||||||
pub async fn read_dhcp_leases() -> io::Result<Vec<DhcpLeaseLine>> {
|
|
||||||
match tokio::fs::read_to_string("/tmp/dhcp.leases").await {
|
|
||||||
Ok(file) => Ok(file.lines().filter_map(parse_dhcp_lease_line).collect()),
|
|
||||||
Err(e) if e.kind() == ErrorKind::NotFound => Ok(Vec::new()),
|
|
||||||
Err(e) => Err(e),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
+3
-2
@@ -1,11 +1,12 @@
|
|||||||
use crate::route::error::ApiError;
|
use crate::route::error::ApiError;
|
||||||
use crate::utils::query::parser::{QueryType, parse_query};
|
use crate::utils::query::parser::{QueryType, parse_query};
|
||||||
|
use wakey_core::{DeviceFilters as Filters, DeviceQuery};
|
||||||
use axum::Json;
|
use axum::Json;
|
||||||
use axum::http::StatusCode;
|
use axum::http::StatusCode;
|
||||||
use axum::response::IntoResponse;
|
use axum::response::IntoResponse;
|
||||||
use axum::{extract::Path, response::Redirect};
|
use axum::{extract::Path, response::Redirect};
|
||||||
|
|
||||||
use crate::route::status::{DeviceQuery, Filters, NamePath};
|
use crate::route::status::NamePath;
|
||||||
use crate::utils::query::get_ips;
|
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
|
||||||
@@ -42,7 +43,7 @@ pub async fn status_smart_redirect(
|
|||||||
},
|
},
|
||||||
..Default::default()
|
..Default::default()
|
||||||
},
|
},
|
||||||
QueryType::Unknown(n) => DeviceQuery {
|
QueryType::Name(n) => DeviceQuery {
|
||||||
name: Some(n),
|
name: Some(n),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
},
|
},
|
||||||
|
|||||||
+1
-44
@@ -1,51 +1,8 @@
|
|||||||
use crate::route::error::ApiError;
|
use crate::route::error::ApiError;
|
||||||
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 serde::{Deserialize, Serialize};
|
|
||||||
use serde_with::skip_serializing_none;
|
|
||||||
use serde_with::{DisplayFromStr, OneOrMany, serde_as};
|
|
||||||
use std::net::IpAddr;
|
|
||||||
|
|
||||||
use crate::arpparse::NUDState;
|
|
||||||
use crate::utils::query::get_macs;
|
use crate::utils::query::get_macs;
|
||||||
|
pub use wakey_core::{DeviceFilters as Filters, DeviceQuery, NamePath, Status};
|
||||||
#[derive(Debug, Default, Clone, Hash, Deserialize, Serialize)]
|
|
||||||
pub struct DeviceQuery {
|
|
||||||
pub name: Option<String>,
|
|
||||||
#[serde(flatten)]
|
|
||||||
pub filter: Filters,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Default, Clone, Hash, Deserialize)]
|
|
||||||
pub struct NamePath {
|
|
||||||
pub name: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[skip_serializing_none]
|
|
||||||
#[derive(Debug, Default, Serialize)]
|
|
||||||
pub struct Status<T> {
|
|
||||||
pub name: Option<String>,
|
|
||||||
pub table: Vec<T>,
|
|
||||||
pub filters: Filters,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[serde_as]
|
|
||||||
#[derive(Debug, Default, Clone, Hash, Serialize, Deserialize)]
|
|
||||||
pub struct Filters {
|
|
||||||
#[serde_as(as = "OneOrMany<_>")]
|
|
||||||
#[serde(default)]
|
|
||||||
pub ips: Vec<IpAddr>,
|
|
||||||
#[serde_as(as = "OneOrMany<_>")]
|
|
||||||
#[serde(default)]
|
|
||||||
pub devs: Vec<String>,
|
|
||||||
#[serde_as(as = "OneOrMany<_>")]
|
|
||||||
#[serde(default)]
|
|
||||||
pub nuds: Vec<NUDState>,
|
|
||||||
#[serde_as(as = "OneOrMany<DisplayFromStr>")]
|
|
||||||
#[serde(default)]
|
|
||||||
pub macs: Vec<MacAddr>,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn get_status_json(
|
pub async fn get_status_json(
|
||||||
Query(DeviceQuery {
|
Query(DeviceQuery {
|
||||||
|
|||||||
@@ -1,52 +0,0 @@
|
|||||||
use crate::utils::wake::{WakeStatus, WakeTarget, WakeTargetResult};
|
|
||||||
use crate::route::wake::{
|
|
||||||
WakeTarget as RouteWakeTarget, WakeTargetResult as RouteWakeResult,
|
|
||||||
WakeTargetStatus as RouteWakeStatus,
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
impl From<WakeTarget> for RouteWakeTarget {
|
|
||||||
fn from(WakeTarget { ip, mac }: WakeTarget) -> Self {
|
|
||||||
Self {
|
|
||||||
ip: Some(ip),
|
|
||||||
mac: Some(mac),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<WakeTargetResult> for RouteWakeResult {
|
|
||||||
fn from(WakeTargetResult { target, status }: WakeTargetResult) -> Self {
|
|
||||||
Self {
|
|
||||||
target: target.into(),
|
|
||||||
status: status.into(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl RouteWakeTarget {
|
|
||||||
pub const fn to_incomplete(self) -> RouteWakeResult {
|
|
||||||
RouteWakeResult {
|
|
||||||
target: self,
|
|
||||||
status: RouteWakeStatus::Incomplete,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
pub const 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,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
+4
-40
@@ -1,49 +1,14 @@
|
|||||||
//! impls are at [`utils::wake::impl`](crate::utils::wake::r#impl) for some reason
|
//! impls are at [`utils::wake::impl`](crate::utils::wake::r#impl) for some reason
|
||||||
use std::io;
|
use std::io;
|
||||||
use std::net::IpAddr;
|
|
||||||
|
|
||||||
/* use crate::arpparse::IpNeighLine;
|
/* use crate::arpparse::IpNeighLine;
|
||||||
use crate::route::api::Status; */
|
use crate::route::api::Status; */
|
||||||
use crate::utils::wake::wake_one;
|
use crate::utils::wake::wake_one;
|
||||||
use crate::{route::error::ApiError, utils::parse::mac};
|
use crate::route::error::ApiError;
|
||||||
use axum::{extract::Json, http::StatusCode, response::IntoResponse};
|
use axum::{extract::Json, http::StatusCode, response::IntoResponse};
|
||||||
use futures::TryFutureExt;
|
use futures::TryFutureExt;
|
||||||
use macaddr::MacAddr;
|
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
use serde_with::skip_serializing_none;
|
|
||||||
use tokio::net::UdpSocket;
|
use tokio::net::UdpSocket;
|
||||||
|
pub use wakey_core::{WakeResult, WakeStatus as WakeTargetStatus, WakeTarget, WakeTargetResult};
|
||||||
#[derive(Debug, Default, Serialize, Clone)]
|
|
||||||
pub struct WakeResult {
|
|
||||||
pub result: Vec<WakeTargetResult>,
|
|
||||||
}
|
|
||||||
#[skip_serializing_none]
|
|
||||||
#[derive(Debug, Serialize, Clone, Copy)]
|
|
||||||
pub struct WakeTargetResult {
|
|
||||||
#[serde(flatten)]
|
|
||||||
pub target: WakeTarget,
|
|
||||||
pub status: WakeTargetStatus,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Clone, Copy, Hash)]
|
|
||||||
#[serde(rename_all = "snake_case")]
|
|
||||||
pub enum WakeTargetStatus {
|
|
||||||
Succeed,
|
|
||||||
/// not a real address...
|
|
||||||
NonexistentAddress,
|
|
||||||
WrongSize,
|
|
||||||
/// input is not enough
|
|
||||||
Incomplete,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[skip_serializing_none]
|
|
||||||
#[derive(Debug, Serialize, Deserialize, Clone, Copy)]
|
|
||||||
pub struct WakeTarget {
|
|
||||||
#[serde(default)]
|
|
||||||
pub ip: Option<IpAddr>,
|
|
||||||
#[serde(default, with = "mac::option_mac")]
|
|
||||||
pub mac: Option<MacAddr>,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn wake_multi(Json(req): Json<Vec<WakeTarget>>) -> impl IntoResponse {
|
pub async fn wake_multi(Json(req): Json<Vec<WakeTarget>>) -> impl IntoResponse {
|
||||||
match wake_multi_split(req).await {
|
match wake_multi_split(req).await {
|
||||||
@@ -65,8 +30,8 @@ pub async fn wake_multi_split(
|
|||||||
sock.set_broadcast(true)?;
|
sock.set_broadcast(true)?;
|
||||||
|
|
||||||
let iter = targets.into_iter().map(async |c| {
|
let iter = targets.into_iter().map(async |c| {
|
||||||
if c.is_incomplete() {
|
if !c.is_complete() {
|
||||||
c.to_incomplete()
|
WakeTargetResult::incomplete(c)
|
||||||
} else {
|
} else {
|
||||||
let t = c.try_into().expect("complete struct failed to try_into");
|
let t = c.try_into().expect("complete struct failed to try_into");
|
||||||
wake_one(&sock, t).await.into()
|
wake_one(&sock, t).await.into()
|
||||||
@@ -93,4 +58,3 @@ pub type WakeStatus = Status<WakeStatusLine>; */
|
|||||||
// }): Query<DeviceQuery>,
|
// }): Query<DeviceQuery>,
|
||||||
// ) -> impl IntoResponse {
|
// ) -> impl IntoResponse {
|
||||||
// }
|
// }
|
||||||
pub mod impls;
|
|
||||||
|
|||||||
+1
-65
@@ -1,65 +1 @@
|
|||||||
use std::collections::HashSet;
|
pub use wakey_linux::devices::{devs_sorted, has_dev};
|
||||||
// /// 50ms
|
|
||||||
// pub async fn get_dev() -> HashSet<String> {
|
|
||||||
// use lda_ipjs::subcommands::address::json as ipjs_json;
|
|
||||||
// let mut devs: HashSet<String> = HashSet::new();
|
|
||||||
// if let Ok(items) = ipjs_json::get(None).await {
|
|
||||||
// for item in items {
|
|
||||||
// if item.ifname != "lo" && !item.ifname.is_empty() {
|
|
||||||
// devs.insert(item.ifname);
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// devs
|
|
||||||
// }
|
|
||||||
|
|
||||||
/// 3ms
|
|
||||||
pub async fn get_dev() -> HashSet<String> {
|
|
||||||
use std::fs;
|
|
||||||
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 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.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
|
|
||||||
}
|
|
||||||
tokio::task::spawn_blocking(get_dev)
|
|
||||||
.await
|
|
||||||
.unwrap_or_default()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn devs_sorted() -> Vec<String> {
|
|
||||||
let mut v: Vec<String> = get_dev().await.into_iter().collect();
|
|
||||||
v.sort();
|
|
||||||
v
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn has_dev(name: &str) -> bool {
|
|
||||||
get_dev().await.contains(name)
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,43 +1,2 @@
|
|||||||
use crate::arpparse::NUDState;
|
pub use wakey_core::DhcpLeaseWithState as DhcpLeaseOut;
|
||||||
use crate::dhcpparse::DhcpLeaseLine;
|
pub use wakey_linux::dhcp::enrich_leases_with_nud_state;
|
||||||
use crate::utils::query::get_macs;
|
|
||||||
use serde_with::skip_serializing_none;
|
|
||||||
use std::net::IpAddr;
|
|
||||||
|
|
||||||
#[skip_serializing_none]
|
|
||||||
#[derive(Debug, Clone, serde::Serialize)]
|
|
||||||
pub struct DhcpLeaseOut {
|
|
||||||
#[serde(flatten)]
|
|
||||||
pub lease_line: DhcpLeaseLine,
|
|
||||||
pub nud_state: Option<NUDState>,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Enrich DHCP leases with NUD state and rank using get_macs
|
|
||||||
pub async fn enrich_leases_with_nud_state(leases: Vec<DhcpLeaseLine>) -> Vec<DhcpLeaseOut> {
|
|
||||||
let ips: Vec<IpAddr> = leases.iter().map(|l| l.ip).collect();
|
|
||||||
let mut map: std::collections::HashMap<IpAddr, NUDState> = std::collections::HashMap::new();
|
|
||||||
if let Ok(rows) = get_macs(&[] as &[&str], &ips, &[] as &[&str], &[], &[]).await {
|
|
||||||
for row in rows {
|
|
||||||
let state = row.state;
|
|
||||||
let r = state.rank();
|
|
||||||
map.entry(row.ip)
|
|
||||||
.and_modify(|e| {
|
|
||||||
let er = e.rank();
|
|
||||||
if r > er {
|
|
||||||
*e = state
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.or_insert(state);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
leases
|
|
||||||
.into_iter()
|
|
||||||
.map(|lease_line| {
|
|
||||||
let nud_state = map.get(&lease_line.ip).copied();
|
|
||||||
DhcpLeaseOut {
|
|
||||||
lease_line,
|
|
||||||
nud_state,
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.collect()
|
|
||||||
}
|
|
||||||
|
|||||||
+7
-63
@@ -1,71 +1,22 @@
|
|||||||
use lda_ipjs::subcommands::neighbor;
|
use anyhow::Result;
|
||||||
use macaddr::MacAddr;
|
|
||||||
|
|
||||||
use crate::arpparse::{IpNeighLine, NUDState};
|
|
||||||
use anyhow::{Context, Result};
|
|
||||||
use std::collections::HashSet;
|
|
||||||
use std::net::IpAddr;
|
use std::net::IpAddr;
|
||||||
|
|
||||||
|
use crate::arpparse::{IpNeighLine, NUDState};
|
||||||
|
|
||||||
pub async fn get_ips(machine_name: &str) -> 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))
|
wakey_linux::devices::get_ips(machine_name).await
|
||||||
.await
|
|
||||||
.with_context(|| format!("DNS resolve failed for {machine_name}"))?
|
|
||||||
.map(|c| c.ip()))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Query neighbor table with multi-filters. Empty slice = no filter.
|
|
||||||
pub async fn get_macs(
|
pub async fn get_macs(
|
||||||
machine_names: &[impl AsRef<str>],
|
machine_names: &[impl AsRef<str>],
|
||||||
ips: &[IpAddr],
|
ips: &[IpAddr],
|
||||||
devs: &[impl AsRef<str>],
|
devs: &[impl AsRef<str>],
|
||||||
state: &[NUDState],
|
state: &[NUDState],
|
||||||
macs: &[MacAddr],
|
macs: &[macaddr::MacAddr],
|
||||||
) -> Result<Vec<IpNeighLine>> {
|
) -> Result<Vec<IpNeighLine>> {
|
||||||
// Resolve machine names to IPs
|
wakey_linux::devices::get_neighbors(machine_names, ips, devs, state, macs).await
|
||||||
let resolved_ips: HashSet<IpAddr> = if !machine_names.is_empty() {
|
|
||||||
futures::future::try_join_all(machine_names.iter().map(|n| get_ips(n.as_ref())))
|
|
||||||
.await?
|
|
||||||
.into_iter()
|
|
||||||
.flatten()
|
|
||||||
.collect()
|
|
||||||
} else {
|
|
||||||
HashSet::new()
|
|
||||||
};
|
|
||||||
|
|
||||||
// Merge provided IPs with resolved IPs
|
|
||||||
let ip_filter: Vec<IpAddr> = if ips.is_empty() && resolved_ips.is_empty() {
|
|
||||||
vec![]
|
|
||||||
} else if ips.is_empty() {
|
|
||||||
resolved_ips.into_iter().collect()
|
|
||||||
} else if resolved_ips.is_empty() {
|
|
||||||
ips.iter().map(|ip| ip.to_canonical()).collect()
|
|
||||||
} else {
|
|
||||||
// Intersection: only IPs that appear in both
|
|
||||||
ips.iter()
|
|
||||||
.map(|ip| ip.to_canonical())
|
|
||||||
.filter(|ip| resolved_ips.contains(ip))
|
|
||||||
.collect()
|
|
||||||
};
|
|
||||||
|
|
||||||
// Convert state filter
|
|
||||||
let nud_filter: Vec<neighbor::NUDState> = state.iter().copied().map(Into::into).collect();
|
|
||||||
|
|
||||||
// Convert devs to &str for nl::get
|
|
||||||
let dev_strs: Vec<&str> = devs.iter().map(AsRef::as_ref).collect();
|
|
||||||
|
|
||||||
// Single rtnetlink call with all filters
|
|
||||||
let results: Vec<IpNeighLine> = neighbor::nl::get(&ip_filter, &dev_strs, &nud_filter, macs)
|
|
||||||
.await
|
|
||||||
.context("rtnetlink failed")?
|
|
||||||
.into_iter()
|
|
||||||
.map(Into::into)
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
Ok(results)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Legacy single-filter wrapper. Use get_macs for multi-filter.
|
|
||||||
#[allow(dead_code)]
|
|
||||||
pub async fn get_mac(
|
pub async fn get_mac(
|
||||||
ip: Option<IpAddr>,
|
ip: Option<IpAddr>,
|
||||||
dev: Option<&str>,
|
dev: Option<&str>,
|
||||||
@@ -73,12 +24,5 @@ pub async fn get_mac(
|
|||||||
) -> Result<Vec<IpNeighLine>> {
|
) -> Result<Vec<IpNeighLine>> {
|
||||||
let ips: Vec<IpAddr> = ip.into_iter().collect();
|
let ips: Vec<IpAddr> = ip.into_iter().collect();
|
||||||
let devs: Vec<&str> = dev.into_iter().collect();
|
let devs: Vec<&str> = dev.into_iter().collect();
|
||||||
let nud: Vec<neighbor::NUDState> = state.iter().copied().map(Into::into).collect();
|
get_macs(&[] as &[&str], &ips, &devs, state, &[]).await
|
||||||
|
|
||||||
Ok(neighbor::nl::get(&ips, &devs, &nud, &[])
|
|
||||||
.await
|
|
||||||
.context("rtnetlink failed")?
|
|
||||||
.into_iter()
|
|
||||||
.map(Into::into)
|
|
||||||
.collect())
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,44 +1,5 @@
|
|||||||
use std::net::IpAddr;
|
pub use wakey_core::QueryInput as QueryType;
|
||||||
|
|
||||||
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 async fn parse_query(q: String) -> QueryType {
|
pub async fn parse_query(q: String) -> QueryType {
|
||||||
let s = if cfg!(feature = "very-smart-parsing") {
|
wakey_linux::devices::classify_query(q).await
|
||||||
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).await {
|
|
||||||
return QueryType::Dev(s.to_string());
|
|
||||||
}
|
|
||||||
// Default: name last // it will fail also
|
|
||||||
QueryType::Unknown(s.to_string())
|
|
||||||
}
|
}
|
||||||
|
|||||||
+7
-93
@@ -1,95 +1,9 @@
|
|||||||
//! why did my Head Ass split these into two.
|
pub async fn wake_one(
|
||||||
|
sock: &tokio::net::UdpSocket,
|
||||||
use std::{io, net::IpAddr};
|
t: wakey_linux::wake::CompleteWakeTarget,
|
||||||
|
) -> wakey_core::WakeTargetResult {
|
||||||
use futures::TryFutureExt;
|
wakey_linux::wake::wake_one(sock, t).await
|
||||||
use macaddr::MacAddr;
|
|
||||||
use tokio::net::UdpSocket;
|
|
||||||
|
|
||||||
use crate::route::wake::WakeTarget as RouteWakeTarget;
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, Hash)]
|
|
||||||
pub struct WakeTarget {
|
|
||||||
pub ip: IpAddr,
|
|
||||||
pub mac: MacAddr,
|
|
||||||
}
|
|
||||||
#[derive(Debug, Clone, Copy, Hash)]
|
|
||||||
pub struct WakeTargetResult {
|
|
||||||
pub target: WakeTarget,
|
|
||||||
pub status: WakeStatus,
|
|
||||||
}
|
|
||||||
#[derive(Debug, Clone, Copy, Hash)]
|
|
||||||
pub enum WakeStatus {
|
|
||||||
Success,
|
|
||||||
NonexistentAddress,
|
|
||||||
WrongSize,
|
|
||||||
}
|
|
||||||
impl WakeTarget {
|
|
||||||
const fn _new(ip: IpAddr, mac: MacAddr) -> Self {
|
|
||||||
Self { ip, mac }
|
|
||||||
}
|
|
||||||
const fn good(self) -> WakeTargetResult {
|
|
||||||
WakeTargetResult::new(self, WakeStatus::Success)
|
|
||||||
}
|
|
||||||
const fn bad(self) -> WakeTargetResult {
|
|
||||||
WakeTargetResult::new(self, WakeStatus::WrongSize)
|
|
||||||
}
|
|
||||||
const fn errored(self) -> WakeTargetResult {
|
|
||||||
WakeTargetResult::new(self, WakeStatus::NonexistentAddress)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy)]
|
pub use wakey_linux::wake::{CompleteWakeTarget as WakeTarget, wake_many as _wake_multi};
|
||||||
pub struct Incomplete;
|
pub use wakey_core::{WakeStatus, WakeTargetResult};
|
||||||
|
|
||||||
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 WakeTargetResult {
|
|
||||||
const fn new(target: WakeTarget, status: WakeStatus) -> Self {
|
|
||||||
Self { target, 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")
|
|
||||||
.or_else(|_| UdpSocket::bind(":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(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// pub async fn wake_query();
|
|
||||||
|
|||||||
@@ -0,0 +1,11 @@
|
|||||||
|
[package]
|
||||||
|
name = "wakey-core"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2024"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
macaddr = { version = "1", features = ["serde", "serde_std"] }
|
||||||
|
serde = { version = "1", features = ["derive"] }
|
||||||
|
serde_with = { version = "3", features = ["json"] }
|
||||||
|
strum = { version = "0", features = ["derive", "strum_macros"] }
|
||||||
|
thiserror = "2"
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
pub mod model;
|
||||||
|
pub mod parse;
|
||||||
|
|
||||||
|
pub use model::*;
|
||||||
@@ -0,0 +1,242 @@
|
|||||||
|
use macaddr::MacAddr;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use serde_with::skip_serializing_none;
|
||||||
|
use serde_with::{DisplayFromStr, OneOrMany, serde_as};
|
||||||
|
use std::{net::IpAddr, str::FromStr};
|
||||||
|
use strum::{Display, EnumString};
|
||||||
|
|
||||||
|
use crate::parse::mac;
|
||||||
|
|
||||||
|
#[skip_serializing_none]
|
||||||
|
#[derive(Debug, PartialEq, Eq, Clone, Hash, Serialize)]
|
||||||
|
pub struct NeighborEntry {
|
||||||
|
pub ip: IpAddr,
|
||||||
|
pub dev: Option<String>,
|
||||||
|
#[serde(with = "mac::option_mac")]
|
||||||
|
pub mac: Option<MacAddr>,
|
||||||
|
pub state: NeighborState,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(
|
||||||
|
Debug, PartialEq, Eq, EnumString, Display, Clone, Copy, Hash, Serialize, Deserialize, Default,
|
||||||
|
)]
|
||||||
|
#[strum(serialize_all = "UPPERCASE", ascii_case_insensitive)]
|
||||||
|
#[serde(rename_all = "UPPERCASE")]
|
||||||
|
pub enum NeighborState {
|
||||||
|
Permanent,
|
||||||
|
Noarp,
|
||||||
|
Reachable,
|
||||||
|
Stale,
|
||||||
|
Incomplete,
|
||||||
|
Delay,
|
||||||
|
Probe,
|
||||||
|
Failed,
|
||||||
|
#[serde(other)]
|
||||||
|
#[default]
|
||||||
|
None,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl NeighborState {
|
||||||
|
pub const fn as_ip_neigh_arg(self) -> &'static str {
|
||||||
|
match self {
|
||||||
|
NeighborState::Permanent => "permanent",
|
||||||
|
NeighborState::Reachable => "reachable",
|
||||||
|
NeighborState::Stale => "stale",
|
||||||
|
NeighborState::Delay => "delay",
|
||||||
|
NeighborState::Probe => "probe",
|
||||||
|
NeighborState::Incomplete => "incomplete",
|
||||||
|
NeighborState::Noarp => "noarp",
|
||||||
|
NeighborState::None => "none",
|
||||||
|
NeighborState::Failed => "failed",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const fn rank(self) -> u8 {
|
||||||
|
match self {
|
||||||
|
NeighborState::Permanent | NeighborState::Reachable => 5,
|
||||||
|
NeighborState::Stale => 4,
|
||||||
|
NeighborState::Delay | NeighborState::Probe | NeighborState::Incomplete => 3,
|
||||||
|
NeighborState::Noarp => 2,
|
||||||
|
NeighborState::None => 1,
|
||||||
|
NeighborState::Failed => 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PartialOrd for NeighborState {
|
||||||
|
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
|
||||||
|
Some(self.cmp(other))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Ord for NeighborState {
|
||||||
|
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
|
||||||
|
self.rank().cmp(&other.rank())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Default, Clone, Hash, Deserialize, Serialize)]
|
||||||
|
pub struct DeviceQuery {
|
||||||
|
pub name: Option<String>,
|
||||||
|
#[serde(flatten)]
|
||||||
|
pub filter: DeviceFilters,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[serde_as]
|
||||||
|
#[derive(Debug, Default, Clone, Hash, Serialize, Deserialize)]
|
||||||
|
pub struct DeviceFilters {
|
||||||
|
#[serde_as(as = "OneOrMany<_>")]
|
||||||
|
#[serde(default)]
|
||||||
|
pub ips: Vec<IpAddr>,
|
||||||
|
#[serde_as(as = "OneOrMany<_>")]
|
||||||
|
#[serde(default)]
|
||||||
|
pub devs: Vec<String>,
|
||||||
|
#[serde_as(as = "OneOrMany<_>")]
|
||||||
|
#[serde(default)]
|
||||||
|
pub nuds: Vec<NeighborState>,
|
||||||
|
#[serde_as(as = "OneOrMany<DisplayFromStr>")]
|
||||||
|
#[serde(default)]
|
||||||
|
pub macs: Vec<MacAddr>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[skip_serializing_none]
|
||||||
|
#[derive(Debug, Default, Serialize)]
|
||||||
|
pub struct Status<T> {
|
||||||
|
pub name: Option<String>,
|
||||||
|
pub table: Vec<T>,
|
||||||
|
pub filters: DeviceFilters,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Default, Clone, Hash, Deserialize)]
|
||||||
|
pub struct NamePath {
|
||||||
|
pub name: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize)]
|
||||||
|
pub struct DhcpLease {
|
||||||
|
pub expires_epoch: u64,
|
||||||
|
pub ip: IpAddr,
|
||||||
|
#[serde(with = "mac")]
|
||||||
|
pub mac: MacAddr,
|
||||||
|
pub name: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[skip_serializing_none]
|
||||||
|
#[derive(Debug, Clone, Serialize)]
|
||||||
|
pub struct DhcpLeaseWithState {
|
||||||
|
#[serde(flatten)]
|
||||||
|
pub lease_line: DhcpLease,
|
||||||
|
pub nud_state: Option<NeighborState>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[skip_serializing_none]
|
||||||
|
#[derive(Debug, Serialize, Deserialize, Clone, Copy, Hash, PartialEq, Eq)]
|
||||||
|
pub struct WakeTarget {
|
||||||
|
#[serde(default)]
|
||||||
|
pub ip: Option<IpAddr>,
|
||||||
|
#[serde(default, with = "mac::option_mac")]
|
||||||
|
pub mac: Option<MacAddr>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl WakeTarget {
|
||||||
|
pub const fn is_complete(&self) -> bool {
|
||||||
|
matches!(
|
||||||
|
self,
|
||||||
|
Self {
|
||||||
|
ip: Some(_),
|
||||||
|
mac: Some(_)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Clone)]
|
||||||
|
pub struct WakeResult {
|
||||||
|
pub result: Vec<WakeTargetResult>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[skip_serializing_none]
|
||||||
|
#[derive(Debug, Serialize, Clone, Copy)]
|
||||||
|
pub struct WakeTargetResult {
|
||||||
|
#[serde(flatten)]
|
||||||
|
pub target: WakeTarget,
|
||||||
|
pub status: WakeStatus,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Clone, Copy, Hash, PartialEq, Eq)]
|
||||||
|
#[serde(rename_all = "snake_case")]
|
||||||
|
pub enum WakeStatus {
|
||||||
|
Succeed,
|
||||||
|
NonexistentAddress,
|
||||||
|
WrongSize,
|
||||||
|
Incomplete,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl WakeTargetResult {
|
||||||
|
pub const fn incomplete(target: WakeTarget) -> Self {
|
||||||
|
Self {
|
||||||
|
target,
|
||||||
|
status: WakeStatus::Incomplete,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub enum QueryInput {
|
||||||
|
Ip(IpAddr),
|
||||||
|
Mac(MacAddr),
|
||||||
|
Dev(String),
|
||||||
|
Nud(NeighborState),
|
||||||
|
Name(String),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Display, thiserror::Error)]
|
||||||
|
pub enum NeighborParseError {
|
||||||
|
IpWhere,
|
||||||
|
IpParseError(#[from] std::net::AddrParseError),
|
||||||
|
MacParseError(#[from] macaddr::ParseError),
|
||||||
|
StateWhere,
|
||||||
|
StateParseError(#[from] strum::ParseError),
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn parse_neighbor_line(s: &str) -> Result<NeighborEntry, NeighborParseError> {
|
||||||
|
let mut it = s.split_whitespace();
|
||||||
|
let ip: IpAddr = it.next().ok_or(NeighborParseError::IpWhere)?.parse()?;
|
||||||
|
|
||||||
|
let mut dev: Option<String> = None;
|
||||||
|
let mut mac: Option<MacAddr> = None;
|
||||||
|
let mut state: Option<NeighborState> = None;
|
||||||
|
let mut last_tok: Option<&str> = None;
|
||||||
|
|
||||||
|
while let Some(tok) = it.next() {
|
||||||
|
match tok {
|
||||||
|
"dev" => dev = it.next().map(str::to_string),
|
||||||
|
"lladdr" => {
|
||||||
|
mac = it.next().map(|m| m.parse()).transpose()?;
|
||||||
|
}
|
||||||
|
"nud" => {
|
||||||
|
state = it.next().map(|st| st.parse()).transpose()?;
|
||||||
|
}
|
||||||
|
other => last_tok = Some(other),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if state.is_none() && let Some(st) = last_tok {
|
||||||
|
state = Some(st.parse()?);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(NeighborEntry {
|
||||||
|
ip,
|
||||||
|
dev,
|
||||||
|
mac,
|
||||||
|
state: state.ok_or(NeighborParseError::StateWhere)?,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FromStr for NeighborEntry {
|
||||||
|
type Err = NeighborParseError;
|
||||||
|
|
||||||
|
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||||
|
parse_neighbor_line(s)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,101 @@
|
|||||||
|
pub fn parse_numeric_ipv4(s: &str) -> Option<std::net::IpAddr> {
|
||||||
|
let s = s.trim();
|
||||||
|
if let Some(hex) = s.strip_prefix("0x").or_else(|| s.strip_prefix("0X"))
|
||||||
|
&& hex.chars().all(|c| c.is_ascii_hexdigit())
|
||||||
|
&& let Ok(n) = u32::from_str_radix(hex, 16)
|
||||||
|
{
|
||||||
|
return Some(std::net::IpAddr::V4(std::net::Ipv4Addr::from(n)));
|
||||||
|
}
|
||||||
|
if s.chars().all(|c| c.is_ascii_digit()) && let Ok(n) = s.parse::<u32>() {
|
||||||
|
return Some(std::net::IpAddr::V4(std::net::Ipv4Addr::from(n)));
|
||||||
|
}
|
||||||
|
if s.len() > 1
|
||||||
|
&& s.as_bytes()[0] == b'0'
|
||||||
|
&& s.chars().all(|c| matches!(c, '0'..='7'))
|
||||||
|
&& let Ok(n) = u32::from_str_radix(s, 8)
|
||||||
|
{
|
||||||
|
return Some(std::net::IpAddr::V4(std::net::Ipv4Addr::from(n)));
|
||||||
|
}
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn extract_host(input: &str) -> &str {
|
||||||
|
let mut s = input.trim();
|
||||||
|
if let Some(idx) = s.find("://") {
|
||||||
|
s = &s[idx + 3..];
|
||||||
|
} else if let Some(rest) = s.strip_prefix("//") {
|
||||||
|
s = rest;
|
||||||
|
}
|
||||||
|
if let Some((_, host)) = s.rsplit_once('@') {
|
||||||
|
s = host;
|
||||||
|
}
|
||||||
|
if let Some(host) = s.strip_prefix('[') {
|
||||||
|
if let Some(end) = host.find(']') {
|
||||||
|
s = &host[..end];
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if let Some(pos) = s.find('/') {
|
||||||
|
s = &s[..pos];
|
||||||
|
}
|
||||||
|
if let Some((host, port)) = s.rsplit_once(':')
|
||||||
|
&& s.matches(':').count() == 1
|
||||||
|
&& port.chars().all(|c| c.is_ascii_digit())
|
||||||
|
{
|
||||||
|
s = host;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
s.trim()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn boolish_str(s: &str) -> bool {
|
||||||
|
let t = s.trim().to_ascii_lowercase();
|
||||||
|
if t.is_empty() {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
matches!(t.as_str(), "1" | "true" | "yes" | "on" | "y")
|
||||||
|
|| (!matches!(t.as_str(), "0" | "false" | "no" | "off" | "n")
|
||||||
|
&& t.parse::<u64>().map(|n| n != 0).unwrap_or(false))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub mod mac {
|
||||||
|
use macaddr::MacAddr;
|
||||||
|
use serde::{Deserialize, Deserializer, Serializer};
|
||||||
|
|
||||||
|
pub fn serialize<S>(m: &MacAddr, serializer: S) -> Result<S::Ok, S::Error>
|
||||||
|
where
|
||||||
|
S: Serializer,
|
||||||
|
{
|
||||||
|
serializer.serialize_str(&m.to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn deserialize<'de, D>(deserializer: D) -> Result<MacAddr, D::Error>
|
||||||
|
where
|
||||||
|
D: Deserializer<'de>,
|
||||||
|
{
|
||||||
|
let s = String::deserialize(deserializer)?;
|
||||||
|
s.parse().map_err(serde::de::Error::custom)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub mod option_mac {
|
||||||
|
use macaddr::MacAddr;
|
||||||
|
use serde::{Deserialize, Deserializer, Serializer};
|
||||||
|
|
||||||
|
pub fn serialize<S>(m: &Option<MacAddr>, serializer: S) -> Result<S::Ok, S::Error>
|
||||||
|
where
|
||||||
|
S: Serializer,
|
||||||
|
{
|
||||||
|
match m {
|
||||||
|
Some(mac) => serializer.serialize_some(&mac.to_string()),
|
||||||
|
None => serializer.serialize_none(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn deserialize<'de, D>(deserializer: D) -> Result<Option<MacAddr>, D::Error>
|
||||||
|
where
|
||||||
|
D: Deserializer<'de>,
|
||||||
|
{
|
||||||
|
let s = Option::<String>::deserialize(deserializer)?;
|
||||||
|
s.map(|x| x.parse()).transpose().map_err(serde::de::Error::custom)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
[package]
|
||||||
|
name = "wakey-linux"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2024"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
anyhow = "1"
|
||||||
|
futures = "0"
|
||||||
|
macaddr = { version = "1", features = ["serde", "serde_std"] }
|
||||||
|
serde_json = "1"
|
||||||
|
tokio = { version = "1", features = ["fs", "net", "rt", "sync"] }
|
||||||
|
wakey-core = { path = "../wakey-core" }
|
||||||
|
|
||||||
|
[dependencies.lda-ipjs]
|
||||||
|
path = "../ipjs"
|
||||||
|
registry = "gitea"
|
||||||
|
version = "*"
|
||||||
@@ -0,0 +1,183 @@
|
|||||||
|
use anyhow::{Context, Result};
|
||||||
|
use futures::future::try_join_all;
|
||||||
|
use std::collections::HashSet;
|
||||||
|
use std::net::IpAddr;
|
||||||
|
use wakey_core::{DeviceQuery, NeighborEntry, NeighborState, QueryInput, parse};
|
||||||
|
|
||||||
|
use lda_ipjs::subcommands::neighbor;
|
||||||
|
|
||||||
|
pub async fn get_ips(machine_name: &str) -> Result<impl Iterator<Item = IpAddr>> {
|
||||||
|
Ok(tokio::net::lookup_host((machine_name, 0))
|
||||||
|
.await
|
||||||
|
.with_context(|| format!("DNS resolve failed for {machine_name}"))?
|
||||||
|
.map(|c| c.ip()))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn get_neighbors(
|
||||||
|
machine_names: &[impl AsRef<str>],
|
||||||
|
ips: &[IpAddr],
|
||||||
|
devs: &[impl AsRef<str>],
|
||||||
|
state: &[NeighborState],
|
||||||
|
macs: &[macaddr::MacAddr],
|
||||||
|
) -> Result<Vec<NeighborEntry>> {
|
||||||
|
let resolved_ips: HashSet<IpAddr> = if !machine_names.is_empty() {
|
||||||
|
try_join_all(machine_names.iter().map(|n| get_ips(n.as_ref())))
|
||||||
|
.await?
|
||||||
|
.into_iter()
|
||||||
|
.flatten()
|
||||||
|
.collect()
|
||||||
|
} else {
|
||||||
|
HashSet::new()
|
||||||
|
};
|
||||||
|
|
||||||
|
let ip_filter: Vec<IpAddr> = if ips.is_empty() && resolved_ips.is_empty() {
|
||||||
|
vec![]
|
||||||
|
} else if ips.is_empty() {
|
||||||
|
resolved_ips.into_iter().collect()
|
||||||
|
} else if resolved_ips.is_empty() {
|
||||||
|
ips.iter().map(|ip| ip.to_canonical()).collect()
|
||||||
|
} else {
|
||||||
|
ips.iter()
|
||||||
|
.map(|ip| ip.to_canonical())
|
||||||
|
.filter(|ip| resolved_ips.contains(ip))
|
||||||
|
.collect()
|
||||||
|
};
|
||||||
|
|
||||||
|
let nud_filter: Vec<neighbor::NUDState> = state.iter().copied().map(to_ipjs_state).collect();
|
||||||
|
let dev_strs: Vec<&str> = devs.iter().map(AsRef::as_ref).collect();
|
||||||
|
|
||||||
|
let results = neighbor::nl::get(&ip_filter, &dev_strs, &nud_filter, macs)
|
||||||
|
.await
|
||||||
|
.context("rtnetlink failed")?
|
||||||
|
.into_iter()
|
||||||
|
.map(map_neighbor_item)
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
Ok(results)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn query_status(query: &DeviceQuery) -> Result<Vec<NeighborEntry>> {
|
||||||
|
get_neighbors(
|
||||||
|
query.name.as_slice(),
|
||||||
|
&query.filter.ips,
|
||||||
|
&query.filter.devs,
|
||||||
|
&query.filter.nuds,
|
||||||
|
&query.filter.macs,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn list_devs() -> HashSet<String> {
|
||||||
|
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 e.file_type()
|
||||||
|
.map(|ft| {
|
||||||
|
if ft.is_symlink() {
|
||||||
|
std::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.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
|
||||||
|
}
|
||||||
|
|
||||||
|
tokio::task::spawn_blocking(get_dev)
|
||||||
|
.await
|
||||||
|
.unwrap_or_default()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn devs_sorted() -> Vec<String> {
|
||||||
|
let mut v: Vec<String> = list_devs().await.into_iter().collect();
|
||||||
|
v.sort();
|
||||||
|
v
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn has_dev(name: &str) -> bool {
|
||||||
|
list_devs().await.contains(name)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn classify_query(q: String) -> QueryInput {
|
||||||
|
let s = parse::extract_host(&q);
|
||||||
|
if let Some(ip) = parse::parse_numeric_ipv4(s).or_else(|| s.parse::<IpAddr>().ok()) {
|
||||||
|
return QueryInput::Ip(ip);
|
||||||
|
}
|
||||||
|
if let Ok(mac) = s.parse::<macaddr::MacAddr>() {
|
||||||
|
return QueryInput::Mac(mac);
|
||||||
|
}
|
||||||
|
if let Ok(state) = s.parse::<NeighborState>() {
|
||||||
|
return QueryInput::Nud(state);
|
||||||
|
}
|
||||||
|
if has_dev(s).await {
|
||||||
|
return QueryInput::Dev(s.to_string());
|
||||||
|
}
|
||||||
|
QueryInput::Name(s.to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn to_ipjs_state(value: NeighborState) -> lda_ipjs::subcommands::neighbor::NUDState {
|
||||||
|
match value {
|
||||||
|
NeighborState::Permanent => lda_ipjs::subcommands::neighbor::NUDState::Permanent,
|
||||||
|
NeighborState::Noarp => lda_ipjs::subcommands::neighbor::NUDState::Noarp,
|
||||||
|
NeighborState::Reachable => lda_ipjs::subcommands::neighbor::NUDState::Reachable,
|
||||||
|
NeighborState::Stale => lda_ipjs::subcommands::neighbor::NUDState::Stale,
|
||||||
|
NeighborState::None => lda_ipjs::subcommands::neighbor::NUDState::None,
|
||||||
|
NeighborState::Incomplete => lda_ipjs::subcommands::neighbor::NUDState::Incomplete,
|
||||||
|
NeighborState::Delay => lda_ipjs::subcommands::neighbor::NUDState::Delay,
|
||||||
|
NeighborState::Probe => lda_ipjs::subcommands::neighbor::NUDState::Probe,
|
||||||
|
NeighborState::Failed => lda_ipjs::subcommands::neighbor::NUDState::Failed,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn from_ipjs_state(value: lda_ipjs::subcommands::neighbor::NUDState) -> NeighborState {
|
||||||
|
match value {
|
||||||
|
lda_ipjs::subcommands::neighbor::NUDState::Permanent => NeighborState::Permanent,
|
||||||
|
lda_ipjs::subcommands::neighbor::NUDState::Noarp => NeighborState::Noarp,
|
||||||
|
lda_ipjs::subcommands::neighbor::NUDState::Reachable => NeighborState::Reachable,
|
||||||
|
lda_ipjs::subcommands::neighbor::NUDState::Stale => NeighborState::Stale,
|
||||||
|
lda_ipjs::subcommands::neighbor::NUDState::None => NeighborState::None,
|
||||||
|
lda_ipjs::subcommands::neighbor::NUDState::Incomplete => NeighborState::Incomplete,
|
||||||
|
lda_ipjs::subcommands::neighbor::NUDState::Delay => NeighborState::Delay,
|
||||||
|
lda_ipjs::subcommands::neighbor::NUDState::Probe => NeighborState::Probe,
|
||||||
|
lda_ipjs::subcommands::neighbor::NUDState::Failed => NeighborState::Failed,
|
||||||
|
lda_ipjs::subcommands::neighbor::NUDState::Other(_) => NeighborState::None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn map_neighbor_item(
|
||||||
|
lda_ipjs::subcommands::neighbor::NeighborItem {
|
||||||
|
ip,
|
||||||
|
dev,
|
||||||
|
mac,
|
||||||
|
state,
|
||||||
|
}: lda_ipjs::subcommands::neighbor::NeighborItem,
|
||||||
|
) -> NeighborEntry {
|
||||||
|
NeighborEntry {
|
||||||
|
ip,
|
||||||
|
dev,
|
||||||
|
mac,
|
||||||
|
state: state
|
||||||
|
.into_iter()
|
||||||
|
.map(from_ipjs_state)
|
||||||
|
.max()
|
||||||
|
.unwrap_or(NeighborState::None),
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,93 @@
|
|||||||
|
use std::io::{self, ErrorKind};
|
||||||
|
use std::net::IpAddr;
|
||||||
|
|
||||||
|
use wakey_core::{DhcpLease, DhcpLeaseWithState};
|
||||||
|
|
||||||
|
const MAC_NAME_CACHE: &str = "/tmp/wakey_mac_names.json";
|
||||||
|
|
||||||
|
pub async fn load_mac_name_cache() -> io::Result<std::collections::BTreeMap<String, String>> {
|
||||||
|
match tokio::fs::read_to_string(MAC_NAME_CACHE).await {
|
||||||
|
Ok(s) => serde_json::from_str(&s).map_err(io::Error::other),
|
||||||
|
Err(e) if e.kind() == ErrorKind::NotFound => Ok(Default::default()),
|
||||||
|
Err(e) => Err(e),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn save_mac_name_cache(map: &std::collections::BTreeMap<String, String>) -> io::Result<()> {
|
||||||
|
let s = serde_json::to_string(map).map_err(io::Error::other)?;
|
||||||
|
let _ = tokio::fs::write(MAC_NAME_CACHE, s).await;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn parse_dhcp_lease_line(line: &str) -> Option<DhcpLease> {
|
||||||
|
let mut c = line.split_whitespace();
|
||||||
|
let expires_epoch: u64 = c.next()?.parse().ok()?;
|
||||||
|
let mac = c.next()?.parse().ok()?;
|
||||||
|
let ip = c.next()?.parse().ok()?;
|
||||||
|
let name = c.next().filter(|c| *c != "*").map(str::to_string);
|
||||||
|
Some(DhcpLease {
|
||||||
|
expires_epoch,
|
||||||
|
ip,
|
||||||
|
mac,
|
||||||
|
name,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn read_dhcp_leases() -> io::Result<Vec<DhcpLease>> {
|
||||||
|
match tokio::fs::read_to_string("/tmp/dhcp.leases").await {
|
||||||
|
Ok(file) => Ok(file.lines().filter_map(parse_dhcp_lease_line).collect()),
|
||||||
|
Err(e) if e.kind() == ErrorKind::NotFound => Ok(Vec::new()),
|
||||||
|
Err(e) => Err(e),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn read_dhcp_leases_with_names() -> io::Result<Vec<DhcpLease>> {
|
||||||
|
let leases = read_dhcp_leases().await?;
|
||||||
|
let mut cache = load_mac_name_cache().await.unwrap_or_default();
|
||||||
|
let mut changed = false;
|
||||||
|
let mut leases_with_names = Vec::with_capacity(leases.len());
|
||||||
|
for mut l in leases {
|
||||||
|
let mac_s = l.mac.to_string();
|
||||||
|
if let Some(ref name) = l.name {
|
||||||
|
if cache.get(&mac_s).map(|v| v != name).unwrap_or(true) {
|
||||||
|
cache.insert(mac_s, name.clone());
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
} else if let Some(prev) = cache.get(&mac_s) {
|
||||||
|
l.name = Some(prev.clone());
|
||||||
|
}
|
||||||
|
leases_with_names.push(l);
|
||||||
|
}
|
||||||
|
if changed {
|
||||||
|
let _ = save_mac_name_cache(&cache).await;
|
||||||
|
}
|
||||||
|
Ok(leases_with_names)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn enrich_leases_with_nud_state(leases: Vec<DhcpLease>) -> Vec<DhcpLeaseWithState> {
|
||||||
|
let ips: Vec<IpAddr> = leases.iter().map(|l| l.ip).collect();
|
||||||
|
let mut map: std::collections::HashMap<IpAddr, wakey_core::NeighborState> =
|
||||||
|
std::collections::HashMap::new();
|
||||||
|
if let Ok(rows) =
|
||||||
|
crate::devices::get_neighbors(&[] as &[&str], &ips, &[] as &[&str], &[], &[]).await
|
||||||
|
{
|
||||||
|
for row in rows {
|
||||||
|
let state = row.state;
|
||||||
|
let r = state.rank();
|
||||||
|
map.entry(row.ip)
|
||||||
|
.and_modify(|e| {
|
||||||
|
if r > e.rank() {
|
||||||
|
*e = state
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.or_insert(state);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
leases
|
||||||
|
.into_iter()
|
||||||
|
.map(|lease_line| DhcpLeaseWithState {
|
||||||
|
nud_state: map.get(&lease_line.ip).copied(),
|
||||||
|
lease_line,
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
pub mod dhcp;
|
||||||
|
pub mod devices;
|
||||||
|
pub mod wake;
|
||||||
|
|
||||||
|
pub use dhcp::*;
|
||||||
|
pub use devices::*;
|
||||||
|
pub use wake::*;
|
||||||
@@ -0,0 +1,78 @@
|
|||||||
|
use std::{io, net::IpAddr};
|
||||||
|
|
||||||
|
use futures::TryFutureExt;
|
||||||
|
use macaddr::MacAddr;
|
||||||
|
use tokio::net::UdpSocket;
|
||||||
|
use wakey_core::{WakeStatus, WakeTarget, WakeTargetResult};
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, Hash)]
|
||||||
|
pub struct CompleteWakeTarget {
|
||||||
|
pub ip: IpAddr,
|
||||||
|
pub mac: MacAddr,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TryFrom<WakeTarget> for CompleteWakeTarget {
|
||||||
|
type Error = ();
|
||||||
|
|
||||||
|
fn try_from(value: WakeTarget) -> Result<Self, Self::Error> {
|
||||||
|
if let WakeTarget {
|
||||||
|
ip: Some(ip),
|
||||||
|
mac: Some(mac),
|
||||||
|
} = value
|
||||||
|
{
|
||||||
|
Ok(Self { ip, mac })
|
||||||
|
} else {
|
||||||
|
Err(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn wake_one(sock: &UdpSocket, t: CompleteWakeTarget) -> 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);
|
||||||
|
}
|
||||||
|
match sock.send_to(&pac, (t.ip, 9)).await {
|
||||||
|
Ok(n) if n == pac.len() => WakeTargetResult {
|
||||||
|
target: WakeTarget {
|
||||||
|
ip: Some(t.ip),
|
||||||
|
mac: Some(t.mac),
|
||||||
|
},
|
||||||
|
status: WakeStatus::Succeed,
|
||||||
|
},
|
||||||
|
Ok(_) => WakeTargetResult {
|
||||||
|
target: WakeTarget {
|
||||||
|
ip: Some(t.ip),
|
||||||
|
mac: Some(t.mac),
|
||||||
|
},
|
||||||
|
status: WakeStatus::WrongSize,
|
||||||
|
},
|
||||||
|
Err(_) => WakeTargetResult {
|
||||||
|
target: WakeTarget {
|
||||||
|
ip: Some(t.ip),
|
||||||
|
mac: Some(t.mac),
|
||||||
|
},
|
||||||
|
status: WakeStatus::NonexistentAddress,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn wake_many(
|
||||||
|
targets: impl IntoIterator<Item = WakeTarget>,
|
||||||
|
) -> io::Result<Vec<WakeTargetResult>> {
|
||||||
|
let sock = UdpSocket::bind("[::]:0")
|
||||||
|
.or_else(|_| UdpSocket::bind(":0"))
|
||||||
|
.await?;
|
||||||
|
sock.set_broadcast(true)?;
|
||||||
|
|
||||||
|
let iter = targets.into_iter().map(async |target| {
|
||||||
|
match CompleteWakeTarget::try_from(target) {
|
||||||
|
Ok(target) => wake_one(&sock, target).await,
|
||||||
|
Err(()) => WakeTargetResult::incomplete(target),
|
||||||
|
}
|
||||||
|
});
|
||||||
|
Ok(futures::future::join_all(iter).await)
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user