OZD · THE ASSOCIATED MACHINE FOR A SITE OF MIT CADR LISP MACHINES
ozd
ozd gives a site’s Lisp Machines their files, the time and their host table.
ozd is for the CADR: it serves its releases. QUUX does not use it, having a file device and a real-time clock of its own.
ozd is written in Rust and has no crate dependencies. It runs on Linux and macOS.
01 / WHAT IT IS
The host a band
asks for its files.
When a CADR boots — in muir-sim, or on an FPGA board with muir-fpga — it asks another host for the date and for its files. MIT called that host its associated machine.
At MIT that was OZ. ozd is OZ as a daemon: one host on one Chaosnet subnet, reached over UDP, for one machine or many.
What a band prints once it has booted, from sys/io/disk.lisp:
MIT Lisp Machine One, with associated machine OZ.
OZ, also called MIT-OZ, was the host where MIT’s Lisp Machines kept their system files. They also asked it for the time and for their host table. Several machines can share one ozd, just as MIT had one associated machine for many Lisp Machines.
02 / SEVEN SERVICES
Seven services.
Four because a machine does not finish booting without them, one because a cold load does not, and two because a band calls them at its associated machine. A simple transaction is one request and one answer; a stream is a connection.
| Contact | Kind | What ozd answers | When a band asks |
|---|---|---|---|
| STATUS | simple transaction | Its name, and the counters of its subnet | (hostat), which asks every host |
| TIME | simple transaction | The universal time, from the system clock | When it wants the date, from the site’s time servers |
| UPTIME | simple transaction | How long ozd has been up | (uptime) |
| FILE | stream | The files in its roots, to read, write, rename and delete | For its sources and its users’ files, from the file host its site names |
| MINI | stream | The same files, read only, one after another | (si:qld), in a cold load that has no FILE yet |
| HOSTAB | stream | A host’s names, address and system type | When it meets a host name that its own table lacks |
| NAME | stream | That nobody is logged in | (finger) with no host |
1/60 s
The unit of UPTIME’s answer, as the band’s own server sends it and its own user end divides it. The manual says seconds. The band’s code says sixtieths, and the band is what reads the answer.
Cold-loaded, a band has a reader, an evaluator and a fasloader, and no FILE yet. (si:qld) reads the rest of it through MINI: one file at a time, on one connection that is never closed. Each file goes whole, and then an EOF.
A cold load sends to the address that its cold/mini.lisp was compiled with. System 100’s names 3060, OZ, so an ozd at 3060 serves it as it is. System 1000 and later take the host that SYS: translated to when it was compiled.
What goes over a MINI connection, as MIT’s own server, cold/minisr.mid, lists it:
| Opcode | From | What it carries |
|---|---|---|
200 201 | the machine | An open, for characters or for binary, with the file name |
202 | ozd | The truename, a newline and the file’s date |
203 | ozd | Why the file cannot be read, and a newline |
200 300 | ozd | The file, as characters or as 16-bit words |
EOF | ozd | The end of the file; the next open may follow |
MINI finds a file as FILE does when it reads one, so nothing outside the roots is read.
03 / THE SWITCH
Then one cable.
Now a link each.
THEN: ONE CABLE
Chaosnet ran on one coaxial cable, which AIM-628 calls the ether. A packet one node sends arrives at every other node, and each decides in hardware whether to keep it, much as on the coaxial Ethernet of its day.
The memo explains whose turn it is to send by analogy with a ring’s token, but the cable itself is a line: its ends may not be joined into a circle.
NOW: A LINK EACH
No cable is shared any more. Ethernet gives each host a link of its own to a switch, and UDP goes point to point. So ozd is the switch.
Each machine sends its packets to ozd. ozd passes each one on to the host it is addressed to, or to every host but its sender if it is a broadcast, and changes nothing on the way.
--peer fixes one that must not move. FILE and MINI serve one tree: a base root, and roots mounted at its top level, each read-only or not. A --tcp listener carries a TCP client to a machine’s stream through the same switch.What ozd does with a packet, by where it is addressed:
| Addressed to | ozd |
|---|---|
3060, this host | answers it |
0, a broadcast | answers it, and passes it to every endpoint but its sender’s |
3051, another machine | passes it on, byte for byte |
3052, a host not yet heard from | drops it, having nowhere to send it |
4401, another subnet | drops it: ozd routes nothing |
Unlike an Ethernet switch, ozd drops a packet for a host it has not heard from yet, rather than sending it to every port.
It is a switch and not a bridge: no forwarding count, no routing, and nothing to another subnet.
04 / BUILD AND RUN
Build it.
Run a site.
ozd is built from its source with Cargo. Its compiler is pinned in rust-toolchain.toml, and nothing else is downloaded.
The Rust toolchain.
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
Install rustup, and open a new shell. A Cargo from your system’s packages ignores rust-toolchain.toml and may be too old for the 2024 edition.
Clone and build.
git clone https://github.com/metebalci/ozd cd ozd cargo build --release
rustup downloads the pinned toolchain the first time Cargo runs here. ozd has no crate dependencies.
Run a site for MIT’s system.
target/release/ozd --address 3060 --name MIT-OZ,OZ,system=UNIX \
--root /srv/lispm --root tree=/path/to/system-100-0/sys,ro \
--host 3050,MIT-LISPM-1,CADR-1,CADR1,LM1,system=LISPM \
--file-dates mit --timezone 5The host table of MIT’s released system lists the Lisp Machine as MIT-LISPM-1 at 3050 and its file and time host as MIT-OZ at 3060, so ozd takes that address and those names. Every root must be an existing directory when ozd starts.
- --address, --name
- This host as the band’s host table has it: its address, its names with the official one first, and its system type.
- --root /srv/lispm
- The base root. It holds the users’ home directories, and ozd writes to it. Each user’s is
/<user>/in lower case: create it and make it writable, or a band that compiles stops with "Access to file denied". - --root tree=...,ro
- The release’s sources, mounted read-only at
/tree, which is where the band looks for them. - --host, --hosts-text
- Machines for HOSTAB to answer for, one flag each, or a band’s own
hosts.text, so that a band whose table lacks a machine can still find it by name. - --listen
- Where ozd answers. Without it,
127.0.0.1:42042, which only this host can reach. - --peer <addr>@<ip>
- The fixed endpoint of a host that must not move. Every other endpoint is learned.
- --tcp 10000,TELNET@3050
- A TCP port that ozd carries to a Chaosnet stream at a machine of its subnet. Nothing listens without it (Into the machine).
- --file-dates mit|utc
- How FILE writes and reads a file’s dates. The default,
utc, is plain UTC with no daylight saving, as System 1002 and later put dates on the wire. Systems 100 to 1001 write them in their site’s zone with MIT’s daylight saving on top, and want--file-dates mitwith their--timezone. - --timezone 5
- With
--file-dates mit, the band’s zone, as its site’s:TIMEZONE: whole hours west of Greenwich, 5 for System 100’s site. FILE’s dates are printed and read at it, with the band’s own daylight saving on top, the old North American calendar, whatever the zone. Without--file-dates mitit is refused. - --log-simple, --log-file, --log-file-probe, --log-mini, --log-tcp
- More lines in the log: each simple transaction answered; each file read, directory listed and login; each FILE probe; each MINI open; each TCP connection opened and closed.
The same flags, one per line, can go in a file of flags that -c names, or OZD_RC, or .ozdrc. --check reads the flags, checks the roots and exits, and --help describes each flag.
Ask it.
cargo run --example ask 3060@127.0.0.1:42042
asking 3060 at 127.0.0.1:42042, from 3376 STATUS MIT-OZ TIME 2026-09-13T13:24:58Z (+0 s from this host) UPTIME 0d 1h 6m 45s FILE / holds 2 directories and 1 file
It asks for STATUS, TIME and UPTIME over CHUDP, as a Lisp Machine would, then lists a directory through FILE and counts what is in it.
Point the machines at it.
A machine names ozd as its default CHUDP peer: its address, then where it listens, 3060@127.0.0.1:42042 on the same computer. muir-sim, a simulator of the CADR, and muir-fpga, the CADR on an FPGA board, both work with ozd this way.
On one computer each program needs its own UDP port: ozd takes 42042, a machine 42043, the next 42044. On separate computers start ozd with --listen 192.0.2.10, and each machine names 3060@192.0.2.10.
A machine whose CHUDP sends a unicast packet only to a peer named for its destination also names every other machine’s address at ozd’s endpoint, so that its packets for them pass through the switch.
05 / INTO THE MACHINE
Into the machine.
A --tcp listener takes TCP connections on the Unix side and carries each one to a Chaosnet stream at a machine. TELNET comes first.
A band serves TELNET. What it lacks is a Unix end to reach it from. Every machine already names ozd as its peer, so ozd listens on a TCP port and carries what comes in to the machine’s TELNET.
Listen.
target/release/ozd ... --tcp 10000,TELNET@3050
A bare port binds the loopback and nothing else. Each connection ozd takes there becomes a stream from ozd to TELNET at 3050, with the bytes going both ways. The machine must be on ozd’s subnet, and ozd must have heard from it.
Connect.
nc 127.0.0.1 10000
This reaches the Lisp top level of the machine at 3050. When either end closes, the other is closed too.
Nothing here is TELNET’s in particular: --tcp <endpoint>,<CONTACT>@<addr> names any stream contact, one for each listener, and may be given more than once. --log-tcp writes a line when a connection opens and one when it closes.
A TELNET connection is a Lisp top level with no login, for whoever reaches the port.
So nothing listens without --tcp, a bare port binds only the loopback, and each listener names its one contact, so that SUPDUP or EVAL is never reachable by accident. Name another address only where every host that can reach it may use the machine.
06 / WHO CAN REACH IT
Who can reach it.
ozd does not ask who you are. Any host that reaches its socket gets an answer, and FILE too.
ozd is meant for a trusted network segment. LOGIN accepts any name and only records it; it is never a credential. So the address ozd listens on decides who can use it: without --listen only this host can, and with 0.0.0.0 every host that can reach it can. ozd has no TLS and does no routing.
WHAT OZD HOLDS TO
A pathname cannot climb out of its root: .. and . are refused, not normalised. A symbolic link is followed only while it stays inside its own root; a command that asks for a link itself, as DELETE, RENAME and an open with INHIBIT-LINKS do, acts on the link and never on what it leads to; and a directory from elsewhere is mounted with --root <name>=<path>, never linked in.
Anything that is not a regular file or a directory is refused, and a ,ro root refuses every write before the disk is touched. ozd refuses to run as root, and needs no privilege: 42042 is an ordinary UDP port. It opens a TCP port only for a --tcp, and only on the loopback unless an address is named.
WHAT THE OPERATOR HOLDS TO
- The roots belong to ozd’s own user, and nobody else writes to them. A hard link inside a root to a file outside it looks like an ordinary file, and so does a bind mount. If only ozd’s user can write to a root, neither can appear there.
- One ozd per root, and each
,roroot read-only on disk as well. ozd checks a path and then opens it, which is safe only because nothing else writes to a root in between.
07 / AS A SERVICE
As a service.
contrib/ has a systemd unit and a launchd daemon. Both run ozd as a user of its own, which owns the writable roots and nothing else. The systemd unit is in use; the launchd daemon has not been tried yet.
Linux, with systemd.
sudo contrib/install-systemd.sh
Build ozd first, and run the script from the source directory. It creates the ozd user and the base root /srv/lispm, installs the binary and the unit, writes /etc/ozdrc unless one exists, checks the flags and the roots as that user, and starts the service. With --dry-run it prints what it would do and changes nothing, which is all it has been checked with.
Edit /etc/ozdrc to suit your site, then sudo systemctl restart ozd. The log goes to the journal: journalctl -u ozd.
macOS, with launchd.
sudo cp contrib/com.metebalci.ozd.plist /Library/LaunchDaemons/
Create a hidden system user named _ozd that owns the writable roots, for example with dscl. Install the binary as /usr/local/bin/ozd and the file of flags as /usr/local/etc/ozdrc, and copy the plist into place.
08 / ABOUT
Read from
the sources.
Every fact about a protocol is read from the Lisp Machine’s own manual and sources: AIM-628, sys/man/chaos.text, sys/doc/chfile.text and the machine’s network and cold-load code. docs/protocols.md says where each one was read.
The name is OZ daemon. OZ was the host that did the same job for the Lisp Machines at the MIT AI Lab in the 1980s.
ozd is written in Rust with Claude Code, using Anthropic’s Claude Opus, and has no crate dependencies.
© 2026 Mete Balci. ozd is AGPL-3.0-or-later.