# 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@ --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 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= BACKEND_SMTP_HOST=smtp.example.com BACKEND_SMTP_USER=someone@example.com BACKEND_SMTP_PASS= ``` 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. |