Skip to content
metebalciPublic

About

The associated machine for a site of MIT CADR Lisp Machines: Chaosnet services over UDP

Topics

Resources

Stars

0 stars

Watchers

1 watching

Forks

Latest commit

 

History

111 Commits

Folders and files

Repository files navigation

ozd

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 (see System 100's sys/site/site.lisp). ozd is OZ as a daemon.

ozd is the associated machine for a site of MIT CADR Lisp Machines. It is one host on one Chaosnet subnet, reached over UDP. It serves the machines their files, the time and their host table, and it is the switch of their subnet: it passes each packet to the machine it is addressed to. Several machines can share one ozd, each naming it as its CHUDP peer, just as MIT had one associated machine for many Lisp Machines.

ozd serves STATUS, TIME, UPTIME, FILE, MINI, HOSTAB and NAME, and it passes packets between the machines on its subnet. The design is in docs/design.md. Every protocol a Lisp Machine speaks is described in docs/protocols.md, together with where each fact comes from, and CHUDP's framing, byte by byte, in docs/chudp.md. ozd's page is https://muir.metebalci.com/ozd/.

Build

ozd is built with Cargo, which you install with rustup:

curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh

Open a new shell, or run . "$HOME/.cargo/env", and then build from this directory:

cargo build --release

ozd is written in Rust 2024 and has no crate dependencies. Its compiler version is pinned in rust-toolchain.toml. rustup reads that file and downloads the pinned toolchain the first time you run Cargo here. If rustup's auto-install setting is disable, run rustup toolchain install in this directory instead. Nothing else is downloaded. A Cargo installed from your system's packages ignores rust-toolchain.toml and may be too old for the 2024 edition, so use rustup.

Run

This is the configuration for a System 100 band. Its host table lists the Lisp Machine as MIT-LISPM-1 at 3050 and its file and time host as MIT-OZ at 3060 (sys/site/hosts.text):

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 5
  • --address and --name describe this host as the band's host table does: its address, its names with the official name first, and its system type. On a System 100 band, keep every address below 140000 octal: that release's routing table holds 96 subnets, and a band whose own subnet is 96 or above stops during its network setup and then runs on with no Chaosnet at all. System 1000 and later hold 256 and do not mind.
  • --root /srv/lispm is the base root. It holds the users' home directories, and ozd writes to it. LOGIN gives each user the home directory /<user>/ in lower case, so the user LISPM's is /srv/lispm/lispm/. Create it and make it writable: ozd does not create it, and a band that compiles a system writes its compiler warnings there and stops with "Access to file denied" if it cannot.
  • --root tree=...,ro mounts the release's sources at /tree, read-only. That is where the band looks for them (sys/site/sys.translations). Every root must be an existing directory when ozd starts.
  • Double the slashes in that translations file. It is read in the machine's traditional syntax, where a slash escapes the next character, so a Unix path must be written "//tree//sys//" and not "/tree/sys/", and the file must not name a readtable. MIT never met this because their translations named TOPS-20 paths. A cold load whose site file has single slashes stops with its reader running off the end of the file.
  • A site can have mounts and no base root. / is then read-only and lists only the mounts. A user's home directory then does not exist, unless a writable mount has its name. For example, --root lispm=/srv/lispm becomes the home of the user LISPM.
  • --host adds a machine to the host table that HOSTAB answers from. Bands whose own table does not know the machine can then find it by name. Each further machine needs its own address and its own --host.
  • --hosts-text names a band's own host table, such as /srv/lispm/sys/site/hosts.text, and HOSTAB answers for the hosts in it as well. A site that already keeps that file for its machines then writes each host once instead of twice. This host's own line in it is passed over, because --name gives its names, and a host with no Chaosnet address is skipped. ozd reads the file when it starts, so a change to it wants a restart.
  • --file-dates says how FILE writes and reads a file's dates. The default, utc, is plain UTC, with no zone and no daylight saving, which is how System 1002 and later put dates on the wire; such a band still shows them in its own local time. Under utc the year has four digits, MM/DD/YYYY HH:MM:SS, where MIT's FILE protocol gives two: an extension for System 1002 and later, so that 1970 and 2099 do not read back as 2070 and 1999. Systems 100 to 1001 write them in their site's zone with MIT's daylight saving on top, and want --file-dates mit with their --timezone, as here. A --timezone without --file-dates mit is refused.
  • --timezone 5 is the band's zone, as its site file's :TIMEZONE gives it: whole hours west of Greenwich, 5 for System 100's site. Under --file-dates mit ozd prints and reads every FILE date at that zone, and puts the band's own daylight saving on top of it, whatever the zone: the old North American calendar, from the last Sunday in April to the last Sunday in October. Without the flag the zone is 0, which is UTC, with that daylight saving on top, so a summer date is printed an hour from plain UTC: the band reads every date through that rule. Whole hours from -12 to 12 are taken; a fraction such as 3.5 is not.
  • --log-simple, --log-file, --log-file-probe, --log-mini and --log-tcp each add lines to the log, and each works on its own. --log-simple writes a line for each simple transaction ozd answers, such as TIME, STATUS or UPTIME. --log-file writes one for each file read, each directory listed and each login, with the address of the machine that asked. --log-file-probe writes one for each FILE probe, which a band makes far more often than it reads. --log-mini writes one for each file a cold load reads through MINI, and one for each it is refused; a cold load that reports its progress through MINI is logged without it, being the one thing such a machine can say, as is an operation ozd does not know. --log-tcp writes one when a --tcp connection opens and one when it closes. Without them, a machine's whole boot leaves one line in the log, the connection it opened. Two lines are written whatever the flags say, because nothing else would mention them: a packet dropped for a machine that has not spoken since ozd started, and a datagram ozd cannot read at all, which is what two ends framing CHUDP differently look like from here. A line names a machine by its address and by its name in the host table, or (?) when the table does not have it.
  • --listen sets where ozd answers. Without it, ozd listens on 127.0.0.1:42042, which only this host can reach. Give an address on your network, or 0.0.0.0, to let other hosts reach it.
  • --peer <addr>@<ip> fixes the endpoint of a host that must not move, such as cbridge. ozd learns every other endpoint from the packets that hosts send.
  • --tcp <endpoint>,<CONTACT>@<addr> makes ozd listen on a TCP port and carry each connection to a Chaosnet stream at a machine of its subnet. For example, --tcp 127.0.0.1:10000,TELNET@3050 lets nc 127.0.0.1 10000 reach the Lisp top level of the machine at 3050, once ozd has heard from that machine. A bare port listens on the loopback. Nothing listens without the flag, and each listener names one contact. See Security before using it.

The same flags, one per line, can also go in a file of flags. ozd reads the file that -c names. Without -c, it reads the file that OZD_RC names. Without either, it reads .ozdrc from the current directory, or else from your home directory. It reads only the first of these that it finds, and flags on the command line override the file (docs/design.md §8). --check reads the flags, checks the roots and exits without changing anything. --trace prints every packet, and --help describes each flag. ozd refuses to run as root, and it logs to stderr, one line per event.

To check that a running ozd answers, run the example that comes with its source, from this directory. It asks a host for STATUS, TIME and UPTIME over CHUDP, as a Lisp Machine would, then lists a directory through FILE and counts what is in 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

The argument names ozd as --peer names a peer: its Chaos address, then where it listens, by name or IP address, at port 42042 unless one is given. The directory listed is /, unless an argument that begins with / names another. The example logs in as ASK, and it sends from an address of its own on the same subnet, host 376, which ozd learns as it learns any machine's. If one of your machines has that address, give another as an argument.

Each machine names ozd as its CHUDP peer. This example runs a machine with muir-sim, a CADR simulator, on the same computer as ozd:

cadr --disk-pack /path/to/disk-sys-100-0.img \
     --chaos-address 3050 --chaos-udp 42043 \
     --chaos-udp-peer 3060@127.0.0.1:42042

Because both programs run on one computer, each needs its own UDP port. ozd uses 42042, the machine uses 42043, and the machine reaches ozd at 127.0.0.1:42042. A second machine on the same computer would use the address 3051 and the port 42044, and so on.

On separate computers, every program can use the default port, 42042, but each must listen on its network address instead of the loopback. For example, start ozd with --listen 192.0.2.10, and run the machine on another computer like this:

cadr --disk-pack /path/to/disk-sys-100-0.img \
     --chaos-address 3050 --chaos-udp 192.0.2.20 \
     --chaos-udp-peer 3060@192.0.2.10

Some CHUDP implementations send a packet only to a peer that is named for its destination. A machine like that must also name every other machine's address at ozd's endpoint, so that packets between the machines pass through ozd (docs/design.md §9).

A cold load reads the rest of its system through MINI, before it has FILE, from the address that its cold/mini.lisp was compiled with. System 100's names 3060, so an ozd at 3060 serves it as it is. System 1000 and later take the address of the host that SYS: translated to when it was compiled, so a cold load built from files compiled against ozd reads from ozd (docs/protocols.md, MINI).

A cold load has no other network, so it cannot say how it is getting on. ozd therefore takes one opcode that MIT's MINI does not, 204, a line for this host's log, so that a machine building a world unattended reports its progress where an operator can read it. The line is written whatever the logging flags say. No band sends it unless its own cold load was built to, and a band that expects a server without it stops reporting rather than wait (docs/protocols.md, MINI).

ozd does no routing, so it cannot connect a site to the Global Chaosnet. A site that wants that runs cbridge, the Chaosnet bridge, as its switch instead of ozd. Each machine then names cbridge as its default CHUDP peer, and ozd becomes one more peer of cbridge, on a port of its own if both run on one computer. ozd still serves the machines, but it passes no packets between them. Through cbridge, a test host has reached ozd's STATUS, TIME, UPTIME and FILE; the Global Chaosnet has not been tried yet (docs/design.md §9).

Security

ozd is meant for a trusted network segment, and it does not authenticate anyone. Any host that can reach its socket gets an answer, from FILE as well. Such a host can read every root, and it can write, rename and delete files in every root that is not marked ,ro. LOGIN accepts any name and only records it; it is never a credential. So the address that ozd listens on decides who can use it. Without --listen, only this host can. With a network address or 0.0.0.0, every host that can reach that address can. ozd has no TLS and does no routing. At a site that joins the Global Chaosnet through cbridge, every host that cbridge lets through can reach ozd, FILE included.

A --tcp listener hands out whatever its contact is. Carried to TELNET, it is a Lisp top level with no login, for anyone who can reach the TCP port. So a listener binds the loopback unless you name an address, and it reaches only the one contact it names. Name a network address only on a segment you trust as much as the machine itself.

Clients can reach the files in the roots and nothing else. A pathname cannot climb out of its root. ozd follows a symbolic link only while the link 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. To serve a directory from elsewhere, mount it with --root <name>=<path> instead of linking to it (docs/design.md §6).

Two risks cannot be caught by checking paths, so the operator must prevent them:

  • The roots must belong to ozd's own user, and nobody else may write to them. A hard link inside a root to a file outside it looks like an ordinary file in the root, and so does a bind mount of another directory. If only ozd's user can write to a root, neither can appear there.
  • Run only one ozd per root, and make each ,ro root read-only on disk as well. ozd checks a path and then opens it. This is safe only because nothing else writes to a writable root in between, and nothing writes to a read-only root at all.

ozd refuses to run as root. It needs no privileges, because 42042 is an ordinary UDP port.

As a service

contrib/ozd.service is a systemd unit, and contrib/com.metebalci.ozd.plist is a launchd daemon. Both run ozd as a user of its own, which owns the writable roots and nothing else, as the Security section asks. The systemd unit is in use; the launchd daemon has not been tried yet.

On Linux with systemd, contrib/install-systemd.sh does the whole installation. Build ozd first, and then run the script as root from this directory:

sudo contrib/install-systemd.sh

The script creates the ozd user and the base root /srv/lispm, which that user owns. It installs the binary and the unit, and it writes a standard file of flags to /etc/ozdrc: the System 100 site of the Run section, with /srv/lispm as its base root. If /etc/ozdrc already exists, the script keeps it. It then checks the flags and the roots as the ozd user, and starts the service. With --dry-run, it prints what it would do and changes nothing. So far it has been checked only with --dry-run.

Edit /etc/ozdrc to suit your site, for example to mount the release's sources at tree or to set --listen, and then run sudo systemctl restart ozd.

To install by hand instead, create the user, give it the base root, and install the binary, a file of flags and the unit:

sudo useradd --system --no-create-home --shell /usr/sbin/nologin ozd
sudo mkdir -p /srv/lispm
sudo chown ozd: /srv/lispm
sudo install -m 755 target/release/ozd /usr/local/bin/ozd
sudo install -m 644 ozdrc /etc/ozdrc
sudo install -m 644 contrib/ozd.service /etc/systemd/system/ozd.service
sudo systemctl daemon-reload
sudo systemctl enable --now ozd

Here ozdrc is your file of flags, one flag per line, such as the flags in the Run section.

Either way, the unit lets ozd write only to /srv/lispm. If your writable roots are elsewhere, list them in the unit's ReadWritePaths= line. A root under /home also needs ProtectHome=read-only in place of ProtectHome=yes. The log goes to the journal: journalctl -u ozd.

On macOS, 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 to /Library/LaunchDaemons/.

How it was written

ozd is written with Claude Code, using Anthropic's Claude Opus.

Licence

ozd, the associated machine for a site of MIT CADR Lisp Machines.

Copyright (C) 2026 Mete Balci

This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.

This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details.

You should have received a copy of the GNU Affero General Public License along with this program. If not, see https://www.gnu.org/licenses/. The full text is in LICENSE.

About

The associated machine for a site of MIT CADR Lisp Machines: Chaosnet services over UDP

Topics

Resources

Stars

0 stars

Watchers

1 watching

Forks

Used by

Contributors

Languages