doc sweep!
This commit is contained in:
@@ -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>,
|
||||
|
||||
@@ -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>,
|
||||
|
||||
@@ -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>,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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?
|
||||
|
||||
@@ -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>,
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,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()?;
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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,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()) {
|
||||
|
||||
@@ -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> =
|
||||
|
||||
@@ -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>> {
|
||||
|
||||
Reference in New Issue
Block a user