The Deck learns a host's wake MAC, so Wake-on-LAN can fire there at all #347

Merged
enricobuehler merged 1 commits from worktree-deck-wol-learn-mac into main 2026-08-19 22:41:37 +00:00
Owner

Fixes #322 — Steam Deck Wake-on-LAN does not work.

Root cause

Every wake gate in the codebase reads !host.mac.is_empty() against the saved record:

  • ConnectPlan::for_host / for_targetwake: settings.auto_wake && !host.mac.is_empty()
  • the console home's can_wake: !online && !h.mac.is_empty()
  • punktfunk wake, which exits 5 when the record has no MAC

That MAC only ever reached the store through trust::learn_mac, fed from the host's mDNS mac TXT — and learn_mac had exactly two callers: the GTK hosts page and the WinUI one.

Neither runs on a Steam Deck. Gaming Mode has only the Decky panel (which drives the headless CLI: discover, hosts list, hosts add, launch) and the console home — and both of those learned the management port alone, never the MAC. So a Deck's records stayed MAC-less forever, every wake gate stayed false, and Wake-on-LAN was skipped in silence: no packet, no error, nothing to show a user. It worked on desktop purely because those two hosts pages learn on every discovery tick.

Ruled out along the way: the magic-packet sender is fine (punktfunk-core::wol, 7/7 tests — per-interface bind, subnet-directed + limited broadcast, unicast, ports 9 and 7), and so is the flatpak sandbox (--share=network). Nothing ever reached them.

The change

Rather than add the missing call twice, the three per-field learners — learn_mac, learn_os, learn_mgmt_port, three pub fns doing three separate load/save cycles — collapse into one trust::learn_from_advert, called at every site where an advert meets a saved record:

Site Before After
clients/linux/src/ui_hosts.rs (GTK) 3 advert scans, 3 calls 1
clients/windows/src/app/hosts.rs (WinUI) 3 advert scans, 3 calls 1
clients/session/src/console.rs (console home) mgmt port only 1
clients/cli/src/main.rs (discover) nothing 1

Remembering one call is not something a front-end can half-do; remembering three is precisely what produced this bug. It takes the three fields rather than a DiscoveredHost because there are two of those — core's and the WinUI shell's verbatim port — and one function has to serve both.

discover is where the panel-only flow is fixed, since it is the one verb the Decky panel runs that ever sees an advert. It keeps KnownHosts::read(), so it still mints no ids and cannot join the race that comment warns about, and learn_from_advert writes only when an advert genuinely taught the record something — a steady-state panel refresh touches no disk.

Falling out of the same root cause

  • The console home now persists the OS chain too, so a Deck host's icon stops vanishing the moment mDNS goes quiet (it was reading the advert with a fallback to h.os, which on a Deck was always empty).
  • punktfunk wake's "connect to it once while it's awake so the client can learn it" is replaced. A MAC comes from an advert and never from a connect — that wording is what sent this diagnosis looking in the wrong place.

Verification

  • cargo clippy -p pf-client-core -p punktfunk-cli -p punktfunk-client-session -p punktfunk-client-linux --all-targets --locked -- -D warnings — clean, in the punktfunk-rust-ci image (macOS compiles none of this code).
  • cargo test -p pf-client-core --lib trust:: — 25/25, including the new apply_advert_learns_what_it_carries_and_keeps_what_it_omits: what an advert carries lands, a repeat reports no change (the no-disk-write property every per-tick caller depends on), and — the one that would actually cost a user their wake — a field the advert omits never clears a learned one.
  • cargo fmt --all --check — clean.

Not verified locally: clients/windows needs an MSVC target. Its change is mechanically identical to the GTK one and the re-export list in clients/windows/src/trust.rs is updated, but it rides on CI's Windows leg — worth a look at that job.

Known limit, left alone deliberately: a Deck learns the MAC on the next discovery after saving a host, not at save time. That is inherent — a sleeping host does not advertise — and matches desktop behaviour, so no hosts add --mac path was added for it.

Fixes #322 — Steam Deck Wake-on-LAN does not work. ## Root cause Every wake gate in the codebase reads `!host.mac.is_empty()` against the **saved record**: - `ConnectPlan::for_host` / `for_target` — `wake: settings.auto_wake && !host.mac.is_empty()` - the console home's `can_wake: !online && !h.mac.is_empty()` - `punktfunk wake`, which exits 5 when the record has no MAC That MAC only ever reached the store through `trust::learn_mac`, fed from the host's mDNS `mac` TXT — and `learn_mac` had exactly **two** callers: the GTK hosts page and the WinUI one. Neither runs on a Steam Deck. Gaming Mode has only the Decky panel (which drives the headless CLI: `discover`, `hosts list`, `hosts add`, `launch`) and the console home — and both of those learned the *management port* alone, never the MAC. So a Deck's records stayed MAC-less forever, every wake gate stayed false, and Wake-on-LAN was skipped in silence: no packet, no error, nothing to show a user. It worked on desktop purely because those two hosts pages learn on every discovery tick. Ruled out along the way: the magic-packet sender is fine (`punktfunk-core::wol`, 7/7 tests — per-interface bind, subnet-directed + limited broadcast, unicast, ports 9 and 7), and so is the flatpak sandbox (`--share=network`). Nothing ever reached them. ## The change Rather than add the missing call twice, the three per-field learners — `learn_mac`, `learn_os`, `learn_mgmt_port`, three `pub fn`s doing three separate load/save cycles — collapse into one `trust::learn_from_advert`, called at **every** site where an advert meets a saved record: | Site | Before | After | |---|---|---| | `clients/linux/src/ui_hosts.rs` (GTK) | 3 advert scans, 3 calls | 1 | | `clients/windows/src/app/hosts.rs` (WinUI) | 3 advert scans, 3 calls | 1 | | `clients/session/src/console.rs` (console home) | mgmt port only | 1 | | `clients/cli/src/main.rs` (`discover`) | nothing | 1 | Remembering one call is not something a front-end can half-do; remembering three is precisely what produced this bug. It takes the three fields rather than a `DiscoveredHost` because there are two of those — core's and the WinUI shell's verbatim port — and one function has to serve both. **`discover` is where the panel-only flow is fixed**, since it is the one verb the Decky panel runs that ever sees an advert. It keeps `KnownHosts::read()`, so it still mints no ids and cannot join the race that comment warns about, and `learn_from_advert` writes only when an advert genuinely taught the record something — a steady-state panel refresh touches no disk. ## Falling out of the same root cause - The console home now persists the **OS chain** too, so a Deck host's icon stops vanishing the moment mDNS goes quiet (it was reading the advert with a fallback to `h.os`, which on a Deck was always empty). - `punktfunk wake`'s *"connect to it once while it's awake so the client can learn it"* is replaced. A MAC comes from an advert and never from a connect — that wording is what sent this diagnosis looking in the wrong place. ## Verification - `cargo clippy -p pf-client-core -p punktfunk-cli -p punktfunk-client-session -p punktfunk-client-linux --all-targets --locked -- -D warnings` — clean, in the `punktfunk-rust-ci` image (macOS compiles none of this code). - `cargo test -p pf-client-core --lib trust::` — 25/25, including the new `apply_advert_learns_what_it_carries_and_keeps_what_it_omits`: what an advert carries lands, a repeat reports no change (the no-disk-write property every per-tick caller depends on), and — the one that would actually cost a user their wake — a field the advert omits never clears a learned one. - `cargo fmt --all --check` — clean. **Not verified locally:** `clients/windows` needs an MSVC target. Its change is mechanically identical to the GTK one and the re-export list in `clients/windows/src/trust.rs` is updated, but it rides on CI's Windows leg — worth a look at that job. **Known limit, left alone deliberately:** a Deck learns the MAC on the *next* discovery after saving a host, not at save time. That is inherent — a sleeping host does not advertise — and matches desktop behaviour, so no `hosts add --mac` path was added for it.
enricobuehler added 1 commit 2026-08-19 22:35:20 +00:00
fix(clients): the Deck learns a host's wake MAC, so Wake-on-LAN can fire there at all
ci / bun-nix (pull_request) Successful in 27s
ci / web (pull_request) Successful in 1m33s
ci / docs-site (pull_request) Successful in 1m41s
ci / rust-arm64 (pull_request) Successful in 1m58s
windows-client / client (arm64, --no-default-features, aarch64-pc-windows-msvc, C:\t-a64) (pull_request) Successful in 3m22s
ci / rust (pull_request) Successful in 5m19s
android / android (pull_request) Successful in 7m21s
windows-client / client (x64, , x86_64-pc-windows-msvc, C:\t) (pull_request) Successful in 6m43s
2b81bd286f
Every wake gate in the codebase reads `!host.mac.is_empty()` against the saved
record — `ConnectPlan::wake`, the console's `can_wake`, `punktfunk wake`. That MAC
only ever reached the store through `trust::learn_mac`, and `learn_mac` had exactly
two callers: the GTK hosts page and the WinUI one.

Neither runs on a Steam Deck. Gaming Mode has only the Decky panel (which drives the
headless CLI) and the console home — and those learned the management port alone,
never the MAC. So a Deck's records stayed MAC-less forever, every wake gate stayed
false, and Wake-on-LAN was skipped silently: no packet, no error, nothing to see.
It worked on desktop purely because those two hosts pages learn on each discovery
tick. (#322)

Rather than add the missing call twice, collapse the three per-field learners
(`learn_mac`, `learn_os`, `learn_mgmt_port` — three `pub fn`s, three load/save
cycles) into one `learn_from_advert`, and call it at every site where an advert
meets a saved record: both desktop hosts pages, the console home, and the CLI's
`discover`. Remembering one call is not a thing a front-end can half-do; remembering
three is what produced this. It takes the three fields rather than a `DiscoveredHost`
because there are two of those — core's and the WinUI shell's verbatim port.

`discover` is where the panel-only flow is fixed: it is the one verb the Decky panel
runs that ever sees an advert. It keeps `KnownHosts::read()`, so it still mints no
ids and cannot join the race that comment warns about, and `learn_from_advert` writes
only when an advert genuinely taught the record something — a steady-state panel
refresh touches no disk.

Two things fall out of the same root cause: the console home now persists the OS
chain too, so a Deck host's icon stops vanishing the moment mDNS goes quiet; and
`punktfunk wake`'s "connect to it once while it's awake" is replaced, since a MAC
comes from an advert and never from a connect — that wording sent this diagnosis
looking in the wrong place.

The magic-packet sender itself was never at fault (`punktfunk-core::wol` passes its
7 tests) and neither was the flatpak sandbox (`--share=network`). Nothing reached
them.

Closes #322
enricobuehler merged commit 230d253b06 into main 2026-08-19 22:41:37 +00:00
enricobuehler deleted branch worktree-deck-wol-learn-mac 2026-08-19 22:41:44 +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#347