Dihvice. Chat Sweep.
lowk traking devices ts way is TUFF. how do you even try to try.
This commit is contained in:
@@ -2,6 +2,9 @@
|
||||
param(
|
||||
[string]$PASSWD
|
||||
)
|
||||
. "$(Split-Path -Parent $PSScriptRoot)/scripts/lib.ps1"
|
||||
|
||||
$PASSWD = Get-DefaultPassword $PASSWD
|
||||
|
||||
# idk what this does it works like that then thats how it is
|
||||
pscp.exe -l root -scp -pw $PASSWD -r 192.168.100.1:/etc/ldlda_help $PSScriptRoot
|
||||
|
||||
+165
-27
@@ -11,49 +11,63 @@ use axum::Router;
|
||||
use tokio::net::TcpListener;
|
||||
use tower_http::services::ServeDir;
|
||||
use wakey_core::{
|
||||
DeviceFilters, DeviceQuery, DhcpLease, DhcpLeaseWithState, NeighborEntry, QueryInput, Status,
|
||||
WakeResult, WakeTarget,
|
||||
Device, DeviceFilters, DeviceInventory, DeviceQuery, DhcpLease, DhcpLeaseWithState, LeaseQuery,
|
||||
NeighborEntry, Presence, Query, QueryInput, Status, WakeResult, WakeTarget,
|
||||
};
|
||||
|
||||
pub type StatusResponse = Status<NeighborEntry>;
|
||||
|
||||
pub async fn resolve_query(input: impl Into<String>) -> Result<DeviceQuery> {
|
||||
query_to_device_query(resolve_selector(input).await?)
|
||||
}
|
||||
|
||||
pub async fn resolve_selector(input: impl Into<String>) -> Result<Query> {
|
||||
Ok(
|
||||
match wakey_linux::devices::classify_query(input.into()).await {
|
||||
QueryInput::Ip(ip_addr) => DeviceQuery {
|
||||
QueryInput::Ip(ip_addr) => Query::Ip(ip_addr),
|
||||
QueryInput::Mac(mac_addr) => Query::Mac(mac_addr),
|
||||
QueryInput::Dev(dev) => Query::Interface(dev),
|
||||
QueryInput::Nud(state) => Query::NeighborState(state),
|
||||
QueryInput::Name(name) => Query::Text(name),
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
pub fn query_to_device_query(query: Query) -> Result<DeviceQuery> {
|
||||
Ok(match query {
|
||||
Query::Ip(ip_addr) => DeviceQuery {
|
||||
filter: DeviceFilters {
|
||||
ips: vec![ip_addr],
|
||||
..Default::default()
|
||||
},
|
||||
..Default::default()
|
||||
},
|
||||
QueryInput::Mac(mac_addr) => DeviceQuery {
|
||||
Query::Mac(mac_addr) => DeviceQuery {
|
||||
filter: DeviceFilters {
|
||||
macs: vec![mac_addr],
|
||||
..Default::default()
|
||||
},
|
||||
..Default::default()
|
||||
},
|
||||
QueryInput::Dev(dev) => DeviceQuery {
|
||||
Query::Interface(dev) => DeviceQuery {
|
||||
filter: DeviceFilters {
|
||||
devs: vec![dev],
|
||||
..Default::default()
|
||||
},
|
||||
..Default::default()
|
||||
},
|
||||
QueryInput::Nud(state) => DeviceQuery {
|
||||
Query::NeighborState(state) => DeviceQuery {
|
||||
filter: DeviceFilters {
|
||||
nuds: vec![state],
|
||||
..Default::default()
|
||||
},
|
||||
..Default::default()
|
||||
},
|
||||
QueryInput::Name(name) => DeviceQuery {
|
||||
Query::Text(name) => DeviceQuery {
|
||||
name: Some(name),
|
||||
..Default::default()
|
||||
},
|
||||
},
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn get_status(query: DeviceQuery) -> Result<StatusResponse> {
|
||||
@@ -70,20 +84,14 @@ pub async fn get_status_for_input(input: impl Into<String>) -> Result<StatusResp
|
||||
get_status(query).await
|
||||
}
|
||||
|
||||
pub async fn get_leases(include_state: bool) -> Result<Vec<DhcpLeaseWithState>> {
|
||||
pub async fn get_leases(query: LeaseQuery) -> Result<Vec<DhcpLeaseWithState>> {
|
||||
let leases = wakey_linux::dhcp::read_dhcp_leases_with_names()
|
||||
.await
|
||||
.context("failed to read DHCP leases")?;
|
||||
if include_state {
|
||||
if query.include_state {
|
||||
Ok(wakey_linux::dhcp::enrich_leases_with_nud_state(leases).await)
|
||||
} else {
|
||||
Ok(leases
|
||||
.into_iter()
|
||||
.map(|lease_line| DhcpLeaseWithState {
|
||||
lease_line,
|
||||
nud_state: None,
|
||||
})
|
||||
.collect())
|
||||
Ok(leases_without_state(leases))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -95,15 +103,7 @@ pub async fn wake_targets(targets: Vec<WakeTarget>) -> Result<WakeResult> {
|
||||
}
|
||||
|
||||
pub async fn wake_from_query(input: impl Into<String>) -> Result<WakeResult> {
|
||||
let status = get_status_for_input(input).await?;
|
||||
let targets = status
|
||||
.table
|
||||
.into_iter()
|
||||
.map(|entry| WakeTarget {
|
||||
ip: Some(entry.ip),
|
||||
mac: entry.mac,
|
||||
})
|
||||
.collect();
|
||||
let targets = resolve_wake_targets(input).await?;
|
||||
wake_targets(targets).await
|
||||
}
|
||||
|
||||
@@ -117,6 +117,110 @@ pub async fn get_ips(name: impl AsRef<str>) -> Result<Vec<std::net::IpAddr>> {
|
||||
.collect())
|
||||
}
|
||||
|
||||
pub async fn resolve_devices(input: impl Into<String>) -> Result<Vec<Device>> {
|
||||
let query = resolve_query(input).await?;
|
||||
inventory(query).await.map(|inventory| inventory.devices)
|
||||
}
|
||||
|
||||
pub async fn inventory(query: DeviceQuery) -> Result<DeviceInventory> {
|
||||
let status = get_status(query.clone()).await?;
|
||||
let leases = get_leases(LeaseQuery {
|
||||
include_state: false,
|
||||
})
|
||||
.await?;
|
||||
Ok(DeviceInventory {
|
||||
devices: merge_devices(status.table, leases, &query),
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn resolve_wake_targets(input: impl Into<String>) -> Result<Vec<WakeTarget>> {
|
||||
let devices = resolve_devices(input).await?;
|
||||
Ok(devices
|
||||
.into_iter()
|
||||
.flat_map(|device| {
|
||||
let mac = device.macs.first().copied();
|
||||
device
|
||||
.ips
|
||||
.into_iter()
|
||||
.map(move |ip| WakeTarget { ip: Some(ip), mac })
|
||||
})
|
||||
.collect())
|
||||
}
|
||||
|
||||
fn merge_devices(
|
||||
neighbors: Vec<NeighborEntry>,
|
||||
leases: Vec<DhcpLeaseWithState>,
|
||||
query: &DeviceQuery,
|
||||
) -> Vec<Device> {
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
let mut by_mac: BTreeMap<String, (Vec<NeighborEntry>, Vec<DhcpLease>)> = BTreeMap::new();
|
||||
|
||||
for row in neighbors {
|
||||
let key = row
|
||||
.mac
|
||||
.map(|m| m.to_string())
|
||||
.unwrap_or_else(|| format!("ip:{}", row.ip));
|
||||
by_mac.entry(key).or_default().0.push(row);
|
||||
}
|
||||
for lease in leases {
|
||||
let key = lease.lease_line.mac.to_string();
|
||||
by_mac.entry(key).or_default().1.push(lease.lease_line);
|
||||
}
|
||||
|
||||
let mut devices: Vec<Device> = by_mac
|
||||
.into_values()
|
||||
.map(|(neighbors, leases)| Device::from_parts(neighbors, leases))
|
||||
.collect();
|
||||
|
||||
if let Some(name) = &query.name {
|
||||
devices.retain(|device| device.names.iter().any(|n| n == name));
|
||||
}
|
||||
if !query.filter.devs.is_empty() {
|
||||
devices.retain(|device| {
|
||||
device
|
||||
.interfaces
|
||||
.iter()
|
||||
.any(|iface| query.filter.devs.contains(iface))
|
||||
});
|
||||
}
|
||||
if !query.filter.ips.is_empty() {
|
||||
devices.retain(|device| device.ips.iter().any(|ip| query.filter.ips.contains(ip)));
|
||||
}
|
||||
if !query.filter.macs.is_empty() {
|
||||
devices.retain(|device| {
|
||||
device
|
||||
.macs
|
||||
.iter()
|
||||
.any(|mac| query.filter.macs.contains(mac))
|
||||
});
|
||||
}
|
||||
if !query.filter.nuds.is_empty() {
|
||||
devices.retain(|device| {
|
||||
device
|
||||
.neighbors
|
||||
.iter()
|
||||
.any(|neighbor| query.filter.nuds.contains(&neighbor.state))
|
||||
});
|
||||
}
|
||||
|
||||
devices.sort_by(|a, b| {
|
||||
presence_rank(b.presence)
|
||||
.cmp(&presence_rank(a.presence))
|
||||
.then_with(|| a.names.first().cmp(&b.names.first()))
|
||||
});
|
||||
devices
|
||||
}
|
||||
|
||||
const fn presence_rank(presence: Presence) -> u8 {
|
||||
match presence {
|
||||
Presence::Online => 3,
|
||||
Presence::LikelyOnline => 2,
|
||||
Presence::Unknown => 1,
|
||||
Presence::Offline => 0,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn http_app(static_root: std::path::PathBuf) -> Router {
|
||||
Router::new()
|
||||
.nest("/api", route::api_router())
|
||||
@@ -182,6 +286,17 @@ mod tests {
|
||||
assert_eq!(query.filter.nuds, vec![NeighborState::Reachable]);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn resolve_selector_keeps_text_vs_structured() {
|
||||
let selector = resolve_selector("reachable")
|
||||
.await
|
||||
.expect("resolve selector");
|
||||
match selector {
|
||||
Query::NeighborState(NeighborState::Reachable) => {}
|
||||
_ => panic!("expected neighbor-state selector"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn leases_without_state_clears_nud_state() {
|
||||
let leases = vec![DhcpLease {
|
||||
@@ -195,6 +310,29 @@ mod tests {
|
||||
assert!(out[0].nud_state.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn merge_devices_combines_lease_and_neighbor() {
|
||||
let neighbors = vec![NeighborEntry {
|
||||
ip: IpAddr::V4(Ipv4Addr::new(192, 168, 1, 10)),
|
||||
dev: Some("br-lan".into()),
|
||||
mac: Some("aa:bb:cc:dd:ee:ff".parse().expect("mac")),
|
||||
state: NeighborState::Reachable,
|
||||
}];
|
||||
let leases = vec![DhcpLeaseWithState {
|
||||
lease_line: DhcpLease {
|
||||
expires_epoch: 1,
|
||||
ip: IpAddr::V4(Ipv4Addr::new(192, 168, 1, 10)),
|
||||
mac: "aa:bb:cc:dd:ee:ff".parse().expect("mac"),
|
||||
name: Some("pc".into()),
|
||||
},
|
||||
nud_state: None,
|
||||
}];
|
||||
let devices = merge_devices(neighbors, leases, &DeviceQuery::default());
|
||||
assert_eq!(devices.len(), 1);
|
||||
assert_eq!(devices[0].presence, Presence::Online);
|
||||
assert_eq!(devices[0].names, vec!["pc".to_string()]);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn wake_targets_marks_incomplete() {
|
||||
let out = wake_targets(vec![WakeTarget {
|
||||
|
||||
+4
-1
@@ -110,7 +110,10 @@ async fn main() -> anyhow::Result<()> {
|
||||
println!("{}", serde_json::to_string_pretty(&status)?);
|
||||
}
|
||||
Command::Leases(args) => {
|
||||
let leases = wakey::get_leases(args.include_state).await?;
|
||||
let leases = wakey::get_leases(wakey_core::LeaseQuery {
|
||||
include_state: args.include_state,
|
||||
})
|
||||
.await?;
|
||||
println!("{}", serde_json::to_string_pretty(&leases)?);
|
||||
}
|
||||
Command::Wake(args) => {
|
||||
|
||||
+1
-1
@@ -12,7 +12,7 @@ pub async fn get_dhcp_leases(
|
||||
) -> impl IntoResponse {
|
||||
let include_state = include_state.as_deref().map(boolish_str).unwrap_or(false);
|
||||
|
||||
match crate::get_leases(include_state).await {
|
||||
match crate::get_leases(wakey_core::LeaseQuery { include_state }).await {
|
||||
Ok(leases) => (
|
||||
StatusCode::OK,
|
||||
Json(crate::compat::legacy_leases_from_domain(leases)),
|
||||
|
||||
@@ -75,6 +75,117 @@ impl Ord for NeighborState {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Clone, Copy, Hash, Serialize, Deserialize, Default)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum Presence {
|
||||
Online,
|
||||
LikelyOnline,
|
||||
#[default]
|
||||
Unknown,
|
||||
Offline,
|
||||
}
|
||||
|
||||
impl From<NeighborState> for Presence {
|
||||
fn from(value: NeighborState) -> Self {
|
||||
match value {
|
||||
NeighborState::Permanent | NeighborState::Reachable => Self::Online,
|
||||
NeighborState::Stale => Self::LikelyOnline,
|
||||
NeighborState::Failed => Self::Offline,
|
||||
NeighborState::Delay
|
||||
| NeighborState::Probe
|
||||
| NeighborState::Incomplete
|
||||
| NeighborState::Noarp
|
||||
| NeighborState::None => Self::Unknown,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Clone, Hash, Serialize)]
|
||||
pub struct DeviceId {
|
||||
#[serde(with = "mac")]
|
||||
pub mac: MacAddr,
|
||||
}
|
||||
|
||||
#[skip_serializing_none]
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
pub struct Device {
|
||||
pub id: Option<DeviceId>,
|
||||
pub names: Vec<String>,
|
||||
pub ips: Vec<IpAddr>,
|
||||
#[serde(with = "mac::vec_mac")]
|
||||
pub macs: Vec<MacAddr>,
|
||||
pub interfaces: Vec<String>,
|
||||
pub neighbors: Vec<NeighborEntry>,
|
||||
pub leases: Vec<DhcpLease>,
|
||||
pub presence: Presence,
|
||||
}
|
||||
|
||||
impl Device {
|
||||
pub fn from_parts(neighbors: Vec<NeighborEntry>, leases: Vec<DhcpLease>) -> Self {
|
||||
use std::collections::BTreeSet;
|
||||
|
||||
let mut names = BTreeSet::new();
|
||||
let mut ips = BTreeSet::new();
|
||||
let mut macs = BTreeSet::new();
|
||||
let mut interfaces = BTreeSet::new();
|
||||
let mut presence = Presence::Unknown;
|
||||
|
||||
for lease in &leases {
|
||||
ips.insert(lease.ip);
|
||||
macs.insert(lease.mac);
|
||||
if let Some(name) = &lease.name {
|
||||
names.insert(name.clone());
|
||||
}
|
||||
}
|
||||
for neighbor in &neighbors {
|
||||
ips.insert(neighbor.ip);
|
||||
if let Some(mac) = neighbor.mac {
|
||||
macs.insert(mac);
|
||||
}
|
||||
if let Some(dev) = &neighbor.dev {
|
||||
interfaces.insert(dev.clone());
|
||||
}
|
||||
presence = std::cmp::max(
|
||||
presence_rank(presence),
|
||||
presence_rank(neighbor.state.into()),
|
||||
)
|
||||
.into();
|
||||
}
|
||||
|
||||
let macs: Vec<MacAddr> = macs.into_iter().collect();
|
||||
Self {
|
||||
id: macs.first().copied().map(|mac| DeviceId { mac }),
|
||||
names: names.into_iter().collect(),
|
||||
ips: ips.into_iter().collect(),
|
||||
macs,
|
||||
interfaces: interfaces.into_iter().collect(),
|
||||
neighbors,
|
||||
leases,
|
||||
presence,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const fn presence_rank(presence: Presence) -> u8 {
|
||||
match presence {
|
||||
Presence::Online => 3,
|
||||
Presence::LikelyOnline => 2,
|
||||
Presence::Unknown => 1,
|
||||
Presence::Offline => 0,
|
||||
}
|
||||
}
|
||||
|
||||
impl From<u8> for Presence {
|
||||
fn from(value: u8) -> Self {
|
||||
match value {
|
||||
3 => Self::Online,
|
||||
2 => Self::LikelyOnline,
|
||||
0 => Self::Offline,
|
||||
_ => Self::Unknown,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Clone, Hash, Deserialize, Serialize)]
|
||||
pub struct DeviceQuery {
|
||||
pub name: Option<String>,
|
||||
@@ -129,6 +240,16 @@ pub struct DhcpLeaseWithState {
|
||||
pub nud_state: Option<NeighborState>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Clone, Serialize, Deserialize)]
|
||||
pub struct LeaseQuery {
|
||||
pub include_state: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Clone, Serialize)]
|
||||
pub struct DeviceInventory {
|
||||
pub devices: Vec<Device>,
|
||||
}
|
||||
|
||||
#[skip_serializing_none]
|
||||
#[derive(Debug, Serialize, Deserialize, Clone, Copy, Hash, PartialEq, Eq)]
|
||||
pub struct WakeTarget {
|
||||
@@ -190,6 +311,15 @@ pub enum QueryInput {
|
||||
Name(String),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum Query {
|
||||
Text(String),
|
||||
Ip(IpAddr),
|
||||
Mac(MacAddr),
|
||||
Interface(String),
|
||||
NeighborState(NeighborState),
|
||||
}
|
||||
|
||||
#[derive(Debug, Display, thiserror::Error)]
|
||||
pub enum NeighborParseError {
|
||||
IpWhere,
|
||||
|
||||
@@ -102,4 +102,30 @@ pub mod mac {
|
||||
.map_err(serde::de::Error::custom)
|
||||
}
|
||||
}
|
||||
|
||||
// Vec<DisplayFromStr> ?
|
||||
pub mod vec_mac {
|
||||
use macaddr::MacAddr;
|
||||
use serde::{Deserialize, Deserializer, Serialize, Serializer};
|
||||
|
||||
pub fn serialize<S>(macs: &[MacAddr], serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: Serializer,
|
||||
{
|
||||
let strings: Vec<String> = macs.iter().map(ToString::to_string).collect();
|
||||
strings.serialize(serializer)
|
||||
}
|
||||
|
||||
pub fn deserialize<'de, D>(deserializer: D) -> Result<Vec<MacAddr>, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
let strings = Vec::<String>::deserialize(deserializer)?;
|
||||
strings
|
||||
.into_iter()
|
||||
.map(|s| s.parse())
|
||||
.collect::<Result<Vec<_>, _>>()
|
||||
.map_err(serde::de::Error::custom)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user