Orrery · the build, end to end
Eleven phases turned a read-only web wall into the thing an operator stands in front of: a room of eight machines you select from, a row of verbs that act on the selection, and five protocols underneath them that were all written by hand — because the one machine this has to run on cannot reach the internet to fetch a crate.
What it is now
Every screen renders copal fleet state and nothing else. A node that is
not in that document cannot be dialled, and there is no second way of learning anything —
a console that discovered nodes for itself would be a second fleet.
| face | command | what it is |
|---|---|---|
| the wall | orrery --listen | a page the gallery screen and a phone can open |
| the seat | orrery --seat ID | one node, full screen, in the terminal you are already in |
| the museum interface | orrery --gui | a native window on a Mac or a node: the lab, the verbs, the panes |
The verb bar under the lab shows only what can apply to the current selection, and the footer counts what cannot. A verb with no transport is absent rather than greyed — the count went 6 → 5 → 3 → 2 → 1 as the phases landed, and the one that remains is a decision rather than a gap.
The schedule, as it actually went
The order was chosen so the riskiest thing was found out early and the useful thing was usable long before the end. Stopping after phase 3 would have left a working console.
| # | phase | proved against |
|---|---|---|
| 0–3 | the seams, input, the Mac backend, the museum interface | a real compositor (weston, headless) and a rendered frame |
| 4 | crypto.rs — the primitives |
published vectors: RFC 7748, 8032, 8439, FIPS 180-4 and 197, NIST GCM |
| 5 | ssh.rs and Terminal |
a real sshd, plus a hand-written server for the lies a real one will not tell |
| 6 | sftp.rs, Exchange and Send |
OpenSSH’s own sftp-server — no fake at all |
| 7 | tls.rs and x509.rs |
openssl s_server, including mutual TLS |
| 8 | rdp.rs and Control |
xrdp — 1024×768, 137 rectangles, every one RLE compressed |
| 9 | the media pane and the card writer | copal-prep.sh on a real terminal, typed ERASE and all |
| 10 | the node’s half, in the other repository | make lint, which checks every embedded script under dash |
| 11 | the truth pass | this page, and a README that no longer promises what it cannot do |
The rule that shaped every phase
No fake where a real server exists. A hand-written server agrees with the client’s own reading of the specification, including wherever that reading is wrong. Four of the five protocol phases ended up proved against somebody else’s implementation, and each of those found something a fake never would have.
The wire
profile.rs names one key exchange, one host-key algorithm, one signature
algorithm and one cipher to offer. A test fails the build if sha1,
cbc, rc4, ssh-rsa or ecdsa ever appears
on a list — and orrery --profile-sshd prints the same list as
sshd_config lines, which the node writes into its own configuration and whose
drift make lint refuses.
the gate
The host key must be an ssh-ed25519-cert-v01 from the fleet CA, of the
host kind, naming that node, valid now — and then the node has to prove it holds the
certified key. No trust-on-first-use, no prompt, no bare-key branch. The signature is
checked before the principals and the dates, because until then they are text
an attacker chose.
the lockdown
The node demands a client certificate from the fleet CA. mstsc and
FreeRDP have none and cannot be given one, so their handshake ends before RDP begins.
Proved against s_server -Verify 1: in with the certificate, out without it.
the narrowing
In SSH the client’s preference decides, so offering ChaCha first and offering it alone are the same session. In TLS the server picks — so the ClientHello names exactly one suite, because the alternative runs at half a megabyte a second.
What running it found
GSSAPIAuthentication no is an error on AlpineOpenSSH there is built without GSSAPI, so the line the profile rendered made every
connection log a complaint about the node’s own configuration. Found on the first run of
ssh-check.sh against a real sshd; the line is gone and a test
keeps it gone.
Measured rather than assumed: 186.1 MB/s for
ChaCha20-Poly1305 against 0.5 MB/s for AES-256-GCM. Since a TLS
server picks the cipher and rustls prefers AES, offering both would have handed a Raspberry
Pi the slow one. That is why tls.rs offers one suite — and why a test now runs
an AES-only server and requires the console to refuse it.
TLS 1.3’s application keys come from the transcript up to the server’s
Finished and nothing after it. Working that point out by arithmetic at the end
of the handshake was right while the client sent only a Finished, and silently
wrong the moment it also sent a certificate: a handshake that completes and then cannot
decrypt a byte.
It derives its channel bindings by hashing the certificate with the digest named in the signature algorithm, and Ed25519 names none. unable to retrieve bindings, and the connection drops before RDP starts. That is a finding about the fleet’s one-algorithm-everywhere decision rather than about this code — and it is why the RDP suite runs against xrdp instead.
fcntl is variadic, and declaring it otherwise is silentC declares it int fcntl(int, int, ...). Declared with a fixed third argument it compiles, links and runs — and on aarch64 Darwin the variadic convention passes that argument on the stack while a fixed one goes in a register, so O_NONBLOCK never arrived. Every read meant to return immediately blocked until the far end spoke. It hid for two phases because every read happened after a question had been asked, and appeared the instant something read a pipe before asking one. Two tests now ask for nothing and require an answer anyway.
Rect::cut_bottom returns the piece it took first, the opposite of
cut_top — the whole file browser drew inside a forty-pixel strip. The gutter
between its columns was never painted, so it showed the canvas’s own zeroes as a black hole.
And a button labelled ← copy drew as copy,
because the font baked U+2192 and not U+2190 and a missing glyph is a blank cell rather than
a panic. All three were found by rendering a frame to a file and looking at it.
What was refused
libavcodec is not on an Alpine node, so RDP negotiates plain bitmaps with RLE —
the same trade the VNC client made in asking for Raw.The arithmetic
Still open
The truth pass exists to keep this list accurate rather than to shorten it.
aarch64-unknown-linux-musl and its Linux suite passes in a container on a Mac,
which is not the same claim.hypr-rdp. The RDP client is proved against
xrdp — a different implementation by other people, and not the one a Wayland node will run.rustls. The risk is kept where it can be read.And the next five days of work, priced
One SSH connection per node instead of one per verb (a second saved per session, eight per fan-out); row-wise blitting instead of per-pixel (the reason Control feels heavy); one byte writer instead of three (150 lines, and three places to get a length wrong becomes one); one pane lifecycle; one fixture script for the container suites. Each is written up with what it costs today and what the fix costs, because a refactoring list with no prices on it is a wish list.