Wake-on-LAN: support WoWLAN so Wi-Fi hosts wake like wired ones #187

Merged
enricobuehler merged 1 commits from worktree-wowlan-support into main 2026-08-12 22:52:01 +00:00
Owner

Wake-on-LAN worked on wired hosts only, for two independent reasons — one on the host, one on the client. Both are fixed here; Punktfunk still never arms anything itself.

The host asked the wrong tool about Wi-Fi

warn_if_not_armed ran ethtool <iface> for every NIC. For a Wi-Fi card that is the wrong question: the magic-packet trigger lives in nl80211's WoWLAN state, and most wireless drivers print Wake-on: d whether or not it is armed. So an armed Wi-Fi host was logged as NOT armed, and told to run ethtool -s wlan0 wol g — a command its driver rejects.

A NIC with /sys/class/net/<iface>/phy80211 is now recognised as wireless and asked iw phy <phy> wowlan show instead, with WoWLAN-correct guidance in the warning: iw … wowlan enable magic-packet, plus the NetworkManager 802-11-wireless.wake-on-wlan magic that survives a reconnect. Wired NICs keep the ethtool path unchanged.

Fallbacks for when iw can't answer (missing binary, driver without the command, or privilege a user-level host service may lack):

  • a positive ethtool reading counts — brcmfmac and other SoC Wi-Fi really do report the bit there — but a negative one never does;
  • failing that, sysfs device/power/wakeup reading disabled, which is world-readable and conclusive in the negative: the kernel will not arm the device to wake the machine at all.

The client's broadcast followed the default route

255.255.255.255 from an unbound socket leaves via the default route, which on a machine running a VPN or mesh network is not the LAN the host sleeps on. A station in WoWLAN sleep stays associated and its AP buffers broadcast frames for it until the next DTIM beacon — but only if the datagram reaches the wireless segment at all.

send_magic_packet now sends from a socket bound to each non-loopback interface's own address, aiming that NIC's directed broadcast and the limited broadcast at it. A failed bind falls back to the routed socket, so no segment is lost. The public API is unchanged, so no client needed a rebuild.

Tests

The iw and ethtool output parsers are split from the commands that produce them, so both unit-test on any platform, and a new end-to-end test asserts a real listener receives the 102 magic-packet bytes.

Docs

wake-on-lan.md gains "Over Wi-Fi" and "Linux (Wi-Fi)" sections and documents all four host log lines; the old "Wired Ethernet is what works, Wi-Fi is unreliable" limit is gone, replaced with the honest caveats (some APs drop broadcast to sleeping stations; some adapters lose power in deeper sleep states). troubleshooting.md now says to search logs for Wake-on- rather than Wake-on-LAN, since the Wi-Fi line reads Wake-on-WLAN.

Verification

On Ubuntu 26.04 with 12 interfaces (good coverage for the per-interface bind): cargo fmt --all --check, cargo clippy -p punktfunk-core -p punktfunk-host --all-targets --locked -- -D warnings, and both wol test sets green.

⚠️ Not exercised against real Wi-Fi hardware — no Wi-Fi Linux box was reachable, so the iw parser is tested against synthetic output only. In particular it is still unconfirmed whether iw phy <phy> wowlan show requires root; the sysfs power/wakeup fallback exists precisely for that case, but it wants one run on a real Wi-Fi host.

Not changed

Punktfunk still only reads NIC state — two read-only subprocesses (iw … wowlan show, ethtool <iface> with no -s) and two sysfs reads. It never arms a card; the commands that would are printed for the user to run. The Windows host still runs no arming check at all; its manual steps are documented instead.

Wake-on-LAN worked on wired hosts only, for two independent reasons — one on the host, one on the client. Both are fixed here; Punktfunk still never arms anything itself. ## The host asked the wrong tool about Wi-Fi `warn_if_not_armed` ran `ethtool <iface>` for every NIC. For a Wi-Fi card that is the wrong question: the magic-packet trigger lives in nl80211's WoWLAN state, and most wireless drivers print `Wake-on: d` whether or not it is armed. So an **armed Wi-Fi host was logged as NOT armed**, and told to run `ethtool -s wlan0 wol g` — a command its driver rejects. A NIC with `/sys/class/net/<iface>/phy80211` is now recognised as wireless and asked `iw phy <phy> wowlan show` instead, with WoWLAN-correct guidance in the warning: `iw … wowlan enable magic-packet`, plus the NetworkManager `802-11-wireless.wake-on-wlan magic` that survives a reconnect. Wired NICs keep the ethtool path unchanged. Fallbacks for when `iw` can't answer (missing binary, driver without the command, or privilege a user-level host service may lack): - a **positive** ethtool reading counts — brcmfmac and other SoC Wi-Fi really do report the bit there — but a negative one never does; - failing that, sysfs `device/power/wakeup` reading `disabled`, which is world-readable and conclusive in the negative: the kernel will not arm the device to wake the machine at all. ## The client's broadcast followed the default route `255.255.255.255` from an unbound socket leaves via the default route, which on a machine running a VPN or mesh network is not the LAN the host sleeps on. A station in WoWLAN sleep stays associated and its AP buffers broadcast frames for it until the next DTIM beacon — but only if the datagram reaches the wireless segment at all. `send_magic_packet` now sends from a socket bound to **each** non-loopback interface's own address, aiming that NIC's directed broadcast and the limited broadcast at it. A failed bind falls back to the routed socket, so no segment is lost. The public API is unchanged, so no client needed a rebuild. ## Tests The `iw` and `ethtool` output parsers are split from the commands that produce them, so both unit-test on any platform, and a new end-to-end test asserts a real listener receives the 102 magic-packet bytes. ## Docs `wake-on-lan.md` gains "Over Wi-Fi" and "Linux (Wi-Fi)" sections and documents all four host log lines; the old "Wired Ethernet is what works, Wi-Fi is unreliable" limit is gone, replaced with the honest caveats (some APs drop broadcast to sleeping stations; some adapters lose power in deeper sleep states). `troubleshooting.md` now says to search logs for `Wake-on-` rather than `Wake-on-LAN`, since the Wi-Fi line reads `Wake-on-WLAN`. ## Verification On Ubuntu 26.04 with 12 interfaces (good coverage for the per-interface bind): `cargo fmt --all --check`, `cargo clippy -p punktfunk-core -p punktfunk-host --all-targets --locked -- -D warnings`, and both wol test sets green. ⚠️ **Not exercised against real Wi-Fi hardware** — no Wi-Fi Linux box was reachable, so the `iw` parser is tested against synthetic output only. In particular it is still unconfirmed whether `iw phy <phy> wowlan show` requires root; the sysfs `power/wakeup` fallback exists precisely for that case, but it wants one run on a real Wi-Fi host. ## Not changed Punktfunk still only *reads* NIC state — two read-only subprocesses (`iw … wowlan show`, `ethtool <iface>` with no `-s`) and two sysfs reads. It never arms a card; the commands that would are printed for the user to run. The Windows host still runs no arming check at all; its manual steps are documented instead.
enricobuehler added 1 commit 2026-08-12 22:50:01 +00:00
feat(wol): support WoWLAN so Wi-Fi hosts wake like wired ones
ci / bun-nix (pull_request) Successful in 1m33s
windows / build (aarch64-pc-windows-msvc) (pull_request) Successful in 1m25s
apple / swift (pull_request) Successful in 2m5s
ci / web (pull_request) Successful in 2m14s
apple / screenshots (pull_request) Skipped
ci / rust-arm64 (pull_request) Successful in 2m57s
ci / docs-site (pull_request) Successful in 3m4s
windows / build (x86_64-pc-windows-msvc) (pull_request) Successful in 2m41s
android / android (pull_request) Successful in 7m3s
ci / rust (pull_request) Successful in 10m23s
6774c4e7a2
The host's arming check asked `ethtool` about every NIC, which is the wrong
question for Wi-Fi: the magic-packet trigger lives in nl80211's WoWLAN state,
and most wireless drivers print `Wake-on: d` whether or not it is armed. An
armed Wi-Fi host was therefore told it was NOT armed, and handed an
`ethtool -s wlan0 wol g` its driver rejects. A NIC with an nl80211 phy
(`/sys/class/net/<i>/phy80211`) is now asked `iw phy <phy> wowlan show`
instead, and the warning carries WoWLAN-correct guidance — `iw ... wowlan
enable magic-packet`, plus the NetworkManager
`802-11-wireless.wake-on-wlan magic` that survives a reconnect. Two fallbacks
for when `iw` can't answer (missing binary, driver without the command, or
privilege the user-level host service lacks): a POSITIVE ethtool reading
counts (brcmfmac & co do report there), a negative one never does, and sysfs
`device/power/wakeup` reading `disabled` is conclusive in the negative.

The client sender now emits from a socket bound to EACH non-loopback
interface's own address rather than leaving the path to the routing table. A
station in WoWLAN sleep stays associated and its AP buffers broadcast frames
for it until the next DTIM beacon — but only if the datagram reaches the
wireless segment at all, and with a VPN or mesh interface holding the default
route `255.255.255.255` never did. A failed bind falls back to the routed
socket, so no segment is lost.

Tests: `iw`/`ethtool` output parsing split from the commands so both are unit-
tested on any platform, and a new end-to-end test asserts a real listener
receives the 102 magic-packet bytes.

Verified on Linux (Ubuntu 26.04, 12 interfaces): `cargo fmt --all --check`,
`cargo clippy -p punktfunk-core -p punktfunk-host --all-targets --locked
-- -D warnings`, and both wol test sets green. NOT yet exercised against real
Wi-Fi hardware — no Wi-Fi Linux box was reachable.
enricobuehler scheduled this pull request to auto merge when all checks succeed 2026-08-12 22:51:54 +00:00
enricobuehler merged commit 124cb66324 into main 2026-08-12 22:52:01 +00:00
Sign in to join this conversation.
No Reviewers
No labels
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: unom/punktfunk#187