doc sweep!

This commit is contained in:
lda
2026-04-06 05:43:37 +07:00 Unverified
parent 67aa50ec5c
commit 79a6157a3a
22 changed files with 188 additions and 23 deletions
+21 -11
View File
@@ -1,8 +1,7 @@
//! ts
//! Typed wrappers for `ip -j address show`.
//!
//! deals with both ip a (addroutput) and ip l (commonoutput)
//!
//! lowk why its free but its indirection and its ass
//! This module is intentionally close to the Linux output shape while still
//! tightening a few fields into more useful Rust types.
pub mod json;
#[cfg(all(unix, feature = "experimental-nl"))]
@@ -16,22 +15,21 @@ use std::net::{IpAddr, Ipv4Addr};
use crate::subcommands::link::OperState;
/// i dont include what i dont know about (almost all ts)
/// One interface row from `ip -j address show`.
#[derive(Serialize, Debug, Deserialize)]
pub struct AddrOutput {
pub ifindex: u32,
pub ifname: String,
/// i imagine UP or DOWN, unknown
/// Interface operational state.
pub operstate: OperState,
// 6 has a serde and the enum doesnt? why. (serializing ts is ass although... im not given an array. they string formatted ts)
#[serde(with = "option_mac", default)]
pub address: Option<MacAddr>,
#[serde(default)] // i wish we have intellisense for this... fuck you metaprogramming
/// Per-address entries attached to this interface.
#[serde(default)]
pub addr_info: Vec<AddrInfo>,
}
// i be copying
// Raw JSON shape from ip -j -4 address show
/// One address entry nested under an interface row.
#[derive(Debug, Deserialize, Serialize)]
pub struct AddrInfo {
pub family: Option<AddressFamily>,
@@ -43,9 +41,9 @@ pub struct AddrInfo {
pub scope: Option<String>,
pub label: Option<String>,
// many more exist; we only take what we need
}
/// Address family used by `ip address` output.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum AddressFamily {
Inet,
@@ -90,6 +88,10 @@ impl<'de> Deserialize<'de> for AddressFamily {
}
}
/// Raw parsed local address plus prefix length.
///
/// This is still a source-shaped type; callers that need a guaranteed usable
/// CIDR should validate that both fields are present.
#[derive(Debug, Default, Clone, Deserialize, Serialize)]
pub struct InterfaceCidr {
pub local: Option<IpAddr>,
@@ -103,37 +105,45 @@ impl InterfaceCidr {
}
impl AddrInfo {
/// Return the parsed local IP address when present.
pub fn local_addr(&self) -> Option<IpAddr> {
self.cidr.local
}
/// Return the parsed prefix length when present.
pub fn prefixlen(&self) -> Option<u8> {
self.cidr.prefixlen
}
/// Return whether this row is IPv4.
pub fn is_ipv4(&self) -> bool {
matches!(self.family, Some(AddressFamily::Inet))
}
/// Return whether this row is IPv6.
pub fn is_ipv6(&self) -> bool {
matches!(self.family, Some(AddressFamily::Inet6))
}
}
impl AddrOutput {
/// Iterate IPv4 address entries.
pub fn ipv4_addrs(&self) -> impl Iterator<Item = &AddrInfo> {
self.addr_info.iter().filter(|info| info.is_ipv4())
}
/// Iterate IPv6 address entries.
pub fn ipv6_addrs(&self) -> impl Iterator<Item = &AddrInfo> {
self.addr_info.iter().filter(|info| info.is_ipv6())
}
}
/// Fetch address data using the default backend.
pub async fn get(dev: Option<&str>) -> anyhow::Result<Vec<AddrOutput>> {
get_with_backend(Backend::Json, dev).await
}
/// Fetch address data using an explicit backend.
pub async fn get_with_backend(
backend: Backend,
dev: Option<&str>,
+4
View File
@@ -11,6 +11,7 @@ use macaddr::MacAddr;
use rtnetlink::packet_route::link::State as NetlinkOperState;
use serde::{Deserialize, Deserializer, Serialize, Serializer};
/// One interface row from `ip -j link show`.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct LinkOutput {
pub ifindex: u32,
@@ -21,6 +22,7 @@ pub struct LinkOutput {
pub address: Option<MacAddr>,
}
/// Operational state of a Linux network interface.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum OperState {
Up,
@@ -100,10 +102,12 @@ impl From<NetlinkOperState> for OperState {
}
}
/// Fetch link rows using the default backend.
pub async fn get(dev: Option<&str>) -> anyhow::Result<Vec<LinkOutput>> {
get_with_backend(Backend::Json, dev).await
}
/// Fetch link rows using an explicit backend.
pub async fn get_with_backend(
backend: Backend,
dev: Option<&str>,
+10 -12
View File
@@ -1,8 +1,4 @@
//! ```bash
//! ip -j n s
//! ```
//!
//! yes. this is a real call.
//! Typed wrappers for `ip -j neigh show`.
pub mod json;
#[cfg(all(unix, feature = "experimental-nl"))]
@@ -16,18 +12,18 @@ use macaddr::MacAddr;
use serde::{Deserialize, Serialize};
use strum::{Display, EnumString};
/// Structured neighbor query input matching the common `ip neigh` flags.
#[derive(Debug, PartialEq, Eq, Clone, Hash, Serialize, Deserialize)]
pub struct NeighborInput {
/// supports only the last item (ignore), `to` keyword is optional
/// Destination address filter. The optional `to` keyword in CLI form is implicit here.
pub to: Option<IpAddr>,
/// supports only one item (it complains if multiple)
pub dev: Option<String>, // im all for simplicity
/// takes multiple, has to have `nud` before bro or it will think you `to`
/// Interface-name filter.
pub dev: Option<String>,
/// Neighbor-state filters.
pub nud: Vec<NUDState>,
}
// as input this must be lowercase. as output it is uppercase
/// docs for items come from a random ahh man website idk
/// Linux neighbor reachability states.
#[derive(
Debug, PartialEq, Eq, EnumString, Display, Clone, Copy, Hash, Serialize, Deserialize, Default,
)]
@@ -70,7 +66,7 @@ pub enum NUDState {
Other(u16),
}
/// everything i see
/// One neighbor row from `ip -j neigh show`.
#[derive(Debug, PartialEq, Eq, Clone, Hash, Serialize, Deserialize)]
pub struct NeighborItem {
#[serde(rename(deserialize = "dst"))]
@@ -83,6 +79,7 @@ pub struct NeighborItem {
pub state: Vec<NUDState>,
}
/// Fetch neighbor rows using the default backend.
pub async fn get(
ip: Option<IpAddr>,
dev: Option<&str>,
@@ -91,6 +88,7 @@ pub async fn get(
get_with_backend(Backend::Json, ip, dev, nud).await
}
/// Fetch neighbor rows using an explicit backend.
pub async fn get_with_backend(
backend: Backend,
ip: Option<IpAddr>,
+13
View File
@@ -5,6 +5,7 @@ use wakey_core::{
WakeTargetResult,
};
/// Legacy status row shape expected by the old `/static` frontend.
#[derive(Debug, Clone, Serialize)]
pub struct LegacyStatusRow {
pub ip: std::net::IpAddr,
@@ -14,6 +15,7 @@ pub struct LegacyStatusRow {
pub state: wakey_core::NeighborState,
}
/// Legacy status response shape expected by the old `/static` frontend.
#[derive(Debug, Clone, Serialize)]
pub struct LegacyStatusResponse {
pub name: Option<String>,
@@ -21,6 +23,7 @@ pub struct LegacyStatusResponse {
pub filters: DeviceFilters,
}
/// Legacy DHCP lease row shape expected by the old `/static` frontend.
#[derive(Debug, Clone, Serialize)]
pub struct LegacyLeaseRow {
pub expires_epoch: u64,
@@ -31,11 +34,13 @@ pub struct LegacyLeaseRow {
pub nud_state: Option<wakey_core::NeighborState>,
}
/// Legacy wake response wrapper expected by the old `/static` frontend.
#[derive(Debug, Clone, Serialize)]
pub struct LegacyWakeResult {
pub result: Vec<LegacyWakeResultRow>,
}
/// Legacy per-target wake row shape.
#[derive(Debug, Clone, Copy, Serialize)]
pub struct LegacyWakeResultRow {
#[serde(flatten)]
@@ -43,6 +48,7 @@ pub struct LegacyWakeResultRow {
pub status: wakey_core::WakeStatus,
}
/// Map legacy-style status rows into the old response shape.
pub fn legacy_status_from_domain(status: Status<NeighborEntry>) -> LegacyStatusResponse {
LegacyStatusResponse {
name: status.name,
@@ -51,6 +57,7 @@ pub fn legacy_status_from_domain(status: Status<NeighborEntry>) -> LegacyStatusR
}
}
/// Project a device inventory into the legacy status response shape.
pub fn legacy_status_from_inventory(
inventory: DeviceInventory,
name: Option<String>,
@@ -68,6 +75,7 @@ pub fn legacy_status_from_inventory(
}
}
/// Convert one neighbor row to the legacy status row shape.
pub fn legacy_status_row(row: NeighborEntry) -> LegacyStatusRow {
LegacyStatusRow {
ip: row.ip,
@@ -77,6 +85,7 @@ pub fn legacy_status_row(row: NeighborEntry) -> LegacyStatusRow {
}
}
/// Project one merged device back into legacy status rows.
pub fn legacy_status_rows_from_device(device: Device) -> Vec<LegacyStatusRow> {
if !device.neighbors.is_empty() {
return device
@@ -107,10 +116,12 @@ pub fn legacy_status_rows_from_device(device: Device) -> Vec<LegacyStatusRow> {
.collect()
}
/// Convert lease rows into the legacy frontend shape.
pub fn legacy_leases_from_domain(leases: Vec<DhcpLeaseWithState>) -> Vec<LegacyLeaseRow> {
leases.into_iter().map(legacy_lease_row).collect()
}
/// Convert one lease row into the legacy frontend shape.
pub fn legacy_lease_row(lease: DhcpLeaseWithState) -> LegacyLeaseRow {
LegacyLeaseRow {
expires_epoch: lease.lease_line.expires_epoch,
@@ -121,12 +132,14 @@ pub fn legacy_lease_row(lease: DhcpLeaseWithState) -> LegacyLeaseRow {
}
}
/// Convert wake results into the legacy frontend shape.
pub fn legacy_wake_from_domain(result: WakeResult) -> LegacyWakeResult {
LegacyWakeResult {
result: result.result.into_iter().map(legacy_wake_row).collect(),
}
}
/// Convert one wake result row into the legacy frontend shape.
pub fn legacy_wake_row(row: WakeTargetResult) -> LegacyWakeResultRow {
LegacyWakeResultRow {
target: row.target,
+3
View File
@@ -7,6 +7,7 @@ use axum::Router;
use tokio::net::TcpListener;
use tower_http::services::ServeDir;
/// Build the temporary HTTP app that serves the legacy API and static frontend.
pub fn http_app(static_root: std::path::PathBuf) -> Router {
Router::new()
.nest("/api", route::api_router())
@@ -20,11 +21,13 @@ pub fn http_app(static_root: std::path::PathBuf) -> Router {
))
}
/// Serve the temporary HTTP app on the provided socket address.
pub async fn serve_http(addr: SocketAddr, static_root: std::path::PathBuf) -> io::Result<()> {
let listener = TcpListener::bind(addr).await?;
axum::serve(listener, http_app(static_root).into_make_service()).await
}
/// Serve the HTTP app using the `static/` directory next to the current executable.
pub async fn serve_http_from_current_exe(addr: SocketAddr) -> io::Result<()> {
let exe = std::env::current_exe()?;
let root = exe
+6
View File
@@ -1,14 +1,19 @@
use anyhow::Result;
use wakey_core::InterfaceSummary;
/// Return interface names only.
///
/// This is the old, lightweight interface listing surface kept for compatibility.
pub async fn list_interfaces() -> Result<Vec<String>> {
Ok(wakey_linux::devices::devs_sorted().await)
}
/// Return condensed interface summaries useful for CLI and wake routing.
pub async fn get_interface_summaries() -> Result<Vec<InterfaceSummary>> {
wakey_linux::devices::list_interface_summaries().await
}
/// Return one named interface summary when present.
pub async fn get_interface_summary(name: &str) -> Result<Option<InterfaceSummary>> {
Ok(get_interface_summaries()
.await?
@@ -16,6 +21,7 @@ pub async fn get_interface_summary(name: &str) -> Result<Option<InterfaceSummary
.find(|iface| iface.ifname == name))
}
/// Resolve a hostname through the local resolver and collect all returned IPs.
pub async fn get_ips(name: impl AsRef<str>) -> Result<Vec<std::net::IpAddr>> {
Ok(wakey_linux::devices::get_ips(name.as_ref())
.await?
+10
View File
@@ -6,11 +6,17 @@ use wakey_core::{
use crate::service::leases::get_leases;
use crate::service::query::resolve_query;
/// Resolve free-form input and return merged devices rather than raw source rows.
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)
}
/// Build a merged device inventory from neighbor-table and DHCP-lease sources.
///
/// This is the current center of gravity for the service layer. Higher-level
/// status and wake flows should prefer deriving from inventory rather than
/// directly from raw Linux source rows.
pub async fn inventory(query: DeviceQuery) -> Result<DeviceInventory> {
let neighbors = wakey_linux::devices::query_status(&query).await?;
let leases = get_leases(wakey_core::LeaseQuery {
@@ -22,6 +28,10 @@ pub async fn inventory(query: DeviceQuery) -> Result<DeviceInventory> {
})
}
/// Merge raw neighbor entries and DHCP leases into device aggregates.
///
/// Identity is currently MAC-first, with an IP-based fallback when a neighbor
/// row does not include a MAC address.
pub fn merge_devices(
neighbors: Vec<NeighborEntry>,
leases: Vec<DhcpLeaseWithState>,
+2
View File
@@ -1,6 +1,7 @@
use anyhow::{Context, Result};
use wakey_core::{DhcpLease, DhcpLeaseWithState, LeaseQuery};
/// Read DHCP leases and optionally enrich them with current neighbor-state data.
pub async fn get_leases(query: LeaseQuery) -> Result<Vec<DhcpLeaseWithState>> {
let leases = wakey_linux::dhcp::read_dhcp_leases_with_names()
.await
@@ -12,6 +13,7 @@ pub async fn get_leases(query: LeaseQuery) -> Result<Vec<DhcpLeaseWithState>> {
}
}
/// Wrap raw DHCP leases in the current service output shape without neighbor state.
pub fn leases_without_state(leases: Vec<DhcpLease>) -> Vec<DhcpLeaseWithState> {
leases
.into_iter()
+12
View File
@@ -1,10 +1,18 @@
use anyhow::Result;
use wakey_core::{DeviceFilters, DeviceQuery, Query, QueryInput};
/// Resolve free-form user input into the legacy `DeviceQuery` filter shape.
///
/// This is the compatibility entrypoint used by CLI and HTTP paths that still
/// speak in terms of `DeviceQuery`.
pub async fn resolve_query(input: impl Into<String>) -> Result<DeviceQuery> {
query_to_device_query(resolve_selector(input).await?)
}
/// Classify one piece of free-form user input into a typed selector.
///
/// The Linux adapter decides whether the input looks like an IP address, MAC,
/// interface name, neighbor state, or plain text.
pub async fn resolve_selector(input: impl Into<String>) -> Result<Query> {
Ok(
match wakey_linux::devices::classify_query(input.into()).await {
@@ -17,6 +25,10 @@ pub async fn resolve_selector(input: impl Into<String>) -> Result<Query> {
)
}
/// Convert the newer selector-oriented `Query` model into a `DeviceQuery`.
///
/// This keeps the old filter-based service and HTTP surfaces working while the
/// internals migrate toward selector- and device-oriented APIs.
pub fn query_to_device_query(query: Query) -> Result<DeviceQuery> {
Ok(match query {
Query::Ip(ip_addr) => DeviceQuery {
+10
View File
@@ -4,8 +4,13 @@ use wakey_core::{Device, DeviceQuery, NeighborEntry, Presence, Status};
use crate::service::inventory::inventory;
use crate::service::query::resolve_query;
/// Service status payload, still expressed in terms of legacy neighbor rows.
pub type StatusResponse = Status<NeighborEntry>;
/// Return status rows derived from the merged device inventory.
///
/// This keeps the old status response shape alive while the underlying model is
/// increasingly device-centered.
pub async fn get_status(query: DeviceQuery) -> Result<StatusResponse> {
let inventory = inventory(query.clone()).await?;
let table = inventory
@@ -20,11 +25,16 @@ pub async fn get_status(query: DeviceQuery) -> Result<StatusResponse> {
})
}
/// Convenience wrapper around [`get_status`] for free-form user input.
pub async fn get_status_for_input(input: impl Into<String>) -> Result<StatusResponse> {
let query = resolve_query(input).await?;
get_status(query).await
}
/// Project a device aggregate back into legacy status rows.
///
/// If the device already has neighbor rows they are reused directly; otherwise a
/// fallback row is synthesized from the best available device data.
pub fn device_to_status_rows(device: &Device) -> Vec<NeighborEntry> {
if !device.neighbors.is_empty() {
return device.neighbors.clone();
+10
View File
@@ -6,6 +6,7 @@ use wakey_core::{WakeResult, WakeTarget};
use crate::service::interfaces::get_interface_summaries;
use crate::service::inventory::resolve_devices;
/// Send Wake-on-LAN packets for already-concrete wake targets.
pub async fn wake_targets(targets: Vec<WakeTarget>) -> Result<WakeResult> {
let result = wakey_linux::wake::wake_many(targets)
.await
@@ -13,11 +14,15 @@ pub async fn wake_targets(targets: Vec<WakeTarget>) -> Result<WakeResult> {
Ok(WakeResult { result })
}
/// Resolve free-form input into wake targets and send the packets.
pub async fn wake_from_query(input: impl Into<String>) -> Result<WakeResult> {
let targets = resolve_wake_targets(input).await?;
wake_targets(targets).await
}
/// Build broadcast wake targets for every broadcast-capable interface.
///
/// This is used by explicit manual wake mode when only a MAC address is supplied.
pub async fn broadcast_wake_targets(mac: MacAddr) -> Result<Vec<WakeTarget>> {
Ok(get_interface_summaries()
.await?
@@ -31,6 +36,7 @@ pub async fn broadcast_wake_targets(mac: MacAddr) -> Result<Vec<WakeTarget>> {
.collect())
}
/// Wake a device explicitly by MAC, optionally targeting a specific IP/broadcast.
pub async fn wake_explicit(mac: MacAddr, ip: Option<IpAddr>) -> Result<WakeResult> {
let targets = match ip {
Some(ip) => vec![WakeTarget {
@@ -42,6 +48,10 @@ pub async fn wake_explicit(mac: MacAddr, ip: Option<IpAddr>) -> Result<WakeResul
wake_targets(targets).await
}
/// Resolve free-form input into concrete wake targets.
///
/// The current resolution strategy fans out one wake target per resolved device IP,
/// using the first known MAC address for that device.
pub async fn resolve_wake_targets(input: impl Into<String>) -> Result<Vec<WakeTarget>> {
let devices = resolve_devices(input).await?;
Ok(devices
+8
View File
@@ -6,6 +6,7 @@ use std::net::IpAddr;
use crate::model::{DhcpLease, NeighborEntry, NeighborState};
use crate::parse::mac;
/// Product-level presence derived from raw neighbor state.
#[derive(Debug, PartialEq, Eq, Clone, Copy, Hash, Serialize, serde::Deserialize, Default)]
#[serde(rename_all = "snake_case")]
pub enum Presence {
@@ -31,12 +32,17 @@ impl From<NeighborState> for Presence {
}
}
/// MAC-first identifier for a device aggregate.
#[derive(Debug, PartialEq, Eq, Clone, Hash, Serialize)]
pub struct DeviceId {
#[serde(with = "mac")]
pub mac: MacAddr,
}
/// Merged view of one discovered network identity.
///
/// This aggregates facts from DHCP leases and neighbor-table rows into a more
/// useful application-level shape.
#[skip_serializing_none]
#[derive(Debug, Clone, Serialize)]
pub struct Device {
@@ -52,6 +58,7 @@ pub struct Device {
}
impl Device {
/// Merge raw neighbor and DHCP facts into one device aggregate.
pub fn from_parts(neighbors: Vec<NeighborEntry>, leases: Vec<DhcpLease>) -> Self {
use std::collections::BTreeSet;
@@ -117,6 +124,7 @@ impl From<u8> for Presence {
}
}
/// Collection of merged discovered devices.
#[derive(Debug, Default, Clone, Serialize)]
pub struct DeviceInventory {
pub devices: Vec<Device>,
+3
View File
@@ -6,6 +6,7 @@ use std::net::IpAddr;
use crate::model::NeighborState;
use crate::parse::mac;
/// One parsed DHCP lease row.
#[derive(Debug, Clone, Serialize)]
pub struct DhcpLease {
pub expires_epoch: u64,
@@ -15,6 +16,7 @@ pub struct DhcpLease {
pub name: Option<String>,
}
/// DHCP lease row plus optional current neighbor-state enrichment.
#[skip_serializing_none]
#[derive(Debug, Clone, Serialize)]
pub struct DhcpLeaseWithState {
@@ -23,6 +25,7 @@ pub struct DhcpLeaseWithState {
pub nud_state: Option<NeighborState>,
}
/// Options for lease retrieval from the service layer.
#[derive(Debug, Default, Clone, Serialize, Deserialize)]
pub struct LeaseQuery {
pub include_state: bool,
+17
View File
@@ -4,23 +4,40 @@ use serde_with::skip_serializing_none;
use crate::parse::mac;
/// A condensed, operator-oriented view of one network interface.
///
/// This is intentionally smaller than the full Linux `ip address show` / `ip link show`
/// payload. It keeps the fields that are currently useful to `wakey`:
/// interface identity, operational state, MAC address, bound addresses, and
/// IPv4 broadcast targets.
#[skip_serializing_none]
#[derive(Debug, Clone, Serialize)]
pub struct InterfaceSummary {
/// Kernel interface index.
pub ifindex: u32,
/// Interface name such as `br-lan`, `eth0`, or `wlan0`.
pub ifname: String,
/// Lowercased operational state such as `up`, `down`, or `unknown`.
pub operstate: String,
#[serde(with = "mac::option_mac")]
/// Link-layer address when one exists.
pub mac: Option<MacAddr>,
/// Interface-bound addresses projected into a smaller usable shape.
pub addrs: Vec<InterfaceAddr>,
}
/// A condensed view of one bound interface address.
#[skip_serializing_none]
#[derive(Debug, Clone, Serialize)]
pub struct InterfaceAddr {
/// Address family such as `inet` or `inet6`.
pub family: Option<String>,
/// CIDR notation such as `192.168.1.1/24`.
pub cidr: Option<String>,
/// IPv4 broadcast target when Linux reports one.
pub broadcast: Option<std::net::Ipv4Addr>,
/// Linux-reported address scope, for example `global` or `link`.
pub scope: Option<String>,
/// Optional Linux label for the address entry.
pub label: Option<String>,
}
+6
View File
@@ -6,6 +6,7 @@ use strum::{Display, EnumString};
use crate::parse::mac;
/// One neighbor-table row, typically derived from `ip neigh` or netlink.
#[skip_serializing_none]
#[derive(Debug, PartialEq, Eq, Clone, Hash, Serialize)]
pub struct NeighborEntry {
@@ -16,6 +17,7 @@ pub struct NeighborEntry {
pub state: NeighborState,
}
/// Linux neighbor reachability state.
#[derive(
Debug, PartialEq, Eq, EnumString, Display, Clone, Copy, Hash, Serialize, Deserialize, Default,
)]
@@ -36,6 +38,7 @@ pub enum NeighborState {
}
impl NeighborState {
/// Lowercase CLI argument form used by `ip neigh`.
pub const fn as_ip_neigh_arg(self) -> &'static str {
match self {
NeighborState::Permanent => "permanent",
@@ -50,6 +53,7 @@ impl NeighborState {
}
}
/// Ordering rank used when choosing the “best” state among multiple rows.
pub const fn rank(self) -> u8 {
match self {
NeighborState::Permanent | NeighborState::Reachable => 5,
@@ -74,6 +78,7 @@ impl Ord for NeighborState {
}
}
/// Errors returned when parsing a text `ip neigh` line.
#[derive(Debug, Display, thiserror::Error)]
pub enum NeighborParseError {
IpWhere,
@@ -83,6 +88,7 @@ pub enum NeighborParseError {
StateParseError(#[from] strum::ParseError),
}
/// Parse one textual `ip neigh` line into a typed neighbor row.
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()?;
+8
View File
@@ -5,6 +5,10 @@ use std::net::IpAddr;
use crate::model::NeighborState;
/// Legacy-compatible query shape used by HTTP and service adapters.
///
/// `name` carries free-form text selection, while `filter` carries explicit
/// machine-readable filters such as IPs, MACs, interfaces, and neighbor states.
#[derive(Debug, Default, Clone, Hash, Deserialize, Serialize)]
pub struct DeviceQuery {
pub name: Option<String>,
@@ -12,6 +16,7 @@ pub struct DeviceQuery {
pub filter: DeviceFilters,
}
/// Explicit device filters for source- and service-level queries.
#[serde_as]
#[derive(Debug, Default, Clone, Hash, Serialize, Deserialize)]
pub struct DeviceFilters {
@@ -29,11 +34,13 @@ pub struct DeviceFilters {
pub macs: Vec<MacAddr>,
}
/// Path helper for routes that receive a single `{name}` segment.
#[derive(Debug, Default, Clone, Hash, Deserialize)]
pub struct NamePath {
pub name: String,
}
/// Low-level classified input used by Linux query classification.
#[derive(Debug)]
pub enum QueryInput {
Ip(IpAddr),
@@ -43,6 +50,7 @@ pub enum QueryInput {
Name(String),
}
/// Higher-level typed selector used by the service layer.
#[derive(Debug, Clone)]
pub enum Query {
Text(String),
+8
View File
@@ -5,6 +5,9 @@ use std::net::IpAddr;
use crate::parse::mac;
/// Concrete Wake-on-LAN destination fields.
///
/// A fully usable target needs both an IP address and a MAC address.
#[skip_serializing_none]
#[derive(Debug, Serialize, Deserialize, Clone, Copy, Hash, PartialEq, Eq)]
pub struct WakeTarget {
@@ -15,6 +18,7 @@ pub struct WakeTarget {
}
impl WakeTarget {
/// Return whether this target has both fields needed to send WoL.
pub const fn is_complete(&self) -> bool {
matches!(
self,
@@ -26,11 +30,13 @@ impl WakeTarget {
}
}
/// Result of waking one or more targets.
#[derive(Debug, Serialize, Clone)]
pub struct WakeResult {
pub result: Vec<WakeTargetResult>,
}
/// Per-target wake result row.
#[skip_serializing_none]
#[derive(Debug, Serialize, Clone, Copy)]
pub struct WakeTargetResult {
@@ -39,6 +45,7 @@ pub struct WakeTargetResult {
pub status: WakeStatus,
}
/// Outcome of trying to wake one target.
#[derive(Debug, Serialize, Clone, Copy, Hash, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum WakeStatus {
@@ -49,6 +56,7 @@ pub enum WakeStatus {
}
impl WakeTargetResult {
/// Construct an incomplete result for a target missing required fields.
pub const fn incomplete(target: WakeTarget) -> Self {
Self {
target,
+14
View File
@@ -3,6 +3,10 @@ use lda_ipjs::subcommands::address;
use std::collections::HashSet;
use wakey_core::{InterfaceAddr, InterfaceSummary};
/// Discover interface names from Linux without requiring `ip`.
///
/// This is the lowest-common-denominator interface listing path and is used for
/// quick existence checks and legacy string-only callers.
pub async fn list_devs() -> HashSet<String> {
fn get_dev() -> HashSet<String> {
let mut devs: HashSet<String> = HashSet::new();
@@ -50,6 +54,15 @@ pub async fn devs_sorted() -> Vec<String> {
v
}
/// Build condensed interface summaries from Linux address data.
///
/// On Unix this prefers the `ipjs` netlink-backed address path; elsewhere it
/// falls back to the JSON command path. The result is not a full `ip address show`
/// dump. It is a smaller projection containing the parts `wakey` currently uses:
/// interface name/index, operstate, MAC, bound addresses, and IPv4 broadcast
/// addresses for Wake-on-LAN delivery.
///
/// Loopback is intentionally excluded.
pub async fn list_interface_summaries() -> Result<Vec<InterfaceSummary>> {
#[cfg(unix)]
let rows = address::nl::get(None)
@@ -91,6 +104,7 @@ pub async fn list_interface_summaries() -> Result<Vec<InterfaceSummary>> {
Ok(out)
}
/// Return whether a named interface exists according to [`list_devs`].
pub async fn has_dev(name: &str) -> bool {
list_devs().await.contains(name)
}
+7
View File
@@ -5,6 +5,7 @@ use std::collections::HashSet;
use std::net::IpAddr;
use wakey_core::{DeviceQuery, NeighborEntry, NeighborState};
/// Resolve a hostname through the local resolver and return all reported IPs.
pub async fn get_ips(machine_name: &str) -> Result<impl Iterator<Item = IpAddr>> {
Ok(tokio::net::lookup_host((machine_name, 0))
.await
@@ -12,6 +13,11 @@ pub async fn get_ips(machine_name: &str) -> Result<impl Iterator<Item = IpAddr>>
.map(|c| c.ip()))
}
/// Query Linux neighbor data and project it into `wakey-core` neighbor rows.
///
/// `machine_names` are resolved first and intersected with any explicit `ips`
/// filter. On Unix this prefers the netlink-backed `ipjs` path; elsewhere it
/// falls back to the JSON command backend.
pub async fn get_neighbors(
machine_names: &[impl AsRef<str>],
ips: &[IpAddr],
@@ -100,6 +106,7 @@ pub async fn get_neighbors(
}
}
/// Convenience wrapper around [`get_neighbors`] using the legacy `DeviceQuery`.
pub async fn query_status(query: &DeviceQuery) -> Result<Vec<NeighborEntry>> {
get_neighbors(
query.name.as_slice(),
+4
View File
@@ -4,6 +4,10 @@ use wakey_core::{NeighborState, QueryInput, parse};
use crate::devices::interfaces::has_dev;
/// Classify one free-form input string into the most specific query variant.
///
/// The current precedence is:
/// IP address, MAC address, neighbor state, interface name, then plain text.
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()) {
+6
View File
@@ -5,6 +5,7 @@ use wakey_core::{DhcpLease, DhcpLeaseWithState};
const MAC_NAME_CACHE: &str = "/tmp/wakey_mac_names.json";
/// 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>> {
match tokio::fs::read_to_string(MAC_NAME_CACHE).await {
Ok(s) => serde_json::from_str(&s).map_err(io::Error::other),
@@ -13,12 +14,14 @@ pub async fn load_mac_name_cache() -> io::Result<std::collections::BTreeMap<Stri
}
}
/// Persist the MAC-to-name cache back 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(())
}
/// Parse one `dnsmasq`-style DHCP lease line.
pub fn parse_dhcp_lease_line(line: &str) -> Option<DhcpLease> {
let mut c = line.split_whitespace();
let expires_epoch: u64 = c.next()?.parse().ok()?;
@@ -33,6 +36,7 @@ pub fn parse_dhcp_lease_line(line: &str) -> Option<DhcpLease> {
})
}
/// Read raw DHCP leases from `/tmp/dhcp.leases`.
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()),
@@ -41,6 +45,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 mut cache = load_mac_name_cache().await.unwrap_or_default();
@@ -64,6 +69,7 @@ pub async fn read_dhcp_leases_with_names() -> io::Result<Vec<DhcpLease>> {
Ok(leases_with_names)
}
/// 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();
let mut map: std::collections::HashMap<IpAddr, wakey_core::NeighborState> =
+6
View File
@@ -5,6 +5,7 @@ use macaddr::MacAddr;
use tokio::net::UdpSocket;
use wakey_core::{WakeStatus, WakeTarget, WakeTargetResult};
/// Wake target with the minimum fields needed to send a magic packet.
#[derive(Debug, Clone, Copy, Hash)]
pub struct CompleteWakeTarget {
pub ip: IpAddr,
@@ -27,6 +28,7 @@ impl TryFrom<WakeTarget> for CompleteWakeTarget {
}
}
/// Send one Wake-on-LAN magic packet to a complete target.
pub async fn wake_one(sock: &UdpSocket, t: CompleteWakeTarget) -> WakeTargetResult {
let mac = t.mac;
let mb = mac.as_bytes();
@@ -60,6 +62,10 @@ pub async fn wake_one(sock: &UdpSocket, t: CompleteWakeTarget) -> WakeTargetResu
}
}
/// Send Wake-on-LAN packets for many targets using one UDP socket.
///
/// Incomplete targets are not rejected with an error; they are returned as
/// `WakeStatus::Incomplete` result rows.
pub async fn wake_many(
targets: impl IntoIterator<Item = WakeTarget>,
) -> io::Result<Vec<WakeTargetResult>> {