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:
lda
2026-04-27 00:55:30 +07:00 Verified
parent 35bed390f7
commit d91b3dab77
55 changed files with 2216 additions and 244 deletions
+1
View File
@@ -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
View File
@@ -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();