cant wait to do all types of shi on ts hook
also turn all into Verifiable macros pls dont fail github
This commit is contained in:
@@ -8,6 +8,7 @@ publish = ["gitea"]
|
||||
anyhow = "1"
|
||||
futures = "0"
|
||||
macaddr = { version = "1", features = ["serde", "serde_std"] }
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_json = "1"
|
||||
tokio = { version = "1", features = ["fs", "net", "rt", "sync"] }
|
||||
wakey-core = { path = "../wakey-core", registry = "gitea", version = "0"}
|
||||
|
||||
+207
-21
@@ -1,10 +1,52 @@
|
||||
use std::io::{self, ErrorKind};
|
||||
use std::net::IpAddr;
|
||||
use std::time::{SystemTime, UNIX_EPOCH};
|
||||
|
||||
use macaddr::MacAddr;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use wakey_core::{DhcpLease, DhcpLeaseWithState};
|
||||
|
||||
const MAC_NAME_CACHE: &str = "/tmp/wakey_mac_names.json";
|
||||
const OBSERVATION_STORE: &str = "/tmp/wakey_observations.json";
|
||||
|
||||
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
|
||||
pub struct LocalObservationStore {
|
||||
#[serde(default)]
|
||||
pub dhcp_clients: std::collections::BTreeMap<String, ObservedDhcpClient>,
|
||||
#[serde(default)]
|
||||
pub neighbors: std::collections::BTreeMap<String, ObservedNeighbor>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct ObservedDhcpClient {
|
||||
pub mac: String,
|
||||
pub ip: Option<IpAddr>,
|
||||
pub hostname: Option<String>,
|
||||
pub first_seen_unix: u64,
|
||||
pub last_seen_unix: u64,
|
||||
pub last_action: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct ObservedNeighbor {
|
||||
pub key: String,
|
||||
pub mac: Option<String>,
|
||||
pub ip: Option<IpAddr>,
|
||||
pub first_seen_unix: u64,
|
||||
pub last_seen_unix: u64,
|
||||
pub last_action: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct LocalDeviceObservation {
|
||||
pub kind: String,
|
||||
pub action: String,
|
||||
pub mac: Option<String>,
|
||||
pub ip: Option<IpAddr>,
|
||||
pub hostname: Option<String>,
|
||||
pub first_seen_unix: u64,
|
||||
pub last_seen_unix: u64,
|
||||
}
|
||||
|
||||
/// Load the MAC-to-name cache used to preserve useful names across lease churn.
|
||||
pub async fn load_mac_name_cache() -> io::Result<std::collections::BTreeMap<String, String>> {
|
||||
@@ -22,43 +64,173 @@ async fn save_mac_name_cache(map: &std::collections::BTreeMap<String, String>) -
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn load_observation_store() -> io::Result<LocalObservationStore> {
|
||||
match tokio::fs::read_to_string(OBSERVATION_STORE).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_observation_store(store: &LocalObservationStore) -> io::Result<()> {
|
||||
let s = serde_json::to_string(store).map_err(io::Error::other)?;
|
||||
tokio::fs::write(OBSERVATION_STORE, s).await
|
||||
}
|
||||
|
||||
pub async fn list_local_observations() -> io::Result<Vec<LocalDeviceObservation>> {
|
||||
let store = load_observation_store().await?;
|
||||
let mut out = Vec::with_capacity(store.dhcp_clients.len() + store.neighbors.len());
|
||||
out.extend(
|
||||
store
|
||||
.dhcp_clients
|
||||
.into_values()
|
||||
.map(|row| LocalDeviceObservation {
|
||||
kind: "dhcp".into(),
|
||||
action: row.last_action,
|
||||
mac: Some(row.mac),
|
||||
ip: row.ip,
|
||||
hostname: row.hostname,
|
||||
first_seen_unix: row.first_seen_unix,
|
||||
last_seen_unix: row.last_seen_unix,
|
||||
}),
|
||||
);
|
||||
out.extend(
|
||||
store
|
||||
.neighbors
|
||||
.into_values()
|
||||
.map(|row| LocalDeviceObservation {
|
||||
kind: "neigh".into(),
|
||||
action: row.last_action,
|
||||
mac: row.mac,
|
||||
ip: row.ip,
|
||||
hostname: None,
|
||||
first_seen_unix: row.first_seen_unix,
|
||||
last_seen_unix: row.last_seen_unix,
|
||||
}),
|
||||
);
|
||||
out.sort_by(|a, b| {
|
||||
b.last_seen_unix
|
||||
.cmp(&a.last_seen_unix)
|
||||
.then(a.kind.cmp(&b.kind))
|
||||
.then(a.mac.cmp(&b.mac))
|
||||
.then(a.ip.cmp(&b.ip))
|
||||
});
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
/// Observe a DHCP hotplug event and update the local MAC-to-name cache.
|
||||
pub async fn observe_dhcp_event(
|
||||
action: &str,
|
||||
mac: MacAddr,
|
||||
_ip: Option<IpAddr>,
|
||||
ip: Option<IpAddr>,
|
||||
hostname: Option<&str>,
|
||||
) -> io::Result<bool> {
|
||||
if !matches!(action, "add" | "update" | "old") {
|
||||
if !matches!(action, "add" | "update" | "old" | "remove") {
|
||||
return Ok(false);
|
||||
}
|
||||
|
||||
let Some(hostname) = hostname
|
||||
let hostname = hostname
|
||||
.map(str::trim)
|
||||
.filter(|v| !v.is_empty() && *v != "*")
|
||||
.map(ToOwned::to_owned);
|
||||
let now = now_unix();
|
||||
let mac_s = mac.to_string();
|
||||
|
||||
let mut store = load_observation_store().await.unwrap_or_default();
|
||||
let mut changed = false;
|
||||
store
|
||||
.dhcp_clients
|
||||
.entry(mac_s.clone())
|
||||
.and_modify(|row| {
|
||||
if row.ip != ip
|
||||
|| row.hostname != hostname
|
||||
|| row.last_action != action
|
||||
|| row.last_seen_unix != now
|
||||
{
|
||||
row.ip = ip;
|
||||
row.hostname = hostname.clone();
|
||||
row.last_action = action.to_string();
|
||||
row.last_seen_unix = now;
|
||||
changed = true;
|
||||
}
|
||||
})
|
||||
.or_insert_with(|| {
|
||||
changed = true;
|
||||
ObservedDhcpClient {
|
||||
mac: mac_s.clone(),
|
||||
ip,
|
||||
hostname: hostname.clone(),
|
||||
first_seen_unix: now,
|
||||
last_seen_unix: now,
|
||||
last_action: action.to_string(),
|
||||
}
|
||||
});
|
||||
if changed {
|
||||
save_observation_store(&store).await?;
|
||||
}
|
||||
|
||||
if let Some(hostname) = hostname {
|
||||
let mut cache = load_mac_name_cache().await.unwrap_or_default();
|
||||
if cache.get(&mac_s).map(|v| v != &hostname).unwrap_or(true) {
|
||||
cache.insert(mac_s, hostname);
|
||||
save_mac_name_cache(&cache).await?;
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(changed)
|
||||
}
|
||||
|
||||
pub async fn observe_neighbor_event(
|
||||
action: &str,
|
||||
mac: Option<MacAddr>,
|
||||
ip: Option<IpAddr>,
|
||||
) -> io::Result<bool> {
|
||||
if !matches!(action, "add" | "update" | "old" | "remove") {
|
||||
return Ok(false);
|
||||
}
|
||||
let Some(key) = mac
|
||||
.map(|value| format!("mac:{}", value))
|
||||
.or_else(|| ip.map(|value| format!("ip:{}", value)))
|
||||
else {
|
||||
return Ok(false);
|
||||
};
|
||||
|
||||
let mut cache = load_mac_name_cache().await.unwrap_or_default();
|
||||
let mac_s = mac.to_string();
|
||||
if cache.get(&mac_s).map(|v| v == hostname).unwrap_or(false) {
|
||||
return Ok(false);
|
||||
let now = now_unix();
|
||||
let mac = mac.map(|value| value.to_string());
|
||||
let mut store = load_observation_store().await.unwrap_or_default();
|
||||
let mut changed = false;
|
||||
store
|
||||
.neighbors
|
||||
.entry(key.clone())
|
||||
.and_modify(|row| {
|
||||
if row.mac != mac
|
||||
|| row.ip != ip
|
||||
|| row.last_action != action
|
||||
|| row.last_seen_unix != now
|
||||
{
|
||||
row.mac = mac.clone();
|
||||
row.ip = ip;
|
||||
row.last_action = action.to_string();
|
||||
row.last_seen_unix = now;
|
||||
changed = true;
|
||||
}
|
||||
})
|
||||
.or_insert_with(|| {
|
||||
changed = true;
|
||||
ObservedNeighbor {
|
||||
key,
|
||||
mac,
|
||||
ip,
|
||||
first_seen_unix: now,
|
||||
last_seen_unix: now,
|
||||
last_action: action.to_string(),
|
||||
}
|
||||
});
|
||||
if changed {
|
||||
save_observation_store(&store).await?;
|
||||
}
|
||||
|
||||
cache.insert(mac_s, hostname.to_string());
|
||||
save_mac_name_cache(&cache).await?;
|
||||
Ok(true)
|
||||
}
|
||||
|
||||
/// Observe a neighbor hotplug event. This is currently a no-op placeholder for
|
||||
/// keeping DHCP and neighbor hook commands symmetrical.
|
||||
pub async fn observe_neighbor_event(
|
||||
_action: &str,
|
||||
_mac: Option<MacAddr>,
|
||||
_ip: Option<IpAddr>,
|
||||
) -> io::Result<bool> {
|
||||
Ok(false)
|
||||
Ok(changed)
|
||||
}
|
||||
|
||||
/// Parse one `dnsmasq`-style DHCP lease line.
|
||||
@@ -88,6 +260,7 @@ pub async fn read_dhcp_leases() -> io::Result<Vec<DhcpLease>> {
|
||||
/// Read DHCP leases and fill missing names from the MAC-name cache.
|
||||
pub async fn read_dhcp_leases_with_names() -> io::Result<Vec<DhcpLease>> {
|
||||
let leases = read_dhcp_leases().await?;
|
||||
let observations = load_observation_store().await.unwrap_or_default();
|
||||
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());
|
||||
@@ -98,6 +271,12 @@ pub async fn read_dhcp_leases_with_names() -> io::Result<Vec<DhcpLease>> {
|
||||
cache.insert(mac_s, name.clone());
|
||||
changed = true;
|
||||
}
|
||||
} else if let Some(prev) = observations
|
||||
.dhcp_clients
|
||||
.get(&mac_s)
|
||||
.and_then(|row| row.hostname.as_ref())
|
||||
{
|
||||
l.name = Some(prev.clone());
|
||||
} else if let Some(prev) = cache.get(&mac_s) {
|
||||
l.name = Some(prev.clone());
|
||||
}
|
||||
@@ -109,6 +288,13 @@ pub async fn read_dhcp_leases_with_names() -> io::Result<Vec<DhcpLease>> {
|
||||
Ok(leases_with_names)
|
||||
}
|
||||
|
||||
fn now_unix() -> u64 {
|
||||
SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.map(|duration| duration.as_secs())
|
||||
.unwrap_or(0)
|
||||
}
|
||||
|
||||
/// Enrich DHCP leases with the best currently known neighbor state per IP.
|
||||
pub async fn enrich_leases_with_nud_state(leases: Vec<DhcpLease>) -> Vec<DhcpLeaseWithState> {
|
||||
let ips: Vec<IpAddr> = leases.iter().map(|l| l.ip).collect();
|
||||
|
||||
Reference in New Issue
Block a user