diff --git a/README.md b/README.md index bb8fc52..e865594 100644 --- a/README.md +++ b/README.md @@ -7,22 +7,23 @@ single flake. Defined in the host table in [`flake.nix`](https://code.emmathe.dev/lyrathorpe/nixfiles/src/branch/main/flake.nix): -| Configuration | System | Machine | -| --------------------- | ---------------- | ---------------------------------------------------------------------------------------------------------------------------------- | -| `lyrathorpe-mbp` | `aarch64-linux` | MacBook Pro (Apple Silicon, Asahi) | -| `lyrathorpe-t400` | `x86_64-linux` | ThinkPad T400 — [install notes](https://docs.lyrapup.pet/nixfiles/hosts/t400/) | -| `lyrathorpe-macpro31` | `x86_64-linux` | Mac Pro 3,1, desktop — [install notes](https://docs.lyrapup.pet/nixfiles/hosts/macpro31/) | -| `emmathorpe-edaas` | `x86_64-linux` | Work WSL box (NixOS-WSL) — [notes](https://docs.lyrapup.pet/nixfiles/hosts/edaas/) | -| `lyrathorpe-rpi5` | `aarch64-linux` | Raspberry Pi 5 headless server: Docker host + nginx reverse proxy — [install notes](https://docs.lyrapup.pet/nixfiles/hosts/rpi5/) | -| `lyrathorpe-mac` | `aarch64-darwin` | macOS (nix-darwin) — [notes](https://docs.lyrapup.pet/nixfiles/hosts/darwin/) | +| Configuration | System | Machine | +| --------------------- | ---------------- | ---------------------------------------------------------------------------------------------------------------------------------------------- | +| `lyrathorpe-mbp` | `aarch64-linux` | MacBook Pro (Apple Silicon, Asahi) | +| `lyrathorpe-t400` | `x86_64-linux` | ThinkPad T400 — [install notes](https://docs.lyrapup.pet/nixfiles/hosts/t400/) | +| `lyrathorpe-macpro31` | `x86_64-linux` | Mac Pro 3,1, desktop — [install notes](https://docs.lyrapup.pet/nixfiles/hosts/macpro31/) | +| `emmathorpe-edaas` | `x86_64-linux` | Work WSL box (NixOS-WSL) — [notes](https://docs.lyrapup.pet/nixfiles/hosts/edaas/) | +| `lyrathorpe-rpi5` | `aarch64-linux` | Raspberry Pi 5 headless server: Docker host + nginx reverse proxy — [install notes](https://docs.lyrapup.pet/nixfiles/hosts/rpi5/) | +| `lyrathorpe-zero2w` | `aarch64-linux` | Raspberry Pi Zero 2 W "Psion sidecar": PPP over RS232 + legacy mail proxy — [install notes](https://docs.lyrapup.pet/nixfiles/hosts/pizero2w/) | +| `lyrathorpe-mac` | `aarch64-darwin` | macOS (nix-darwin) — [notes](https://docs.lyrapup.pet/nixfiles/hosts/darwin/) | Shared layers: `home` (home-manager: shell, git, editor), `modules/common-nixos.nix` (all NixOS hosts: fonts, nix-ld, caches), `modules/workstation.nix` (physical graphical hosts: audio, thermald, earlyoom, fwupd), `modules/laptop.nix` (laptops: Wi-Fi, Bluetooth, power, lid), `modules/desktop.nix` (wired desktops: NetworkManager), and -`modules/ssh.nix` (key-only sshd). The x86 hosts also pull `nixos-hardware` -profiles. The full module catalogue is below. +`modules/ssh.nix` (key-only sshd). The x86 hosts and both Raspberry Pis also +pull `nixos-hardware` profiles. The full module catalogue is below. ## Repository layout @@ -56,17 +57,17 @@ module reaches a host: **baseModules** (every NixOS host, via `flake.nix`), **host table** (listed explicitly per host in `flake.nix`), or **transitively** (pulled in by another module's `imports`). -| Module | Imported by | What it does / when to use it | -| ------------------ | --------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `common-nixos.nix` | baseModules (all NixOS) | Timezone/locale, store hygiene (auto-optimise, big download buffer, **no** auto-GC), the nix-community binary cache, `nix-ld`, **sudo-rs** in place of sudo, base CLI (`git`, `fastfetch`), and the fleet-wide font stack. | -| `users.nix` | baseModules (all NixOS) | Builds `users.users` from the registry for the host's `hostUsers`; enables zsh; enables Firefox + Thunderbird **only** when `features.swayDesktop.enable` is on. Applies per-user `linger`. | -| `features.nix` | baseModules (all NixOS) | Declares the feature-flag options (`features.swayDesktop.enable`, `features.claudeCode.enable`) so any host can read/set them without importing the heavy implementation module, plus the CPU capability fact they derive from (`features.cpu.microarchLevel`) and the assertion that guards it. See "CPU capability gating". | -| `workstation.nix` | transitively (via laptop/desktop) | Form-factor-agnostic base for physical graphical hosts: turns on `swayDesktop`, Dvorak console, PipeWire, firewall (default-deny), fstrim, earlyoom, fwupd, thermald (x86), redistributable fw. | -| `laptop.nix` | host table (MBP, T400) | `imports` workstation.nix, then adds the portable bits: iwd Wi-Fi, lid suspend/lock, Bluetooth + blueman. | -| `desktop.nix` | host table (Mac Pro) | `imports` workstation.nix, then swaps Wi-Fi for wired NetworkManager. Pair with `portable = false` in the host table. | -| `sway.nix` | host table (graphical hosts) | Implementation of `features.swayDesktop`: the system Sway package, the greetd/ReGreet (cage) greeter forced to Dvorak, xdg-portal, Wayland utility packages. Home-side Sway config is in `home/sway.nix`. | -| `ssh.nix` | host table (T400, Mac Pro, RPi5) | Enables sshd, opens port 22, enforces a key-only policy (no password / keyboard-interactive, no root). Authorized keys come from the registry via `users.nix`. | -| `firmware/` | referenced by MBP host config | Committed Apple peripheral firmware blobs for the Asahi MBP (see "MacBook (Asahi) firmware"). | +| Module | Imported by | What it does / when to use it | +| ------------------ | ------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `common-nixos.nix` | baseModules (all NixOS) | Timezone/locale, store hygiene (auto-optimise, big download buffer, **no** auto-GC), the nix-community binary cache, `nix-ld`, **sudo-rs** in place of sudo, base CLI (`git`, `fastfetch`), and the fleet-wide font stack. | +| `users.nix` | baseModules (all NixOS) | Builds `users.users` from the registry for the host's `hostUsers`; enables zsh; enables Firefox + Thunderbird **only** when `features.swayDesktop.enable` is on. Applies per-user `linger`. | +| `features.nix` | baseModules (all NixOS) | Declares the feature-flag options (`features.swayDesktop.enable`, `features.claudeCode.enable`) so any host can read/set them without importing the heavy implementation module, plus the CPU capability fact they derive from (`features.cpu.microarchLevel`) and the assertion that guards it. See "CPU capability gating". | +| `workstation.nix` | transitively (via laptop/desktop) | Form-factor-agnostic base for physical graphical hosts: turns on `swayDesktop`, Dvorak console, PipeWire, firewall (default-deny), fstrim, earlyoom, fwupd, thermald (x86), redistributable fw. | +| `laptop.nix` | host table (MBP, T400) | `imports` workstation.nix, then adds the portable bits: iwd Wi-Fi, lid suspend/lock, Bluetooth + blueman. | +| `desktop.nix` | host table (Mac Pro) | `imports` workstation.nix, then swaps Wi-Fi for wired NetworkManager. Pair with `portable = false` in the host table. | +| `sway.nix` | host table (graphical hosts) | Implementation of `features.swayDesktop`: the system Sway package, the greetd/ReGreet (cage) greeter forced to Dvorak, xdg-portal, Wayland utility packages. Home-side Sway config is in `home/sway.nix`. | +| `ssh.nix` | host table (T400, Mac Pro, both Pis) | Enables sshd, opens port 22, enforces a key-only policy (no password / keyboard-interactive, no root). Authorized keys come from the registry via `users.nix`. | +| `firmware/` | referenced by MBP host config | Committed Apple peripheral firmware blobs for the Asahi MBP (see "MacBook (Asahi) firmware"). | Form-factor decision: a **laptop** imports `laptop.nix` (default `portable = true`); a **wired desktop** imports `desktop.nix` and sets diff --git a/docs/hosts/pizero2w.md b/docs/hosts/pizero2w.md new file mode 100644 index 0000000..be4cd23 --- /dev/null +++ b/docs/hosts/pizero2w.md @@ -0,0 +1,205 @@ +# 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. | diff --git a/flake.lock b/flake.lock index e3ef6d0..f91f7d0 100644 --- a/flake.lock +++ b/flake.lock @@ -185,6 +185,22 @@ "type": "github" } }, + "legacy-email-proxy": { + "flake": false, + "locked": { + "lastModified": 1781718202, + "narHash": "sha256-b+d/PqeGuQgdU/fYAla5dobmsDOsd6aYImzWRhAZ/RQ=", + "ref": "refs/heads/main", + "rev": "4bde4f884db2150b1b5ae5d2ac3e3d7e82ab2567", + "revCount": 12, + "type": "git", + "url": "https://code.emmathe.dev/lyrathorpe/legacy-email-proxy" + }, + "original": { + "type": "git", + "url": "https://code.emmathe.dev/lyrathorpe/legacy-email-proxy" + } + }, "nix-darwin": { "inputs": { "nixpkgs": [ @@ -368,6 +384,7 @@ "git-hooks": "git-hooks", "home-manager": "home-manager", "kube-tmux": "kube-tmux", + "legacy-email-proxy": "legacy-email-proxy", "nix-darwin": "nix-darwin", "nix-homebrew": "nix-homebrew", "nix-index-database": "nix-index-database", diff --git a/flake.nix b/flake.nix index d8109f4..6a59d57 100644 --- a/flake.nix +++ b/flake.nix @@ -67,6 +67,13 @@ url = "github:jonmosco/kube-tmux"; flake = false; }; + # legacy-email-proxy: cleartext POP3/SMTP front end for the Psion's mail + # client, proxied to authenticated IMAPS/SMTPS. Ships its own package and + # NixOS module; the Pi Zero 2 W host just enables the service. + legacy-email-proxy = { + url = "git+https://code.emmathe.dev/lyrathorpe/legacy-email-proxy"; + inputs.nixpkgs.follows = "nixpkgs"; + }; }; outputs = @@ -327,6 +334,26 @@ ./users/lyrathorpe/home.nix ]; }; + + lyrathorpe-zero2w = { + system = "aarch64-linux"; + portable = false; + # Headless "Psion sidecar": PPP over RS232 plus a legacy mail proxy + # (hosts/PiZero2W/). No sway.nix; the raspberry-pi-3 profile carries + # the kernel/firmware/device tree (the Zero 2 W is the Pi 3's + # BCM2837 SoC) and ssh.nix adds key-only sshd. This board has 512 MB + # of RAM and never builds its own system -- see + # docs/hosts/pizero2w.md. + modules = [ + ./hosts/PiZero2W/configuration.nix + inputs.nixos-hardware.nixosModules.raspberry-pi-3 + ./modules/ssh.nix + ]; + users.lyrathorpe.homeModules = [ + ./home + ./users/lyrathorpe/home.nix + ]; + }; }; # Darwin host table — macOS machines built via mkDarwinHost. The shared @@ -365,8 +392,24 @@ # nixpkgs instance for that system. Outputs here become per-system # attrsets automatically (e.g. devShells..default). perSystem = - { config, pkgs, ... }: { + config, + pkgs, + system, + ... + }: + { + # One-shot SD card for bringing the Pi Zero 2 W up: that host's own + # configuration plus the sd-image module, so the first boot is + # already the real system. aarch64-linux only -- building it needs + # an aarch64 Linux builder. See docs/hosts/pizero2w.md. + packages = lib.optionalAttrs (system == "aarch64-linux") { + zero2w-sd-image = + ((mkHost hosts.lyrathorpe-zero2w).extendModules { + modules = [ ./hosts/PiZero2W/sd-image.nix ]; + }).config.system.build.sdImage; + }; + # treefmt drives `nix fmt` and the formatting check below. nixfmt # stays the .nix formatter (the tree is already nixfmt-formatted); # shfmt covers shell and prettier covers markdown/yaml/json. diff --git a/hosts/PiZero2W/configuration.nix b/hosts/PiZero2W/configuration.nix new file mode 100644 index 0000000..90f1e30 --- /dev/null +++ b/hosts/PiZero2W/configuration.nix @@ -0,0 +1,111 @@ +# Raspberry Pi Zero 2 W (aarch64) "Psion sidecar": an RS232 companion for a +# Psion 5MX. Two roles, split into submodules: ./serial-ppp.nix (PPP over the +# serial line, NAT out to wifi, telnet login) and ./email-proxy.nix (cleartext +# POP3/SMTP front end for the Psion's mail client). The raspberry-pi-3 +# nixos-hardware profile (the Zero 2 W is the same BCM2837 SoC as the Pi 3) and +# key-only sshd (../../modules/ssh.nix) are layered on in the flake host table. +# Install notes: see ../../docs/hosts/pizero2w.md. +{ lib, ... }: +{ + imports = [ + ./hardware-configuration.nix + ./serial-ppp.nix + ./email-proxy.nix + ]; + + # Match the flake's nixosConfigurations attribute name so `nh os switch` + # (which selects by the local hostname) resolves without an explicit -H flag. + networking.hostName = "lyrathorpe-zero2w"; + + # Headless server: modules/sway.nix is not imported and + # features.swayDesktop.enable defaults to false, so this host keeps plain + # TTY/SSH login. + + # Claude Code is a Node application. It runs on aarch64, but not usefully in + # 512 MB of RAM, and its closure is unwelcome on an SD card. + features.claudeCode.enable = false; + + # 512 MB total and no swap partition -- SD cards wear out under swap writes. + # Compressed RAM swap instead; zstd is the best ratio-per-cycle the SoC can + # sustain. + zramSwap = { + enable = true; + algorithm = "zstd"; + }; + + # The NixOS manual and man page index cost build time and a chunk of the card + # for a box that is administered over SSH from elsewhere. + documentation.nixos.enable = false; + + # Own the firmware partition declaratively: every switch rewrites config.txt, + # the vendor device trees and the overlays below. Without this the card keeps + # whatever config.txt the flashed image wrote and the UART overlays never + # load. uboot.enable keeps the GPU firmware chainloading U-Boot -> extlinux, + # which is how the NixOS aarch64 SD image boots; leaving it off would rewrite + # config.txt without a `kernel=` line and the board would stop booting. + hardware.raspberry-pi.firmware = { + enable = true; + uboot.enable = true; + }; + + hardware.raspberry-pi.configtxt = { + settings.all = { + # Headless: hand the VideoCore the minimum and leave the rest to Linux. + # start_x/camera_auto_detect otherwise reserve VRAM for a camera stack + # this board does not have. + gpu_mem = 16; + start_x = 0; + camera_auto_detect = false; + # Left on, the firmware auto-loads the KMS display overlay, which wants + # more VRAM than this board can spare for a monitor it will never have. + display_auto_detect = false; + }; + + # Replaces the profile's default (vc4-kms-v3d), which is display hardware + # this host never uses. + deviceTreeOverlays.all = [ + # Move the PL011 UART off Bluetooth and onto GPIO 14/15, so /dev/ttyAMA0 + # is the RS232 header. The mini UART (ttyS0) derives its baud rate from + # the core clock and drifts at 115200. + { disable-bt = { }; } + # RTS/CTS on GPIO 16/17: the Psion's modem profile uses hardware flow + # control, and so does pppd in ./serial-ppp.nix. + { uart0.ctsrts = true; } + ]; + }; + + # Wifi is the Pi's uplink and the route the Psion reaches the internet over + # (./serial-ppp.nix masquerades onto it). + networking.interfaces.wlan0.useDHCP = true; + networking.wireless = { + enable = true; + interfaces = [ "wlan0" ]; + # PSKs stay out of the Nix store: wpa_supplicant reads them at runtime from + # this file, which is created on the device (root-owned, 0600) and contains + # psk_home= + # See ../../docs/hosts/pizero2w.md. + secretsFile = "/var/lib/wpa_supplicant/secrets.conf"; + networks."CHANGE-ME-SSID".pskRaw = "ext:psk_home"; + }; + + # The board takes a DHCP lease over wifi, so its address moves. mDNS makes it + # findable as lyrathorpe-zero2w.local instead of hunting through the router's + # lease table -- which matters most on first boot, when it is the only way in. + services.avahi = { + enable = true; + openFirewall = true; + publish = { + enable = true; + addresses = true; + workstation = true; + }; + }; + + # Default-deny inbound. sshd opens 22 (../../modules/ssh.nix); everything the + # Psion talks to is reached over the PPP link, which ./serial-ppp.nix marks + # trusted. + networking.firewall.enable = true; + + # See `man configuration.nix` / the stateVersion docs before changing. + system.stateVersion = "26.05"; +} diff --git a/hosts/PiZero2W/email-proxy.nix b/hosts/PiZero2W/email-proxy.nix new file mode 100644 index 0000000..4035933 --- /dev/null +++ b/hosts/PiZero2W/email-proxy.nix @@ -0,0 +1,25 @@ +# legacy-email-proxy: a cleartext POP3 (110) and SMTP (25) front end for the +# Psion's built-in mail client, forwarded to authenticated IMAPS/SMTPS. +# +# The package, the systemd unit and its hardening all live upstream +# (https://code.emmathe.dev/lyrathorpe/legacy-email-proxy); this host only +# enables the service and points it at the credentials. +{ inputs, ... }: +{ + imports = [ inputs.legacy-email-proxy.nixosModules.default ]; + + services.legacy-email-proxy = { + enable = true; + + # The listeners are unauthenticated and unencrypted by design, so the + # firewall is what confines them: ppp0 is trusted, wlan0 is not, and 110/25 + # are never opened there (./serial-ppp.nix). They stay on the default + # 0.0.0.0 rather than the PPP address because 10.0.0.1 exists only while + # the Psion is plugged in, and a bind-time dependency on a serial cable is + # a restart loop waiting to happen. + + # Backend hostnames and credentials. Kept out of the Nix store: created on + # the device, root-owned 0600. See ../../docs/hosts/pizero2w.md. + environmentFile = "/var/lib/legacy-email-proxy/backend.env"; + }; +} diff --git a/hosts/PiZero2W/hardware-configuration.nix b/hosts/PiZero2W/hardware-configuration.nix new file mode 100644 index 0000000..71071f6 --- /dev/null +++ b/hosts/PiZero2W/hardware-configuration.nix @@ -0,0 +1,33 @@ +# PLACEHOLDER hardware configuration for the Raspberry Pi Zero 2 W. +# +# This file is NOT the real generated config -- it exists only so the host +# evaluates in CI before the Pi is provisioned. The machine will not boot from +# it as-is. On first install, regenerate this file on the device with +# nixos-generate-config --root /mnt +# and replace this placeholder with the output (commit it). See ../../docs/hosts/pizero2w.md. +# +# Like every hardware-configuration.nix in this repo, this file is excluded from +# the formatter and linters (see the pre-commit/treefmt excludes in flake.nix). +{ modulesPath, ... }: +{ + imports = [ (modulesPath + "/installer/scan/not-detected.nix") ]; + + nixpkgs.hostPlatform = "aarch64-linux"; + + # The Zero 2 W boots from an SD card with a FAT firmware partition and an ext4 + # root. Labels match the conventional sd-image layout; the real generated + # config will use by-uuid device paths instead. + fileSystems."/" = { + device = "/dev/disk/by-label/NIXOS_SD"; + fsType = "ext4"; + }; + + fileSystems."/boot/firmware" = { + device = "/dev/disk/by-label/FIRMWARE"; + fsType = "vfat"; + }; + + # 512 MB of RAM and an SD card: no swap partition (SD cards wear out under + # swap writes). zram takes its place; see ../../hosts/PiZero2W/configuration.nix. + swapDevices = [ ]; +} diff --git a/hosts/PiZero2W/sd-image.nix b/hosts/PiZero2W/sd-image.nix new file mode 100644 index 0000000..3aab2a5 --- /dev/null +++ b/hosts/PiZero2W/sd-image.nix @@ -0,0 +1,44 @@ +# SD-card image of this host, used exactly once: to bring the board up. +# +# Deliberately NOT imported by ./configuration.nix. The flake extends the host +# with it (see packages.aarch64-linux.zero2w-sd-image in ../../flake.nix), so +# the card carries the host's own kernel, config.txt and SSH keys rather than a +# generic installer that then has to be reconfigured over a console this host +# does not have -- pppd owns the serial port (./serial-ppp.nix). +# +# It does not carry the runtime secrets. Seed those into the card's root +# partition before first boot; see ../../docs/hosts/pizero2w.md. +{ + config, + lib, + modulesPath, + ... +}: +{ + imports = [ "${modulesPath}/installer/sd-card/sd-image.nix" ]; + + # sd-image.nix pulls in profiles/all-hardware.nix, which is every driver and + # firmware blob NixOS knows about. The raspberry-pi-3 profile already carries + # what this board has, and the card is small. + hardware.enableAllHardware = lib.mkForce false; + + image.baseName = "nixos-zero2w"; + + sdImage = { + # Compressing costs a long single-threaded pass and buys nothing: the image + # is written straight to a card with dd. + compressImage = false; + + # The default 30 MiB does not hold the vendor GPU firmware, U-Boot and the + # BCM2837 device trees and overlays that nixos-hardware installs here. + firmwareSize = 128; + + # The firmware partition is populated by nixos-hardware's firmware module + # (it takes over sdImage.populateFirmwareCommands); the root side is the + # stock extlinux install, which no longer arrives with it. + populateRootCommands = '' + mkdir -p ./files/boot + ${config.boot.loader.generic-extlinux-compatible.populateCmd} -c ${config.system.build.toplevel} -d ./files/boot + ''; + }; +} diff --git a/hosts/PiZero2W/serial-ppp.nix b/hosts/PiZero2W/serial-ppp.nix new file mode 100644 index 0000000..c51a86b --- /dev/null +++ b/hosts/PiZero2W/serial-ppp.nix @@ -0,0 +1,89 @@ +# The serial half of the Psion sidecar: a PPP link to a Psion 5MX over +# /dev/ttyAMA0 (RS232 level shifter on the GPIO header, 115200 8N1 with +# RTS/CTS), masqueraded out of wifi, plus a telnet login for the Psion's +# terminal client. +# +# Cleartext telnet and unauthenticated PPP are safe *only* because the link is +# a two-node cable: the peer is a machine from 1999 that speaks no TLS. Nothing +# here is exposed to wlan0. +{ pkgs, ... }: +let + # Point-to-point addresses for the serial link; nothing else routes here. + piAddress = "10.0.0.1"; + psionAddress = "10.0.0.2"; +in +{ + # pppd needs exclusive use of the port. NixOS starts a getty on any serial + # console named in boot.kernelParams; ttyAMA0 is not one today, but disable it + # explicitly so a later kernel-param change cannot silently steal the line. + systemd.services."serial-getty@ttyAMA0".enable = false; + + services.pppd = { + enable = true; + peers.psion.config = '' + /dev/ttyAMA0 + 115200 + ${piAddress}:${psionAddress} + + # Hardware flow control, matching the Psion's modem profile. + crtscts + + # A null-modem cable has no carrier detect and no peer to authenticate. + local + noauth + + # The systemd unit is Type=notify, so pppd must stay in the foreground. + nodetach + lock + + # Wait for the Psion rather than failing when it is unplugged, and keep + # waiting for the next time it is plugged back in. + passive + persist + maxfail 0 + holdoff 1 + + # Hand the Psion resolvers over the link, so its Internet profile can set + # "get DNS from server = True" instead of hard-coding them. + ms-dns 1.1.1.1 + ms-dns 8.8.8.8 + ''; + }; + + # The Psion's route to the internet. The original write-up used pppd's + # proxyarp instead; NAT keeps the Psion out of the LAN broadcast domain and + # does not depend on what the wifi router tolerates. + networking.nat = { + enable = true; + externalInterface = "wlan0"; + internalIPs = [ "${psionAddress}/32" ]; + }; + + # Everything the Psion connects to (telnet here, POP3/SMTP in + # ./email-proxy.nix) is reachable over the PPP link and nowhere else. + networking.firewall.trustedInterfaces = [ "ppp0" ]; + + # The Psion's terminal client speaks telnet over TCP, which it renders far + # better than the raw serial console. Socket-activated, one process per + # connection; busybox's telnetd in inetd mode hands straight over to login. + systemd.sockets.telnetd = { + description = "Telnet login socket for the Psion"; + wantedBy = [ "sockets.target" ]; + listenStreams = [ "${piAddress}:23" ]; + socketConfig = { + Accept = true; + # ppp0 (and with it 10.0.0.1) only exists while the Psion is connected; + # FreeBind lets the socket be listening before that. + FreeBind = true; + }; + }; + + systemd.services."telnetd@" = { + description = "Telnet login for the Psion"; + serviceConfig = { + ExecStart = "-${pkgs.busybox}/bin/busybox telnetd -i -l ${pkgs.shadow}/bin/login"; + StandardInput = "socket"; + StandardError = "journal"; + }; + }; +}