NextBSD · Userland daemon · Plan
afpdA clean-room Apple Filing Protocol server so NextBSD can serve files to Macs — the "constant" that works over either UFS or HFS+ underneath, because the fork-storage layer is abstracted. The authentic file-sharing half of the old-Mac stack.
Feasible and clean, but a real build — the biggest write-new component in either filesystem track, because there's no permissive code to wrap (netatalk is GPL, Apple's server is closed), so the protocol engine is fully original. It's the wland precedent taken further: wland shelled a stock supplicant; AFP has nothing to shell.
fork_backend vtable, three backends (UFS2 extended attributes, AppleDouble sidecars, HFS+ native). The DSI/AFP engine never knows which is underneath — so the same server serves UFS or HFS+.pam_directoryservices/PAM + NSS — the server never touches the credential store. DHX2 first (modern default), cleartext for old Macs, Kerberos later._afpovertcp._tcp), OpenPAM, launchd (socket-activated on TCP 548), LibreSSL (DHX2 crypto), SQLite (CNID). Write-new: the DSI+AFP engine, UAMs, fork abstraction, CNID allocator.Transport: DSI over TCP 548. A fixed 16-byte DSI header frames every message; small command set, all MVP: OpenSession/CloseSession, Command (carries AFP requests), Write, GetStatus (pre-login server info), Tickle (keepalive), Attention (server→client). Skip AppleTalk entirely — DSI-over-TCP covers AFP 2.2 onward.
Versions: target AFP 3.x primary (advertise AFP3.1–AFP3.4) — TCP-only, UTF-8 filenames, POSIX permissions, 64-bit forks; AFP 3.4 is a minor error-code tweak. Add AFP 2.2 (AFPX03) secondary for System 7 → OS 9 (31-byte Mac-Roman names). [U] the earliest classic Mac OS that speaks AFP-over-TCP vs AppleTalk-only — very early System 7 may be AppleTalk-only, which this server won't serve.
| Group | Commands |
|---|---|
| Session / auth | FPGetSrvrInfo, FPLogin/FPLoginExt, FPLoginCont, FPLogout, FPGetSrvrParms, FPMapID/FPMapName |
| Volume | FPOpenVol, FPCloseVol, FPGetVolParms |
| Catalog / dir | FPGetFileDirParms, FPSetFileDirParms, FPEnumerateExt2, FPCreateDir/FPCreateFile, FPDelete, FPRename/FPMoveAndRename, FPResolveID/FPCreateID (CNID) |
| Forks | FPOpenFork (data or resource), FPRead(Ext), FPWrite(Ext), FPFlushFork, FPCloseFork, FPSetForkParms (EOF) |
| Locking | FPByteRangeLock(Ext) — mandatory, Mac clients rely on it |
| "Looks optional, Finder breaks without" → MVP | FPExchangeFiles (safe-save atomic swap), FPByteRangeLockExt |
| Later | FPCatSearch, FPGetACL/FPSetACL, FPGetExtAttr/FPSetExtAttr (AFP 3.2+) |
AFP auth is a pluggable UAM challenge/response in FPLogin+FPLoginCont. Implementation order:
"DHX2") — the modern default: 1024-bit Diffie-Hellman, session key MD5(K), CAST5-CBC, fixed IVs, a 16-byte nonce exchange for mutual auth. Crypto via LibreSSL/OpenSSL BN+EVP (BSD-friendly), not libgcrypt (LGPL — netatalk's choice; pick differently to stay clean-room)."Cleartxt Passwrd") — 8-char, for System 7 → OS 9 that can't do DHX2. Gate behind opt-in."DHCAST128") — bridges OS 9 / early OS X. Optional if DHX2+cleartext cover the matrix./etc/pam.d/afpd stack mirroring login (auth optional pam_directoryservices.so → auth required pam_unix.so). Cleartext hands the password straight to pam_authenticate. DHX/DHX2/Kerberos recover the plaintext inside the UAM after decryption, then hand that to pam_authenticate — so the AFP server never reads master.passwd or the directory schema directly. User→uid/gid resolution goes through NSS (nss_directoryservices + getpwnam). Same arm's-length principle as the whole directory design. UAMs are internal statically-linked modules (a struct afp_uam table), not dlopen'd plugins — tighter trust boundary.Every AFP object has a data fork, a resource fork, and Finder info (+ a stable CNID) that the server must serve regardless of backing FS. One interface, swappable backends — this is the whole point: the DSI/AFP engine never knows which is underneath.
struct fork_backend {
ssize_t (*rsrc_read/write)(vol, path, off, buf, len);
int (*finderinfo_get/set)(vol, path, uint8_t fi[32]);
int (*meta_get/set)(vol, path, struct afp_meta*); /* AFP flags, comment, CNID */
};
| Backend | How |
|---|---|
UFS2 EA (ea=sys, default) | Resource fork + metadata in UFS2 extended attributes (extattr_get/set_file, USER namespace). Adopt netatalk's documented on-disk EA/AppleDouble-v2 layout (a data format, not GPL code) for migration + dual-server interop. [U] verify UFS2 per-EA size limit — if a resource fork can exceed it, fall back to an AppleDouble sidecar for that object. |
AppleDouble sidecar (ea=ad) | .AppleDouble/ + ._name files (AppleDouble v2). For exports/filesystems without usable EAs (NFS-backed shares). This is exactly how Apple's own UFS stored forks — direct Apple precedent. |
| HFS+ native | Resource fork via ..namedfork/rsrc / the HFS attr file; FinderInfo native. Selected by the vtable when a real HFS+ mount is present — the AFP command layer is unchanged. |
CNID (Catalog Node ID) — AFP needs a stable 32-bit file ID surviving renames/moves. UFS inodes are stable but 64-bit + reused, so a persistent map is required: SQLite (public domain) per-volume .cnid.db. A real subsystem (allocation, rename tracking, orphan reaping), not a stub. ACLs — AFP's NFSv4-style ACEs map almost directly to FreeBSD NFSv4 ACLs on UFS2 (acl_*_file, ACL_TYPE_NFS4). Case — the server is case-policy-neutral; it advertises case-insensitivity in FPGetVolParms only when the backing mount (the UFS casefold patch or HFS+) actually provides it.
launchd (socket-activated, TCP 548) ─┐ Bonjour: _afpovertcp._tcp via mDNSResponder
▼
afpd parent ── accept() ── fork() per connection ── child drops priv to authed user
│
├─ ① DSI transport (framing, tickle, request/reply demux) write-new
├─ ② AFP command dispatch (FPxxx jump table, per-session state) write-new
├─ ③ UAM engine (cleartext / DHX2 / DHX) ── PAM ── directoryservicesd
├─ ④ VFS / volume layer (path resolve, FPEnumerate, perms) write-new
├─ ⑤ fork_backend vtable (EA · AppleDouble · HFS+-native) write-new
└─ ⑥ CNID store (SQLite per volume) reuse+gluenextbsd-userland/src/afp/, C, bsd.prog.mk, PROG=afpd, BINDIR=/usr/sbin; man pages as NextBSD-originals. Link mirrors wland + -lpam, the crypto lib, SQLite.org.nextbsd.afpd with socket activation (Sockets dict, port 548) — not a Mach service. An optional MachServices org.nextbsd.afp can back an afpctl admin CLI (list/kick sessions) the way wlan↔wland talk — but that's management, not the data path.org.nextbsd.* — Apple's AFP server is closed and net-new here; both ends authored in-tree, so no com.apple.* "fake Apple label."Reuse vs write-new: reuse — mDNSResponder, OpenPAM + pam_directoryservices + NSS, launchd, LibreSSL (DHX2), SQLite (CNID), FreeBSD extattr/acl/sendfile/flock. Write-new (clean-room, netatalk as design reference only — zero GPL code): the DSI+AFP engine, UAM handshakes, fork abstraction, CNID allocator, config parser.
| Milestone | Effort |
|---|---|
| DSI + fork model + launchd/mDNS + login-cleartext (first read-only mount) | ~1–1.5 mo |
| Full MVP catalog+fork+write+locking, AFP 3.x, DHX2, EA backend + CNID, homes | +3–5 mo |
AppleDouble fallback, AFP 2.2 (classic Macs), ACL mapping, FPExchangeFiles, hardening | +2–3 mo |
| MVP → trustworthy daily driver | ~6–9 mo |
| Kerberos UAM, HFS+-native backend, full interop matrix | v0.2+ |
| Risk | |
|---|---|
| Clean-room correctness across Mac versions (highest) | AFP is under-documented (Apple's specs are archived/incomplete). FPEnumerate flags, error-code mapping, Finder's exact FPExchangeFiles/lock expectations are empirical — a long tail of "Finder does something weird," found only by testing against real System 7 / OS 9 / OS X / macOS 11–26 clients. A physical/VM Mac test matrix is mandatory. |
| DHX2 crypto correctness | Fixed IVs, MD5(K), CAST5-CBC, nonce byte-order must match bit-for-bit or auth silently fails. [U] confirm CAST5-CBC is enabled in NextBSD's LibreSSL (legacy cipher, may be in the legacy provider). |
| Fork-storage edge cases | UFS2 per-EA size vs large resource forks; atomicity of data-fork + metadata-EA writes (crash between = orphaned metadata); ._ files leaking to non-Mac clients; CNID stability across mv/restore/FPExchangeFiles (CNID-DB corruption = Finder aliases break). |
| Byte-range locking | AFP mandatory-ish locks vs FreeBSD advisory locks — impedance mismatch; old multi-client apps (FileMaker, Quark) are the stress case. |
| Case dependency | Correctness on mixed-case names needs the UFS casefold patch (or HFS+). |
| AFP deprecation (strategic) | Server unaffected; Apple drops the client (macOS 15.5 deprecated → macOS 27 removes). Serves old Macs beautifully; modern macOS 27+ can't mount it. [U] the "macOS 27 ships with no AFP client" claim is from a single dev-beta report — high-confidence-but-unverified until GA. |