7e6561aaa2
ci / rust (push) Failing after 51s
ci / web (push) Successful in 53s
windows-host / package (push) Failing after 2m54s
apple / swift (push) Successful in 1m19s
ci / docs-site (push) Successful in 1m10s
android / android (push) Successful in 3m38s
windows-msix / package (arm64, C:\Users\Public\ffmpeg-arm64, aarch64-pc-windows-msvc, C:\t-a64) (push) Successful in 1m21s
windows-msix / package (x64, C:\Users\Public\ffmpeg, x86_64-pc-windows-msvc, C:\t) (push) Successful in 1m11s
windows / build (aarch64-pc-windows-msvc) (push) Failing after 39s
windows / build (x86_64-pc-windows-msvc) (push) Failing after 41s
ci / bench (push) Successful in 4m48s
decky / build-publish (push) Successful in 13s
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Successful in 5s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 5s
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 3s
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 4s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 4s
release / apple (push) Successful in 8m47s
deb / build-publish (push) Successful in 9m26s
flatpak / build-publish (push) Successful in 4m44s
apple / screenshots (push) Successful in 5m56s
rpm / build-publish (fedora-44, punktfunk-fedora44-rpm) (push) Has been cancelled
rpm / build-publish (bazzite, punktfunk-fedora-rpm) (push) Has been cancelled
docker / deploy-docs (push) Successful in 17s
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
115 lines
4.4 KiB
Rust
115 lines
4.4 KiB
Rust
//! Host-side Wake-on-LAN support.
|
|
//!
|
|
//! Two jobs, both best-effort (a failure here never affects streaming):
|
|
//! 1. [`wake_macs`] — report the host's wake-capable NIC MAC(s) so a client can persist them
|
|
//! (from the mDNS `mac` TXT record, [`crate::discovery`]) and wake this host later, once it's
|
|
//! asleep and no longer advertising.
|
|
//! 2. [`warn_if_not_armed`] — *detect & warn only* whether the NIC is actually armed to wake on a
|
|
//! magic packet. We never change NIC settings (that's the user's call); we just surface the
|
|
//! single most common reason WoL silently fails.
|
|
|
|
use std::net::IpAddr;
|
|
|
|
/// Upper bound on advertised MACs — keeps the mDNS TXT record small. A host has at most a couple
|
|
/// of wake-capable NICs; the routed one is always first.
|
|
const MAX_MACS: usize = 4;
|
|
|
|
/// MAC(s) of the host's wake-capable NIC(s), lowercase `aa:bb:cc:dd:ee:ff`, with the NIC that
|
|
/// bears `primary_ip` (the address clients reach us on) FIRST, then other non-loopback NICs as
|
|
/// fallbacks. Best-effort — an empty list just means clients can't auto-wake (they fall back to
|
|
/// manual MAC entry). Deduped; all-zero MACs skipped; capped at [`MAX_MACS`].
|
|
pub fn wake_macs(primary_ip: IpAddr) -> Vec<String> {
|
|
let ifaces = if_addrs::get_if_addrs().unwrap_or_default();
|
|
|
|
// Interface names in priority order: the one holding `primary_ip` first, then every other
|
|
// non-loopback interface that has an IP, de-duplicated by name (an iface has one MAC but may
|
|
// appear once per address).
|
|
let mut names: Vec<String> = Vec::new();
|
|
if let Some(primary) = ifaces.iter().find(|i| i.ip() == primary_ip) {
|
|
names.push(primary.name.clone());
|
|
}
|
|
for i in &ifaces {
|
|
if i.is_loopback() {
|
|
continue;
|
|
}
|
|
if !names.contains(&i.name) {
|
|
names.push(i.name.clone());
|
|
}
|
|
}
|
|
|
|
let mut out: Vec<String> = Vec::new();
|
|
for name in names {
|
|
let Ok(Some(mac)) = mac_address::mac_address_by_name(&name) else {
|
|
continue;
|
|
};
|
|
let b = mac.bytes();
|
|
if b == [0u8; 6] {
|
|
continue; // unset / virtual
|
|
}
|
|
let s = format!(
|
|
"{:02x}:{:02x}:{:02x}:{:02x}:{:02x}:{:02x}",
|
|
b[0], b[1], b[2], b[3], b[4], b[5]
|
|
);
|
|
if !out.contains(&s) {
|
|
out.push(s);
|
|
}
|
|
if out.len() >= MAX_MACS {
|
|
break;
|
|
}
|
|
}
|
|
out
|
|
}
|
|
|
|
/// Log whether the host NIC bearing `primary_ip` is armed to wake on a magic packet. Detect &
|
|
/// warn only — never modifies settings. Linux-only (reads `ethtool <iface>`); a no-op elsewhere
|
|
/// and silent when it can't tell (no `ethtool`, insufficient privilege).
|
|
#[cfg(target_os = "linux")]
|
|
pub fn warn_if_not_armed(primary_ip: IpAddr) {
|
|
let ifaces = if_addrs::get_if_addrs().unwrap_or_default();
|
|
let Some(iface) = ifaces
|
|
.iter()
|
|
.find(|i| i.ip() == primary_ip)
|
|
.map(|i| i.name.clone())
|
|
else {
|
|
return;
|
|
};
|
|
match ethtool_wol_has_magic(&iface) {
|
|
Some(true) => {
|
|
tracing::info!(iface = %iface, "Wake-on-LAN armed (magic packet) on host NIC")
|
|
}
|
|
Some(false) => tracing::warn!(
|
|
iface = %iface,
|
|
"Wake-on-LAN is NOT armed on this host's NIC — clients cannot wake it from sleep. \
|
|
Enable it with: sudo ethtool -s {iface} wol g (and turn on 'Wake on LAN'/'Wake on \
|
|
PCIe' in BIOS). Wired Ethernet is required; Wi-Fi wake is unreliable.",
|
|
),
|
|
None => {} // couldn't determine — stay quiet rather than cry wolf
|
|
}
|
|
}
|
|
|
|
#[cfg(not(target_os = "linux"))]
|
|
pub fn warn_if_not_armed(_primary_ip: IpAddr) {}
|
|
|
|
/// Parse `ethtool <iface>` for the *current* Wake-on setting and report whether it includes `g`
|
|
/// (wake on MagicPacket). Returns `None` if ethtool is missing/failed or the field is absent.
|
|
#[cfg(target_os = "linux")]
|
|
fn ethtool_wol_has_magic(iface: &str) -> Option<bool> {
|
|
let out = std::process::Command::new("ethtool")
|
|
.arg(iface)
|
|
.output()
|
|
.ok()?;
|
|
if !out.status.success() {
|
|
return None;
|
|
}
|
|
let text = String::from_utf8_lossy(&out.stdout);
|
|
for line in text.lines() {
|
|
let t = line.trim();
|
|
// The current setting is "Wake-on: <flags>"; skip the "Supports Wake-on: ..." capability
|
|
// line. `g` = MagicPacket, `d` = disabled.
|
|
if let Some(flags) = t.strip_prefix("Wake-on:") {
|
|
return Some(flags.trim().contains('g'));
|
|
}
|
|
}
|
|
None
|
|
}
|