10 Commits
16 changed files with 445 additions and 29 deletions

No files matched your search

-5
View File
@@ -3,8 +3,3 @@ target = "armv7-unknown-linux-musleabihf"
[target.armv7-unknown-linux-musleabihf] [target.armv7-unknown-linux-musleabihf]
linker = "rust-lld" linker = "rust-lld"
[alias]
ldabr = "b -r --target=target.armv7-unknown-linux-musleabihf"
t = "test -- --nocapture --test-threads=1"
tdebug = "test -- --nocapture --test-threads=1 --show-output"
File renamed without changes.
-2
View File
@@ -1,10 +1,8 @@
you should use block code to refence code, like so: you should use block code to refence code, like so:
```rust ```rust
hello_world!(println); hello_world!(println);
// indented lines // indented lines
``` ```
in the chat. Else it fucks up formatting. in the chat. Else it fucks up formatting.
We work towards clean, maintainable design. im not the best at this thing. We work towards clean, maintainable design. im not the best at this thing.
Generated
+12 -1
View File
@@ -610,6 +610,17 @@ version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
[[package]]
name = "lda-ipjs"
version = "0.0.1"
dependencies = [
"macaddr",
"serde",
"serde_json",
"serde_with",
"strum",
]
[[package]] [[package]]
name = "libc" name = "libc"
version = "0.2.175" version = "0.2.175"
@@ -1179,7 +1190,7 @@ checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65"
[[package]] [[package]]
name = "wakey" name = "wakey"
version = "0.1.6" version = "0.1.5"
dependencies = [ dependencies = [
"axum", "axum",
"axum-extra", "axum-extra",
+6 -9
View File
@@ -1,6 +1,6 @@
[package] [package]
name = "wakey" name = "wakey"
version = "0.1.6" version = "0.1.5"
edition = "2024" edition = "2024"
publish = ["gitea"] publish = ["gitea"]
@@ -16,12 +16,7 @@ serde_json = "1.0.143"
serde_with = { version = "3.14.0", features = ["json"] } serde_with = { version = "3.14.0", features = ["json"] }
strum = { version = "0.27.2", features = ["derive", "strum_macros"] } strum = { version = "0.27.2", features = ["derive", "strum_macros"] }
thiserror = "2.0.16" thiserror = "2.0.16"
tokio = { version = "1.47.1", features = [ tokio = { version = "1.47.1", features = ["fs", "process", "rt-multi-thread", "io-util"] }
"fs",
"process",
"rt-multi-thread",
"io-util",
] }
urlencoding = "2.1.3" urlencoding = "2.1.3"
[profile.release] [profile.release]
@@ -30,5 +25,7 @@ strip = true
[features] [features]
default = ["very-smart-parsing"] default = ["very-smart-parsing"]
very-smart-parsing = [ very-smart-parsing = [] # this is the a-bit-redundant parse thing that copilot made
] # this is the a-bit-redundant parse thing that copilot made
[workspace]
members = ["ipjs"]
+12
View File
@@ -0,0 +1,12 @@
[package]
name = "lda-ipjs"
description = "ip -j show schemas"
version = "0.0.1"
edition = "2024"
[dependencies]
macaddr = { version = "1.0.1", features = ["serde", "serde_std"] }
strum = { version = "0.27.2", features = ["derive", "strum_macros"] }
serde_json = "1.0.143"
serde_with = { version = "3.14.0", features = ["json"] }
serde = { version = "1.0.219", features = ["derive"] }
+19
View File
@@ -0,0 +1,19 @@
//! low
//!
//! # lda-ipj
//!
//! this package will represent all my needs with the all the subcommands of ip -j.
//!
//! ## what i need
//!
//! ```console
//! ip -j neigh
//! ```
//!
//! i also need to see devices and idk MAYBE maybe not MAYBE UHHHHHH maybe broadcast
//!
pub mod neighbor;
pub mod utils;
// i want a generalized way to build and call
+70
View File
@@ -0,0 +1,70 @@
//! ```
//! ip -j n s
//! ```
//!
//! yes. this is a real call.
use crate::utils::serialize::mac::{des_opm, ser_opm};
use std::net::IpAddr;
use macaddr::MacAddr;
use serde::{Deserialize, Serialize};
use strum::{Display, EnumString};
#[derive(Debug, PartialEq, Eq, Clone, Hash, Serialize, Deserialize)]
pub struct NeighborInput {
/// supports only the last item (ignore), `to` keyword is optional
to: IpAddr,
/// supports only one item (it complains)
dev: String, // im all for simplicity
/// takes multiple, has to have `nud` before bro or it will think you `to`
nud: Vec<NUDState>,
}
// as input this must be lowercase. as output it is uppercase
#[derive(Debug, PartialEq, Eq, EnumString, Display, Clone, Copy, Hash, Serialize, Deserialize)]
#[strum(serialize_all = "lowercase", ascii_case_insensitive)]
#[serde(rename_all = "UPPERCASE")]
pub enum NUDState {
/// the neighbour entry is valid forever and can
/// be only be removed administratively.
Permanent,
/// the neighbour entry is valid. No attempts to
/// validate this entry will be made but it can
/// be removed when its lifetime expires.
Noarp,
/// the neighbour entry is valid until the
/// reachability timeout expires.
Reachable,
/// the neighbour entry is valid but suspicious.
/// This option to ip neigh does not change the
/// neighbour state if it was valid and the
/// address is not changed by this command.
Stale,
/// this is a pseudo state used when initially
/// creating a neighbour entry or after trying to
/// remove it before it becomes free to do so.
None,
/// the neighbour entry has not (yet) been
/// validated/resolved.
Incomplete,
/// neighbor entry validation is currently
/// delayed.
Delay,
/// neighbor is being probed.
Probe,
/// max number of probes exceeded without
/// success, neighbor validation has ultimately
/// failed.
Failed,
}
#[derive(Debug, PartialEq, Eq, Clone, Hash, Serialize, Deserialize)]
pub struct NeighborItem {
dst: IpAddr,
dev: String,
#[serde(deserialize_with = "des_opm", serialize_with = "ser_opm")]
lladdr: Option<MacAddr>,
state: Vec<NUDState>,
}
+1
View File
@@ -0,0 +1 @@
pub mod serialize;
+45
View File
@@ -0,0 +1,45 @@
use macaddr::MacAddr;
use serde::{self, Deserialize, Deserializer, de::Error as DeError};
use serde::{Serialize, Serializer, de};
pub fn serialize_macs<S>(macs: &[MacAddr], serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
let strings: Vec<String> = macs.iter().map(|m| m.to_string()).collect();
serde::Serialize::serialize(&strings, serializer)
}
/// Serialize a MacAddr as a string
pub fn serialize_mac<S>(mac: &MacAddr, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
serializer.serialize_str(&mac.to_string())
}
/// Deserialize a MacAddr from a string
pub fn deserialize_mac<'de, D>(deserializer: D) -> Result<MacAddr, D::Error>
where
D: Deserializer<'de>,
{
let s = <String as serde::Deserialize>::deserialize(deserializer)?;
s.parse::<MacAddr>().map_err(DeError::custom)
}
/// serialize an [`Option<MacAddr>`]
pub fn ser_opm<S: Serializer>(bro: &Option<MacAddr>, ser: S) -> Result<S::Ok, S::Error> {
Option::<String>::serialize(&bro.as_ref().map(ToString::to_string), ser)
}
/// deserialize an [`Option<MacAddr>`]
pub fn des_opm<'de, D>(des: D) -> Result<Option<MacAddr>, D::Error>
where
D: serde::Deserializer<'de>,
{
Option::<&str>::deserialize(des)?
.map(str::parse)
.transpose()
.map_err(de::Error::custom)
}
+2
View File
@@ -0,0 +1,2 @@
pub mod mac;
pub mod vec;
+37
View File
@@ -0,0 +1,37 @@
use serde::Deserialize;
use serde::de;
#[derive(Deserialize)]
#[serde(untagged)]
enum OneOrMany<T> {
One(T),
Many(Vec<T>),
}
pub fn vec_from_strs<'de, D, T>(des: D) -> Result<Vec<T>, D::Error>
where
D: serde::Deserializer<'de>,
T: std::str::FromStr,
T::Err: std::fmt::Display,
{
let raw: OneOrMany<String> = OneOrMany::<String>::deserialize(des)?;
let mut out = Vec::new();
match raw {
OneOrMany::One(s) => {
let t = s.trim();
if !t.is_empty() {
out.push(t.parse().map_err(de::Error::custom)?);
}
}
OneOrMany::Many(vs) => {
for s in vs {
let t = s.trim();
if t.is_empty() {
continue;
}
out.push(t.parse().map_err(de::Error::custom)?);
}
}
}
Ok(out)
}
+60
View File
@@ -0,0 +1,60 @@
# taking back my code AHHHH
this is going to be lengthy. ci is spotless but idk how it works cuz powershell. this is good but i cant afford VPN on the router; we will need to do some bullshit.
## overview
we need a:
- small ahh bin in the router.
- forwards a shell and some functions.
- may be shell calling fns maybe some unsafe c. i cant unsafe c.
- big ahh bin in the vps.
- connect to the small ahh bin thru the wide ass internet.
- forwards some APIs.
- static site
- now need no hosts; apis can be called anywhere anyway.
- do offer these in the big ahh bins tho lets not miss out some shit.
- automation
- gitea
- on the vps or the uh idk.
- ABSOLUTELY can auto build main:latest every day / other day.
- and releases.
- router
- a script to pull the tar. from actions artifacts or from releases. (lua or shell yo)
- a script for the bin and the init.d scripts for these two.
- vps
- a systemd service (ima use ai for ts ngl)
- an auto updater. maybe we overkill and dnf. maybe go easy and git clone + cargo install. maybe we pull from release or nightly. idk
i think idk
## router
### current
everything.
we have some functions that call ip neigh a bajillion times.
we have some other functions that read the same file/folder in /sys or sum shit.
we have a function that broadcast to a set ahh address.
we have some routes; and some data structures acting as schema for them ahh. we dont do posts with json but gets with query strings for some ahh reasons.
these are carryover from the early days.
also from the early days are the js.
static was 100% ai with 0 intervention.
we have a search bar of sort that hooks through a smart endpoint that searches for everything.
we have some bullshit. and two buttons to check and wake. that pings `/api/smart/{query}` and `/wake?name=query`. why.
we have the result of the wake and or status and the errors in the first text box.
the list of cliets with names connected to the router the second box.
thats cool.
### changes?
idk changes.
+173
View File
@@ -0,0 +1,173 @@
use macaddr::MacAddr6;
use serde::Deserialize;
use std::{net::IpAddr, process::Command};
// Raw JSON shape from ip -j -4 address show
#[derive(Debug, Deserialize)]
struct IpJAddrInfo {
family: Option<String>,
local: Option<String>,
broadcast: Option<String>,
scope: Option<String>,
label: Option<String>,
prefixlen: Option<u8>,
// many more exist; we only take what we need
}
#[derive(Debug, Deserialize)]
struct IpJAddrEntry {
ifname: String,
// interface MAC address (present on non-loopback):
address: Option<String>,
addr_info: Option<Vec<IpJAddrInfo>>,
}
#[derive(Debug, Clone)]
pub struct AddrInfo {
pub local: IpAddr,
pub broadcast: Option<IpAddr>,
pub scope: Option<String>,
pub label: Option<String>,
pub prefixlen: Option<u8>,
}
#[derive(Debug, Clone)]
pub struct IpAEntry {
pub ifname: String,
pub mac: Option<MacAddr6>,
pub addr_info: Vec<AddrInfo>,
// IPv4 only (inet)
}
// Run ip -j -4 address show and deserialize
fn read_ip_json() -> Result<Vec<IpJAddrEntry>, Box<dyn std::error::Error>> {
let out = Command::new("ip")
.args(["-j", "-4", "address", "show"])
.output()?;
if !out.status.success() {
return Err(format!("ip exited with {}", out.status).into());
}
let entries: Vec<IpJAddrEntry> = serde_json::from_slice(&out.stdout)?;
Ok(entries)
}
fn parse_ip(s: &str) -> Option<IpAddr> {
s.parse::<IpAddr>().ok()
}
fn parse_mac(s: &str) -> Option<MacAddr6> {
// ip outputs lowercase aa:bb:..., which MacAddr6 can parse
s.parse::<MacAddr6>().ok()
}
// Public: read and convert to a cleaned model
pub fn get_ip_addr_entries() -> Result<Vec<IpAEntry>, Box<dyn std::error::Error>> {
let raw = read_ip_json()?;
let mut out = Vec::new();
for e in raw {
let mac = e.address.as_deref().and_then(parse_mac);
let mut infos = Vec::new();
if let Some(list) = e.addr_info {
for ai in list {
// keep only IPv4
if ai.family.as_deref() != Some("inet") {
continue;
}
if let Some(local) = ai.local.as_deref().and_then(parse_ip) {
let broadcast = ai.broadcast.as_deref().and_then(parse_ip);
infos.push(AddrInfo {
local,
broadcast,
scope: ai.scope,
label: ai.label,
prefixlen: ai.prefixlen,
});
}
}
}
out.push(IpAEntry {
ifname: e.ifname,
mac,
addr_info: infos,
});
}
Ok(out)
}
// Helper: first “good” (global) broadcast on an interface
pub fn broadcast_for_ifname(ifname: &str) -> Result<Option<IpAddr>, Box<dyn std::error::Error>> {
let entries = get_ip_addr_entries()?;
let dev = entries.into_iter().find(|e| e.ifname == ifname);
if let Some(dev) = dev {
// Prefer scope=global with a broadcast; else any broadcast
if let Some(b) = dev
.addr_info
.iter()
.filter(|ai| ai.scope.as_deref() == Some("global"))
.filter_map(|ai| ai.broadcast)
.next()
{
return Ok(Some(b));
}
if let Some(b) = dev.addr_info.iter().filter_map(|ai| ai.broadcast).next() {
return Ok(Some(b));
}
}
Ok(None)
}
// Helper: broadcast for a given interface MAC
pub fn broadcast_for_mac(
mac: MacAddr6,
) -> Result<Option<(String, IpAddr)>, Box<dyn std::error::Error>> {
let entries = get_ip_addr_entries()?;
for e in entries {
if let Some(m) = e.mac {
if m == mac {
// prefer global broadcast
if let Some(b) = e
.addr_info
.iter()
.filter(|ai| ai.scope.as_deref() == Some("global"))
.filter_map(|ai| ai.broadcast)
.next()
{
return Ok(Some((e.ifname, b)));
}
if let Some(b) = e.addr_info.iter().filter_map(|ai| ai.broadcast).next() {
return Ok(Some((e.ifname, b)));
}
}
}
}
Ok(None)
}
// Fallback: if nothing else, use limited broadcast (may be filtered on some networks)
pub fn fallback_broadcast() -> IpAddr {
// 255.255.255.255
IpAddr::from([255, 255, 255, 255])
}
// Convenience: all broadcasts grouped by interface (IPv4)
pub fn all_broadcasts() -> Result<Vec<(String, IpAddr)>, Box<dyn std::error::Error>> {
let Some(r) = Some(12) else {
e.addr_info
.iter()
.filter(|ai| ai.scope.as_deref() == Some("global"))
.filter_map(|ai| ai.broadcast)
};
let entries = get_ip_addr_entries()?;
let mut out = Vec::new();
for e in entries {
for ai in e.addr_info {
if let Some(b) = ai.broadcast {
out.push((e.ifname.clone(), b));
}
}
}
Ok(out)
}
-12
View File
@@ -6,7 +6,6 @@ pub mod status;
pub mod wake; pub mod wake;
use crate::assets; use crate::assets;
use crate::dhcpparse::load_mac_name_cache;
use crate::route::api::ip; use crate::route::api::ip;
use crate::route::api::status_redirect; use crate::route::api::status_redirect;
use crate::route::api::status_smart_redirect; use crate::route::api::status_smart_redirect;
@@ -15,8 +14,6 @@ use crate::route::dhcp::get_dhcp_leases;
use crate::route::status::get_status_json; use crate::route::status::get_status_json;
use crate::route::wake::wake_multi; use crate::route::wake::wake_multi;
use axum::Json;
use axum::response::IntoResponse;
use axum::routing::post; use axum::routing::post;
use axum::{Router, http::header, response::Html, routing::get}; use axum::{Router, http::header, response::Html, routing::get};
@@ -60,13 +57,4 @@ pub fn api_router() -> Router {
.route("/devs", get(devs_router)) .route("/devs", get(devs_router))
.route("/wake", post(wake_multi)) .route("/wake", post(wake_multi))
.route("/ips/{name}", get(ip)) .route("/ips/{name}", get(ip))
.route(
"/mac-cache",
get(|| async {
match load_mac_name_cache().await {
Ok(h) => Json(h).into_response(),
Err(e) => e.to_string().into_response(),
}
}),
)
} }
+8
View File
@@ -47,6 +47,14 @@ pub struct Filters {
pub macs: Vec<MacAddr>, pub macs: Vec<MacAddr>,
} }
#[deprecated = "use ApiError"]
#[skip_serializing_none]
#[derive(Debug, Serialize, Default)]
pub struct StatusError {
pub name: Option<String>,
pub error: String,
}
pub async fn get_status_json( pub async fn get_status_json(
Query(DeviceQuery { Query(DeviceQuery {
name, name,