Files
nixfiles/docs/hosts/pizero2w.md
T
Emma ThorpeandClaude Opus 5 a94a749f29
CI / flake (push) Skipped
CI / flake (pull_request) Successful in 4m16s
feat(hosts): add the Raspberry Pi Zero 2 W Psion sidecar
A headless aarch64 companion for a Psion 5MX: PPP over RS232 with NAT out to
wifi and a telnet login, plus a cleartext POP3/SMTP proxy for the Psion's mail
client.

- hosts/PiZero2W/: host config, serial-ppp.nix, email-proxy.nix, an SD-image
  variant, and a hardware-configuration.nix placeholder.
- Host table entry on nixos-hardware's raspberry-pi-3 profile; the Zero 2 W is
  the Pi 3's BCM2837 SoC. nixpkgs' linuxPackages_rpi02w is deprecated and warns
  that the linux-rpi series is being removed in favour of nixos-hardware.
- The host owns its firmware partition (hardware.raspberry-pi.firmware), which
  is what puts the disable-bt and uart0/ctsrts overlays in config.txt so
  /dev/ttyAMA0 is the RS232 header rather than Bluetooth. uboot.enable keeps the
  U-Boot -> extlinux boot path the rewritten config.txt would otherwise lose.
- packages.aarch64-linux.zero2w-sd-image: the host's own configuration as an
  installable card. The board has no Ethernet and no free serial port, so a
  generic image would leave no way in.
- The mail proxy comes from the legacy-email-proxy flake, which provides the
  package and the NixOS module; nothing about it is vendored here.
- docs/hosts/pizero2w.md, plus README host table and shared-layer notes.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-21 13:38:27 +01:00

206 lines
11 KiB
Markdown

# Raspberry Pi Zero 2 W (`lyrathorpe-zero2w`)
Headless `aarch64-linux` "Psion sidecar": an RS232 companion for a Psion 5MX,
after [Kian Ryan's PPP modem and terminal
write-up](https://www.kianryan.co.uk/2022-11-28-psion-sidecar-ppp-modem-and-terminal/).
Two roles, split into submodules:
- **PPP link + telnet** (`serial-ppp.nix`) — `pppd` on `/dev/ttyAMA0`, the Psion
on the far end of a null-modem cable, NAT out to Wi-Fi, and a telnet login for
the Psion's terminal client.
- **Legacy mail proxy** (`email-proxy.nix`) — cleartext POP3/SMTP for the
Psion's built-in mail client, forwarded to authenticated IMAPS/SMTPS by
[legacy-email-proxy](https://code.emmathe.dev/lyrathorpe/legacy-email-proxy).
That project ships its own package and NixOS module, so `email-proxy.nix`
here is only `services.legacy-email-proxy.enable` plus a path to the
credentials — nothing about the proxy is vendored into this flake.
`sd-image.nix` in the same directory is not part of the running system: it is
the one-shot install card, built as `packages.aarch64-linux.zero2w-sd-image`.
See "Install".
## Hardware and boot
The Zero 2 W is a BCM2837 — the Pi 3's SoC — so the host table uses
`nixos-hardware`'s `raspberry-pi-3` profile for the kernel, firmware and device
tree. Boot is the same U-Boot + extlinux path as the other Pi.
Unlike the Pi 5, this host owns the firmware partition declaratively
(`hardware.raspberry-pi.firmware.enable`): every `switch` rewrites
`/boot/firmware`, including `config.txt`. Two settings there matter:
| `config.txt` | Why |
| ------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------- |
| `dtoverlay=disable-bt` | Moves the PL011 UART off Bluetooth onto GPIO 14/15, so `/dev/ttyAMA0` is the RS232 header. The mini UART (`ttyS0`) drifts at 115200. |
| `dtoverlay=uart0,ctsrts` | RTS/CTS on GPIO 16/17. Both `pppd` and the Psion's modem profile use hardware flow control. |
| `kernel=u-boot.bin` | `hardware.raspberry-pi.firmware.uboot.enable`. Without it the rewritten `config.txt` would have no `kernel=` line and the board would stop booting. |
`gpu_mem=16`, `start_x=0`, `camera_auto_detect=0` and `display_auto_detect=0`
hand the VideoCore the minimum: the board has 512 MB total and no display.
## Never build on the Pi
512 MB of RAM and an SD card. It cannot compile its own system, and there is
deliberately no swap partition (SD cards wear out under swap writes) — zram
takes its place. Build somewhere else and push the result:
```sh
# from a workstation, using another aarch64 machine as the builder
nixos-rebuild switch --flake .#lyrathorpe-zero2w \
--build-host lyrathorpe@lyrathorpe-rpi5 \
--target-host lyrathorpe@<pi-address> --use-remote-sudo
```
The `raspberry-pi-3` profile builds the vendor kernel from source and it is not
in the binary cache, so the first build is long (hours on the Pi 5, less on the
MacBook). Later builds reuse it. The same applies to the SD image below: it
contains that kernel, so it needs an `aarch64-linux` builder too. From an
`x86_64` box or a Mac, that means a remote builder (`nix.buildMachines`) or, on
Darwin, `nix.linux-builder.enable`.
## Install
The card is built from this flake, not downloaded. A generic NixOS image would
boot, but there would be no way into the machine afterwards: it has no Ethernet,
no wifi credentials, and this configuration hands the serial port to `pppd`, so
there is no console either. Building the host's own image sidesteps all three —
the first boot is already the real system, with the SSH key from the registry
in place.
1. **Set the SSID.** `networking.wireless.networks` in `configuration.nix` still
says `CHANGE-ME-SSID`. It is baked into the image at build time; only the PSK
is read at runtime.
2. **Build and write the card.** On an `aarch64-linux` machine (or with one
configured as a builder):
```sh
nix build .#packages.aarch64-linux.zero2w-sd-image
sudo dd if=result/sd-image/nixos-zero2w.img of=/dev/sdX bs=4M conv=fsync status=progress
```
Check `/dev/sdX` twice. `dd` does not ask.
3. **Seed the secrets before first boot.** They are not in the image. Mount the
card's second partition (the ext4 root) and write both files described under
"Secrets" below:
```sh
sudo mount /dev/sdX2 /mnt
sudo mkdir -p /mnt/var/lib/wpa_supplicant /mnt/var/lib/legacy-email-proxy
printf 'psk_home=%s\n' 'the-pre-shared-key' \
| sudo tee /mnt/var/lib/wpa_supplicant/secrets.conf > /dev/null
sudo chmod 600 /mnt/var/lib/wpa_supplicant/secrets.conf
# ... and /mnt/var/lib/legacy-email-proxy/backend.env, same permissions
sudo umount /mnt
```
Skip the PSK and the board boots with no network at all.
4. **Boot it.** Give it a few minutes on first boot — it resizes the root
partition and generates host keys on a slow card. Then:
```sh
ssh lyrathorpe@lyrathorpe-zero2w.local # mDNS; services.avahi publishes it
```
5. **Give the login user a password** (`passwd lyrathorpe`) if you want console
or telnet login; the SSH key from
[`users/registry.nix`](https://code.emmathe.dev/lyrathorpe/nixfiles/src/branch/main/users/registry.nix)
already works without one.
6. Thereafter, rebuild from another machine as in the previous section.
`hosts/PiZero2W/hardware-configuration.nix` is a **placeholder** — but its
layout (`/` on label `NIXOS_SD`, `/boot/firmware` on label `FIRMWARE`) is
exactly what the SD image produces, so there is nothing to regenerate for a card
install. Run `nixos-generate-config` and replace it only if you deviate from
that layout.
If the board never appears on the network, it is almost always the PSK file.
Re-mount the card and check it. Failing that, a mini-HDMI monitor and a
micro-USB keyboard get you a console on `tty1` — the serial port will not,
because `pppd` holds it.
## Secrets (not in the Nix store)
Both files are created on the device, owned by root, mode `0600`. Neither is
managed by this flake; the units that read them fail loudly if they are absent.
**Wi-Fi PSK** — `/var/lib/wpa_supplicant/secrets.conf`:
```
psk_home=<the pre-shared key>
```
The SSID itself _is_ in `configuration.nix` and is currently the placeholder
`CHANGE-ME-SSID`; set it to the real network. `wpa_supplicant` resolves
`pskRaw = "ext:psk_home"` against this file at runtime.
**Mail backend** — `/var/lib/legacy-email-proxy/backend.env`, a systemd
`EnvironmentFile`:
```
BACKEND_IMAP_HOST=imap.example.com
BACKEND_IMAP_USER=someone@example.com
BACKEND_IMAP_PASS=<app password>
BACKEND_SMTP_HOST=smtp.example.com
BACKEND_SMTP_USER=someone@example.com
BACKEND_SMTP_PASS=<app password>
```
Ports and TLS default sensibly (IMAPS 993, SMTPS 465); the full variable list is
in the proxy's README.
### Why POP3 and not IMAP
The Psion's built-in mail client speaks POP only, so POP3 is what the proxy
exposes. If a third-party IMAP client is ever installed on the device, the
answer is **not** to add an IMAP frontend to the proxy: the backend is already
IMAP, so there is no protocol to translate, only TLS to remove. An `stunnel`
client (plaintext 143 on the PPP link, IMAPS 993 outbound) does that in a few
lines with no code, and credentials pass straight through — IMAP clients always
authenticate.
SMTP stays on the proxy either way. A client of this vintage cannot do SMTP
AUTH, which is exactly why the proxy injects the backend credentials.
## Psion configuration
Matches the addressing in `serial-ppp.nix` (`10.0.0.1` the Pi, `10.0.0.2` the
Psion):
- **Modem** control panel, a "Direct Cable Connection" profile: 115200 baud,
Hardware (RTS/CTS) flow control; on the Advanced tab, Terminal Detect and
Carrier Detect both **off**.
- **Internet** control panel, a new profile: Connection Type **Direct**, Manual
Login **True**. Addresses: get IP from server **False**, static **10.0.0.2**.
Get DNS from server **True** — `pppd` sends resolvers over the link
(`ms-dns`), so nothing is hard-coded on the Psion.
- Advanced: PPP extensions **False**, plain-text authentication **True**.
- Terminal client: telnet to **10.0.0.1 port 23**. It renders non-ANSI output
far better than the raw serial console does.
- Mail client: POP3 and SMTP server **10.0.0.1**, no encryption, no
authentication.
## Security
Everything on this host that the Psion talks to is unauthenticated and
unencrypted, because a 1999 palmtop speaks no TLS:
- **telnet on 23** — cleartext login, including the password.
- **POP3 on 110 / SMTP on 25** — full mailbox access and an open relay to anyone
who reaches them.
The confinement is the firewall, and it is the only thing standing there:
`ppp0` is a trusted interface, `wlan0` is not, and those ports are never opened
on it. The proxy binds `0.0.0.0` rather than `10.0.0.1` on purpose — the PPP
address only exists while the Psion is plugged in, and a bind-time dependency on
a serial cable is a restart loop waiting to happen. Do not add these ports to
`networking.firewall.allowedTCPPorts`, and do not put this board on an untrusted
network.
Only sshd (port 22, key-only, via
[`modules/ssh.nix`](https://code.emmathe.dev/lyrathorpe/nixfiles/src/branch/main/modules/ssh.nix))
is reachable over Wi-Fi.
## Troubleshooting
| Symptom | Check |
| ----------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| No PPP at all | `systemctl status pppd-psion`, then `journalctl -u pppd-psion -f` while the Psion dials. `passive`/`persist` mean it waits, not fails. |
| PPP negotiates, then hangs | Flow control. Confirm `dtoverlay=uart0,ctsrts` is in `/boot/firmware/config.txt` and that the Psion's modem profile is set to Hardware. |
| `/dev/ttyAMA0` missing or is a Bluetooth device | `disable-bt` did not apply — the firmware partition was not rewritten. Confirm `/boot/firmware` is a mounted partition; the activation script skips with a warning if it is not. |
| Something else holds the port | `systemctl status serial-getty@ttyAMA0` — it is disabled in `serial-ppp.nix`, and must stay that way. |
| Mail proxy dead | `systemctl status legacy-email-proxy`. A missing `backend.env` fails the unit before it starts. |