mobile-linux-push-notifications: flesh out

This commit is contained in:
Colin 2023-12-09 13:17:18 +00:00
parent f865fdd262
commit 83afdc0e47
1 changed files with 79 additions and 38 deletions

View File

@ -4,34 +4,15 @@ description = "how to suspend the application cores without missing VoIP calls o
+++
## DRAFT
- motivation
- battery life -> suspend SoC
- receive messages over modem or WLAN
- modem wakes SoC for calls; WLAN doesn't do anything
- wowlan
- how to enable
- flakiness, race conditions (and: "i'll come back to that")
- notification server
- "why" (so that we can wake only on the "important" packets)
- implementations (ntfy)
- ntfy integrations
- how to: Matrix
- how to: Prosody
- resolving the wowlan race condition
- thoughts and future
- e.g. Chatty wants to implement ntfy support
## Motivation
the more i carry my Pinephone for long stretches, the more i feel limited by battery life. there's a lot one can do to deal with that, and among those options is to reduce idle power draw.
in fact if you use something like [SXMO](https://sxmo.org), it'll keep a charge for the whole day if you were to keep it screen-off in your pocket. it does that by suspending the CPU to RAM, but keeping the modem powered -- great if all you care about is being notified on incoming calls/texts, not so great if you want to be notified by apps running on the CPU.
if you use a desktop environment like [SXMO](https://sxmo.org), it'll keep a charge for the whole day if you were to keep it screen-off in your pocket. it does that by suspending the CPU to RAM, but keeping the modem powered -- great if all you care about is being notified on incoming calls/texts, not so great if you want to be notified by apps running on the CPU.
a typical solution to this is to wake the CPU whenever the system gets an IP packet, yield that to userspace & give your applications a chance to sound a ringer, vibrate, etc, and then suspend again when they're done.
the more you can decide "this packet isn't urgent" without CPU intervention, the more you can avoid waking or even passing it on to the CPU, the more you can extend battery life. and so most systems provide tooling to help you tune IP-based wakeups if you know where to poke.
the more you can decide "this packet isn't urgent" without CPU intervention, the more you can avoid waking the CPU, the more you can extend battery life. and so most systems provide tooling to help you tune IP-based wakeups if you know where to poke.
## Wake on LAN
@ -40,11 +21,11 @@ a.k.a. "Wake-On-Wireless LAN" or "wowlan". [Wake on LAN](https://en.wikipedia.or
this can work on WiFi, too, but on a mobile system you enable this by speaking directly to the WiFi chip, rather than BIOS/UEFI. if you're lucky, `sudo iw phy0 wowlan enable any` will do just that.
then enter sleep (`rtcwake -m mem -s 300` to suspend to RAM for 300 seconds), and from a different device on the same WiFi network, wake your phone with `wol <your_phones_mac_address>` e.g. `wol 02:ba:7c:9c:cc:78` (find the MAC address with `iw dev`).
then enter sleep (`rtcwake -m mem -s 300` to suspend to RAM for 300 seconds), and from a different device on the same LAN, wake your phone with `wol <your_phones_mac_address>` e.g. `wol 02:ba:7c:9c:cc:78` (find the MAC address with `iw dev`).
for the Pinephone, whether or not this works depends on how your kernel is configured. firstly, your kernel needs to know to keep the WiFi chip powered during suspend. second, there's a GPIO routed from the WiFi chip to the main SoC: the WiFi chip toggles this GPIO when it sees the magic packet, and the SoC needs to be told to recognize that as an interrupt source that the kernel can respond to. [megi's](https://xnux.eu/devices/pine64-pinephone.html) kernel does these things, i cannot say about other kernels like Mobian's.
for the Pinephone, whether or not this works depends on how your kernel is configured. firstly, your kernel needs to know to keep the WiFi chip powered during suspend. second, there's a GPIO routed from the WiFi chip to the main SoC: the WiFi chip toggles this GPIO when it sees the magic packet, and the SoC needs to be told to recognize that as an interrupt source that the kernel can respond to. [megi's](https://xnux.eu/devices/pine64-pinephone.html) kernel does these things, i can't say about other kernels like Mobian's.
so if you're on megi's kernel, this should work... at least once if you repeat the process enough. ugh, WiFi and Bluetooth share a lot of the same resources and don't always play nicely together. a quick hack is to just disable bluetooth: `rm /lib/firmware/rtl_bt` (back it up first). there's also a `CONFIG_BT_COEXIST` kernel option you could mess with if you're pursuing a longer-term fix (i don't really use bluetooth, but would appreciate to hear better fixes if anyone pursues them).
so if you're on megi's kernel, this should work... at least once if you repeat the process enough. WiFi and Bluetooth share a lot of the same resources and don't always play nicely together. a quick to get WoL to work reliably is to just disable bluetooth: `rm /lib/firmware/rtl_bt` (back it up first). there's also a `CONFIG_BT_COEXIST` kernel option you could mess with if you're pursuing a longer-term fix (i don't really use bluetooth, but would appreciate to hear better fixes if anyone pursues them).
and now, `wol` should pretty reliably wake the phone. there's a race condition if you try that _during_ the `rtcwake` call instead of waiting a second for it to complete -- i'll address that further down.
@ -57,26 +38,34 @@ for that, things diverge quickly from any sort of standard.
but `wol` actually just sends out an ordinary UDP packet with a specific payload, so getting from there to "wake on TCP traffic sent to port 22", or "wake on TCP traffic sent from IP foo" shouldn't be _technically_ difficult.
the Pinephone's Realtek WiFi chip exposes a programmable pattern-matching facility. just give it a byte string and a mask, and it'll wake on any packet whose bytes within the masked regions match those within the byte string.
so if you know that a packet carrying TCP specifies its destination port at bytes 37-38, it's easy to tell the WiFi chip to "wake on TCP traffic sent to port 22": `sudo iwpriv wlan0 wow_set_pattern pattern=-:-:-:-:-:-:-:-:-:-:-:-:-:-:-:-:-:-:-:-:-:-:-:-:-:-:-:-:-:-:-:-:-:-:-:-:00:16` (fields are hexadecimal, `:` indicates byte boundaries, `-` indicates "i don't care what value this byte is set to").
for example, [IPv4][ipv4-header] [TCP][tcp-header] packets specify their destination port at bytes 37-38. it's easy to tell the WiFi chip to "wake on any TCP packet sent to port 22": `sudo iwpriv wlan0 wow_set_pattern pattern=-:-:-:-:-:-:-:-:-:-:-:-:-:-:-:-:-:-:-:-:-:-:-:-:-:-:-:-:-:-:-:-:-:-:-:-:00:16` (fields are hexadecimal, `:` indicates byte boundaries, `-` indicates "i don't care what value this byte is set to").
do this before the `rtcwake` call, then try `ssh`ing into your phone while it's asleep: with any luck it should wake!
figuring out the byte pattern which achieves what you want is painful, so i've got a script for that [here](TODO). equivalent to the above is `rtl8723cs-wowlan tcp --dest-port 22`. if you've a chat application which is talking over https, you might try `rtl8723cs-wowlan tcp --source-port 443` and see that the phone wakes every time you get a message. but then you'll get spurious wakes if you leave a web browser open, etc. you could use `lsof -i4` to locate the local port(s) your IM app is using and wake more specifically on those. but you'll still hit limits with this approach, especially if you're using a chatty protocol like Matrix, or if you idle in a bunch of channels configured to notify you only on @mention.
figuring out the byte pattern which achieves what you want is painful, so i've got a script for that [here](wowlan-script). equivalent to the above is `rtl8723cs-wowlan tcp --dest-port 22`. if you have a chat application which is talking over https, you might try `rtl8723cs-wowlan tcp --source-port 443` and see that the phone wakes every time you get a message. but then you'll get spurious wakes if you leave a web browser open, etc. you could use `lsof -i4` to locate the local port(s) your IM app is using and wake more specifically on those. but you'll still hit limits with this approach, especially if you're using a chatty protocol like Matrix, or if you idle in a bunch of channels configured to notify you only on @mention.
enter... notification servers!
[ipv4-header]: https://en.wikipedia.org/wiki/Internet_Protocol_version_4#Header
[tcp-header]: https://en.wikipedia.org/wiki/Transmission_Control_Protocol#TCP_segment_structure
[wowlan-script]: TODO
## Notification Servers
when an application wants to sound a notification on iOS or Android when it's not in focus, it's not the application running on the phone which does that, but actually some other server (operated by or for the developer) which tells _Apple's_ server (or Google's) about the notification, and then it reaches your phone. the justification for that is precisely to allow the type of power-saving we're aiming for here, and because that approach is such a privacy nightmare passionate people have created open source alternatives to the official MITM servers.
when an application wants to sound a notification on iOS or Android when it's not in focus, it's not the application running on the phone which does that, but actually some other server (operated by or for the developer) which tells _Apple's_ server (or Google's) about the notification, and then they relay it to your phone. one of the justification for that is precisely to allow the type of power-saving we're aiming for here, but because that approach poses privacy concerns, passionate people have created open source alternatives to the official Apple/Google notification delivery servers.
[UnifiedPush](https://unifiedpush.org/) defines the standards for each confusingly-named portion of this data flow,
and [ntfy](https://ntfy.sh/) provides hosted and self-hosted implementations for all the major components. most mature applications provide a way for you to integrate all this: i'll show how to do this for Matrix-synapse and Prosody (XMPP). both can be configured server-side if you have access to that, or client-side *if your client exposes an option for it*.
[UnifiedPush][UnifiedPush] defines the standards for each confusingly-named portion of this data flow,
and [ntfy][ntfy] provides hosted and self-hosted implementations for all the major components. most mature applications provide a way for you to integrate all this: i'll show how to do this for Matrix-synapse and Prosody (XMPP). both can be configured server-side if you have access to that, or client-side *if your client exposes an option for it*.
but first, let's prove this push notification system in a simpler CLI workflow.
[UnifiedPush]: https://unifiedpush.org/
[ntfy]: https://ntfy.sh
but first, let's prove this push notification system in an application-agnostic CLI workflow.
### Minimal ntfy Workflow
ntfy operates a free-to-use server for the MITM component, so install the CLI package for the publish/subscribe portions and we can test a wake-on-notification setup trivially.
ntfy operates a free-to-use server for the push gateway component (i.e. the part that's normally routed through Apple/Google), so install the CLI package for the publish/subscribe portions and we can test a wake-on-notification setup trivially.
on your phone:
```sh
@ -92,7 +81,7 @@ $ rtl8723cs-wowlan tcp --dest-port 1234
#^ replace 1234 with the port from `lsof`
```
then sleep the phone with `rtcwake -m mem`, and from another computer (or the [ntfy web interface](https://ntfy.sh/app)) send a notification:
then sleep the phone with `rtcwake -m mem`, and from another computer (or the [ntfy web interface][ntfy-web-interface]) send a notification:
```sh
$ ntfy pub TEST_WOWLAN_TOPIC "hello phone"
@ -100,18 +89,20 @@ $ ntfy pub TEST_WOWLAN_TOPIC "hello phone"
if all is well, your phone should awake and the `ntfy sub` command from earlier should have printed "hello phone"!
[ntfy-web-interface]: https://ntfy.sh/app
### Hosting a ntfy instance
this part is **optional**. most applications which speak UnifiedPush provide an option to only send a summary (like "new Matrix message") instead of the actual contents, so you only really need to concern yourself with this if you want the control of self-hosting.
this part is **optional**. most applications which speak UnifiedPush provide an option to only send a summary (like "new Matrix message") instead of the actual contents, so you only really need to concern yourself with this if you want the control of self-hosting or are worried more specifically about metadata leaks.
NixOS ships [options](https://search.nixos.org/options?show=services.ntfy-sh) for hosting your own ntfy. use it like:
NixOS ships [options][nixos-ntfy-options] for hosting your own ntfy. use it like:
```nix
services.ntfy-sh.enable = true;
```
ntfy listens on port 2586 by default, so you can subscribe like `ntfy sub MY_INSTANCE_ADDRESS:2586/TEST_WOWLAN_TOPIC` and you can use a wowlan rule that matches the source port instead of checking `lsof` manually: `rtl8723cs-wowlan tcp --source-port 2586`.
ntfy listens on port 2586 by default, so you can subscribe like `ntfy sub MY_INSTANCE_ADDRESS:2586/TEST_WOWLAN_TOPIC`, and use a wowlan rule that matches the source port instead of checking `lsof` manually: `rtl8723cs-wowlan tcp --source-port 2586`.
however, running it this way allows anyone to use your server. the easy way to overcome this is to treat the topic as a shared secret, and forbid use of any topic except your secret topic. naturally, that only works if you secure the connection so do all this behind a TLS-capable reverse proxy like nginx:
@ -149,16 +140,17 @@ systemd.services.ntfy-sh.preStart = ''
deploy that and subscribe to the https url this time. note the port change to 2587: you still want a unique port against which you can write a wowlan rule, and it's easier to put nginx on a new port than ntfy's default 2586.
if you're thorough, you might notice some spurious wakeups with this setup. ntfy sends keep-alive packets every 45 seconds, but that's no good for us! best is to disable those keep-alives, and only put the phone to sleep for shortish durations (e.g. 10 minutes): i'll revisit the nuances around this near the end of this article.
if you're thorough, you might notice some spurious wakeups with this setup. ntfy sends keep-alive packets every 45 seconds, but that's no good for us! best is to decrease/disable those keep-alives. i'll revisit the topic of keep-alives & sleep durations near the end of this article.
```nix
services.ntfy-sh.settings.keepalive-interval = "30m";
```
[nixos-ntfy-options]: https://search.nixos.org/options?show=services.ntfy-sh
### Synapse (Matrix) ntfy integration
the Synapse Matrix server exposes an API for controlling its push behavior [here](https://spec.matrix.org/unstable/push-gateway-api/).
the Synapse Matrix server exposes an API for controlling its push behavior [here][matrix-push-docs].
some of the larger clients, like Fluffychat, can be seen to host their own push gateways, which they point Synapse to via this API.
presumably one could configure these clients to request a different push gateway (i.e. ntfy.sh, or your own instance from just above),
either in the UI or with an edit to their source code. but if you have CLI admin access to Synapse, it's easier to do this generically from the CLI.
@ -222,10 +214,11 @@ repeat the first query and you'll see both of these listed. to delete the new pu
anyway, put your phone to sleep, have someone send you a message that Synapse would alert you on, and now your phone should awake!
[matrix-push-docs]: https://spec.matrix.org/unstable/push-gateway-api/
### Prosody (XMPP) ntfy integration
Prosody has [mod_cloud_notify](https://modules.prosody.im/mod_cloud_notify) and [XEP-0357](https://xmpp.org/extensions/xep-0357.html), but at the time of writing client support is wanting. easier is to hack it in server-side.
Prosody has [mod_cloud_notify][mod_cloud_notify] and [XEP-0357][XEP-0357], but at the time of writing, client support is wanting. easier is to hack it in server-side.
Prosody has a Lua-based module system, so we can just author our own `mod_ntfy_push`, drop it in the modules directory, and then import it:
@ -241,7 +234,55 @@ TODO: link `mod_ntfy_push.lua` here
i've only authored this module to alert me on jingle calls. one could presumably alert on DMs, MUC messages, or anything more particular by finding the right Prosody hooks (mod_cloud_notify may be an ok guide for that).
[mod_cloud_notify]: https://modules.prosody.im/mod_cloud_notify
[XEP-0357]: https://xmpp.org/extensions/xep-0357.html
### Pinephone Wowlan Race Condition
as alluded, sending a wake packet within about a second of sending the SoC to sleep will fail to wake the system. worse, the SoC will be _stuck_ in the sleep state, not waking on any _future_ wake packets (you can still wake it via the power button, and it should behave correctly during the next sleep cycle). one can imagine lots of ways an edge-triggered interrupt could be misimplemented to cause this type of race, but i wasn't able to diagnose it any more than this.
in light of that type of thing, i'm being cautious and never sleeping the phone for more than 10 minutes at a time (`rtcwake -s 600`).
so if you get a VoIP call, there's something like a 0.2% chance you'll miss it and see a notification for it 10 minutes later.
there's a higher chance than that i miss a call just by forgetting my phone in the other room, so i'm not especially concerned.
but if it bothers you, a workaround exists by coordinating your notification server and your phone such that it knows when your phone is about to enter sleep and delays notifications during that time by a couple seconds to avoid triggering the race. [here's][race-fix] how i do that.
there are other reasons you may want to not sleep for extended durations: NATs. in theory, a TCP connection with no traffic [should remain routable for at least 2 hours][TCP-NAT] -- meaning you could safely sleep for 2 hours and the notification server will still be able to reach you at any point. in practice, i don't know how that holds across all networks. UDP has far lower guarantees ([30s to 120s][UDP-NAT] based on who you ask): consider that if you're using a UDP-based VPN like Wireguard. there's also lots of gotchas around WiFi connection details (moving from one network to another, GTK/encryption rekeying, DHCP lease renewals, etc).
in any case, longer sleep durations give diminishing returns. if you sleep for 600s, and then wake for 15s before going back to sleep, you've already captured 95% of the gains you could get from this sleep method. sleeping for an hour isn't going to get you that much more battery life.
[race-fix]: TODO
[TCP-NAT]: https://datatracker.ietf.org/doc/html/rfc5382#section-5
[UDP-NAT]: https://www.rfc-editor.org/rfc/rfc4787#section-4.3
## Wrapping Up
i only covered the WiFi usecase. the modem also has a line to the CPU and could be made to wake it under similar scenarios. i believe it already _does_ for calls/SMS, but i haven't gotten around to understanding or tuning its behavior for IP traffic (information from anyone who has looked into this would be appreciated!)
i'm not convinced this push notification system will have a place in the long-term future of mobile Linux.
in my setup, i'm not actually surfacing any of the content from the push gateway onto the phone's display. the push gateway could just as well be sending the phone 0-byte packets. i'm just using it to wake the phone and then let the applications learn about new messages/calls and tell the DE about them in their ordinary manner. there are simpler ways to do that.
maybe some XDG spec or Wayland protocol will be developed to let applications tell the OS "incoming data on this TCP connection isn't urgent. but data on this other TCP connection _is_ urgent" and then the OS can program the wowlan rules accordingly, and notification servers have no special place. on the other hand, a lot of messaging systems are having to implement Apple/Google's notification system for their users anyway, so am i describing an actual reduction in complexity there or would it more likely be [yet another standard][xkcd-another-standard] everyone would need to support?
**maybe you want to help determine that future**? if so: Librem's working on their Chatty client, and while i have no special affiliation with them, i'll point out that they're [actively considering][chatty-notifications] how to integrate sleep/push notifications into their client recently.
besides that, KDE is also [looking into][kde-notifications-1] similar [things][kde-notifications-2].
[xkcd-another-standard]: https://xkcd.com/927/
[chatty-notifications]: https://gitlab.gnome.org/World/Chatty/-/issues/846
[ked-notifications-1]: https://www.volkerkrause.eu/2022/11/12/kde-unifiedpush-push-notifications.html
[kde-notifications-2]: https://plasma-mobile.org/2023/05/05/this-month-plasma-mobile/#push-notifications
### Where to Learn More
- Arch Linux wiki has a page on [Wake-on-LAN](https://wiki.archlinux.org/title/Wake-on-LAN)
- official SoC/peripheral documentation can sometimes be found in the [linux-sunxi wiki](https://linux-sunxi.org/Main_Page)
- [this](https://github.com/binwiederhier/ntfy/issues/319) ntfy ticket illuminated the Matrix interactions for me
- i found out about the Pinephone's wowlan second-hand from Peetz0r in [#linux-sunxi](https://irclog.whitequark.org/linux-sunxi/2021-02-18) (IRC)
- i found out about BT_COEXIST issues from Aren in [#sxmo](https://sxmo.org/support) (IRC/Matrix)
- Guido hinted me onto Phosh's plans for wake-on-lan/push notifications in [#phosh](https://matrix.to/#/#phosh:talk.puri.sm) (Matrix)
- i got some hints about XMPP push notification support from xavi in [chat@dino.im](xmpp:chat@dino.im) (XMPP)
- more generally, i keep tabs on app-level and ecosystem developments through:
- [LINMob](https://linmob.net)
- [This Week in Gnome](https://thisweek.gnome.org/)
- [LinuxPhoneApps](https://linuxphoneapps.org/)
- [postmarketOS podcast](https://cast.postmarketos.org/)