uninsane/content/blog/DRAFT-2023-11-06-mobile-lin.../index.md

10 KiB

+++ title = "Wake-on-LAN and Push Notifications in Mobile Linux" description = "how to suspend the application cores without missing VoIP calls or messages" +++

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, 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.

Wake on LAN

a.k.a. "Wake-On-Wireless LAN" or "wowlan". Wake on LAN is an actual established standard for remotely waking one ethernet device from another device on the same LAN. the user or OS enables this feature in the BIOS, the PC suspends, some other device sends a specially crafted packet (a "magic packet"), magic happens, and the BIOS wakes the system.

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).

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 kernel does these things, i cannot 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).

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.

Wowlan for Userspace

you're probably not interested in manually calling wol from some other computer on the same network to wake your phone. you'd probably prefer it to wake anytime you get a Matrix or XMPP message, or something. 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").

do this before the rtcwake call, then try sshing 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. 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.

enter... notification servers!

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.

UnifiedPush defines the standards for each confusingly-named portion of this data flow, and 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 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.

on your phone:

$ ntfy sub TEST_WOWLAN_TOPIC

then determine the local port that ntfy process connected from with lsof -i4 -P, and create a wake condition for it:

# clear out previous rules
$ rtl8723cs-wowlan enable-clean
$ 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) send a notification:

$ 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"!

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.

NixOS ships options for hosting your own ntfy. use it like:

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.

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:

services.ntfy-sh.enable = true;

services.nginx.virtualHosts."MY.HOST" = {
  forceSSL = true;
  listen = [
    { addr = "0.0.0.0"; port = 2587; ssl = true; }
    { addr = "0.0.0.0"; port = 443;  ssl = true; }
  ];
  locations."/" = {
    proxyPass = "http://127.0.0.1:2586";
    proxyWebsockets = true;  #< support websocket upgrades. without that, `ntfy sub` hangs silently
    recommendedProxySettings = true; #< adds headers so ntfy logs include the real IP
    extraConfig = ''
      # absurdly long timeout (86400s=24h) so that we never hang up on clients.
      proxy_read_timeout 86400s;
    '';
  };
};

services.ntfy-sh.settings = {
  base-url = "https://MY.HOST";
  behind-proxy = true;
  auth-default-access = "deny-all";
};
systemd.services.ntfy-sh.preStart = ''
  # note that this will fail upon first run, i.e. before ntfy has created its db.
  # just restart the service.
  ${pkgs.ntfy-sh}/bin/ntfy access everyone TEST_WOWLAN_TOPIC read-write
'';

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.

services.ntfy-sh.settings.keepalive-interval = "30m";