the Chat sweep
This commit is contained in:
@@ -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