NextBSD · Porting plan · Execution

DirectoryServices on NextBSD — the file-level porting plan

Exactly what Apple source to vendor into nextbsd-userland/src, where every file installs on the filesystem, what to drop/replace in the FreeBSD-compat layer (PAM, NSS, membership), and a phased roadmap for directoryservicesd — the net-new daemon that stands in for the closed opendirectoryd.

Drafted 2026-07-16 from a four-agent source-grounded scope (install layout · src vendoring · compat/PAM · daemon architecture), each reading the real nextbsd-userland / nextbsd-freebsd-compat trees against the verified Apple open-source inventory. Supersedes the deferred scoping doc freebsd-opendirectory-port-plan at the execution level. Background: the directory-services history & source-availability research. Planning only — no code written.

Thesis

directoryservicesd is a net-new C daemon built on NextBSD's proven wland/configd pattern (bootstrap_check_in + a raw mach_msg MIG loop + a launchd job) that embeds the reusable Apple C++ plugin bodies from DirectoryService-621.12 in-process, behind the C DirServices.h API. It is not a port of Apple's ~50k-LOC C++ DirectoryService daemon core, and not the never-released opendirectoryd.

This resolves the old scoping doc's "Path A (port the C++ daemon) vs Path B (write a minimal daemon)" fork: we take Path B's daemon shell wrapping Path A's plugin logic — the smallest honest thing that runs local login, exactly how wland is a net-new shell around stock wpa_supplicant. Legally it also lands clean: the daemon is our code (functional name, wland precedent), the reused pieces keep their Apple names (compat identifiers), and nothing is branded opendirectoryd.

1Architecture: the full stack

Client → daemon → backend, and what lives above vs below the Mach line

┌──────────────── GERSHWIN (above the Mach line — libobjc2 + Foundation exist here) ─────────────┐
│  ObjC OpenDirectory.framework   ODNode / ODSession / ODQuery / ODRecord   (OpenDirectory-146)  │
│  Accounts pref pane · Directory-Utility-equivalent                                             │
└───────────────┬────────────────────────────────────────────────────────────────────────────────┘
                │  ObjC → CF-C
┌───────────────┼──────────────── nextbsd-userland (below the Mach line — NO ObjC) ───────────────┐
│  libCFOpenDirectory.so   (OpenDirectory-146 Core/, CoreFoundation-C twin — ODNodeRef/ODSessionRef)│
│  dscl · dsmbrutil · dscacheutil · pwpolicy   (DSTools-162 C/C++ CLIs)                            │
│               │  MIG subsystem `directoryservices` (flat scalars, NO out-of-line — see R4)      │
│      ╔════════▼══════════════════════════════════════════════════════════════════════════════╗  │
│      ║  directoryservicesd    NET-NEW C shell · /usr/sbin · launchd org.nextbsd.directoryservicesd║
│      ║   ├─ MIG server thread          (clone of WLAN/mach_service.c)                            ║
│      ║   ├─ dispatcher: node routing + AuthenticationAuthority                                   ║
│      ║   └─ in-process plugins behind DirServices.h   (DirectoryService-621.12, C++)             ║
│      ║        • Local  (dslocal plist)   [v0.1]   • BSD-flat (/etc/master.passwd fallback)[v0.1] ║
│      ║        • Cache / Search / Configure [v0.1]  • LDAPv3 (bind/search/replica)        [v0.2]  ║
│      ╚═════╤══════════════════════╤═══════════════════════╤═══════════════════════════════════════╝
│            │ ;ShadowHash; (local) │ ;ApplePasswordServer; │ ;Kerberosv5;                          │
│            ▼                      ▼                       ▼                                       │
│   in-proc hash verify     passwordserviced          libkrb5 (link, not a daemon)                 │
│   (DSPasswordServerPlugin  (NET-NEW C daemon +       MIT-krb5 shipped; KDC external              │
│    hash-slot logic)  [v0.1] Cyrus SASL)     [v0.3]                                    [v0.3]      │
└──────▲───────────────────────────────────────────────────▲──────────────────────────────────────┘
       │ nss_directoryservices.so (NSS → getpwnam/getgrnam) │ pam_directoryservices.so (PAM auth)

1.1 Process model — in-process plugins, defer XPC modules

Real opendirectoryd (10.7+) runs out-of-process XPC modules. NextBSD has launchd + libxpc + libdispatch, so that path is technically open — but two facts make it the wrong first move: dispatch_mach is unfinished in this port (configd already falls back to a raw mach_msg loop), and MIG out-of-line data is broken in this kernel. An XPC-module fabric leans on exactly those. The DirectoryService-621.12 plugins were written to run in-process behind DirServices.h anyway, and NextBSD's libCoreFoundation already ships CFPlugIn.c/CFBundle*.c — so the native in-process loader is reusable as-is. Choosing XPC modules would mean writing the module host and reverse-engineering the never-released odxpc schema; choosing in-process means compiling code that already exists the way it exists.

Keep the seam Structure plugins behind a stable in-process vtable (Initialize/ProcessRequest/Shutdown) so one could later be relocated behind an XPC transport without touching the dispatcher. That seam is exactly where a closed AD plugin would have to attach — which is the only reason to keep it clean, not a commitment to build AD (it stays permanently out, §8).

1.2 The ObjC / C++ split — forced by the tree

Verified by scanning the tree: the only .m files in nextbsd-userland are libdispatch internals and test fixtures — no Foundation, no NSObject, no libobjc2. So:

1.3 Mach service names — org.nextbsd.*

The two net-new daemons take functional NextBSD names (the wlandorg.nextbsd.wlan rule): org.nextbsd.directoryservices and org.nextbsd.passwordservice; launchd job labels org.nextbsd.directoryservicesd / …passwordserviced.

Reconciled tension One agent argued for keeping Apple's com.apple.system.DirectoryService.* Mach labels (because Apple's libinfo looks them up); another argued for org.nextbsd.*. Resolution: org.nextbsd.* wins — because (a) NextBSD abandons Apple's libinfo/lookupd path for FreeBSD nsswitch (§6), so those libinfo_v1 labels are moot, and (b) we author the OD transport glue ourselves, so nothing Apple-derived hard-codes a label we must match. Fabricating com.apple.opendirectoryd for a NextBSD daemon would be the "fake Apple label" the mach_service.c comments explicitly reject. Mitigation (R9): grep the reused DSTools binaries at port time for a baked-in com.apple.private.opendirectoryd.rpc lookup; if present, patch it or register a bootstrap alias. (Contrast configd/notifyd, which keep com.apple.* precisely because they model a released Apple daemon that Apple-derived code binds to by that name.)

2What to vendor into src/

Three userland components + one Gershwin framework — not a monolith

The daemon (C++/Mach), the password engine (separate Apple repo, separate trust boundary + BSD-licensed SASL), and the CLIs have different toolchain/link needs; and the ObjC OD API physically can't compile in userland. So the port splits:

# ── nextbsd-userland/src/ (below the Mach line) ──
src/DirectoryService/          # directoryservicesd daemon + libDirectoryService (C API)
  directoryservicesd.cpp       WRITE-NEW  main(): liblaunch bootstrap + dispatch + MIG loop
  CServerPlugin.cpp CHandlers.cpp Mbrd_*.cpp CLog/CPlugInList/CRefTable.cpp
                               VENDOR     DirectoryService-621.12 Server/ (in-proc CFPlugIn host)
  directoryservices.defs  mach_service.cpp  ds_mig.cpp
                               WRITE-NEW  MIG surface, modeled on WLAN/wlan.defs + mach_service.c
  plugins/{BSD,Cache,Local,Search,Configure,Common}/*.cpp
                               VENDOR     DirectoryService-621.12 PlugIns/ (6 built-ins, static-linked)
  plugins/LDAPv3/*.cpp         VENDOR-DEFERRED  needs OpenLDAP libldap; gate off until v0.2
  api/DirServices.cpp  include/DirectoryService/DirServices.h
                               VENDOR     APIFramework/ — C++ impl behind the C header

src/libCFOpenDirectory/        # CoreFoundation-C twin — NO ObjC
  CFOpenDirectory.c CFOD{Node,Record,Query,Session}.c + headers
                               VENDOR     OpenDirectory-146 Core/

src/PasswordService/           # net-new password-auth daemon (v0.3)
  passwordserviced.cpp  pws.defs  pws_mig.cpp
                               WRITE-NEW  daemon host (never released by Apple)
  AuthFile.c CAuthFileBase.cpp DES.c SMBAuth.c KerberosInterface.c
                               VENDOR     DSPasswordServerPlugin-281.1 (hash-slot / policy)
  PolicyBase.m → .c   CPSPlugIn.mm → .cpp
                               REWRITE    strip ObjC/ObjC++ → C/C++
  sasl/                        VENDOR|PORTS  passwordserver_sasl-215 (Cyrus SASL, BSD) — or base cyrus-sasl2 [U]

src/DSTools/                   # CLIs
  dscl.m → dscl.c              REWRITE    ObjC → C over DirServices.h (WLAN CLI pattern)
  dscacheutil.c dsmbrutil.c dsconfigldap.c
                               VENDOR     DSTools-162 (already C)
  pwpolicy.cpp                 VENDOR     DSTools-162 (C++)

# ── Gershwin (above the Mach line) ──
Gershwin …/OpenDirectory.framework/  ODSession.m ODNode.m ODRecord.m ODQuery.m
                               VENDOR-INTO-GERSHWIN  OpenDirectory-146 Framework/ (ObjC)
Not in any reusable tree — permanently absent The Active Directory plugin (always closed), the PasswordServer daemon and opendirectoryd (never released → written new), and NetInfo (retired; a cautionary reference, not a target). dseditgroup/dsimport/dsconfigad are missing from DSTools-162 → write-new or omit (dsconfigad needs the absent AD plugin → omit).

3The directoryservicesd Makefile

Modeled line-for-line on src/WLAN/Makefile — with the C++ and CFPlugIn deltas called out

PROG=		directoryservicesd
MAN=		directoryservicesd.8
BINDIR=		/usr/sbin

SRCS=		directoryservicesd.cpp mach_service.cpp ds_mig.cpp
SRCS+=		CServerPlugin.cpp CHandlers.cpp CPlugInList.cpp CRefTable.cpp CLog.cpp
SRCS+=		Mbrd_MembershipResolver.cpp Mbrd_HashTable.cpp Mbrd_Cache.cpp
.PATH:		${.CURDIR}/plugins/BSD ${.CURDIR}/plugins/Cache ${.CURDIR}/plugins/Local \
		${.CURDIR}/plugins/Search ${.CURDIR}/plugins/Configure ${.CURDIR}/plugins/Common
SRCS+=		CBSDPlugin.cpp CCachePlugin.cpp CDSLocalPlugin.cpp \
		CSearchPlugin.cpp CConfigurePlugin.cpp CBasePlugin.cpp

# MIG server stub: directoryservices.defs → directoryservicesServer.c (host mig, MIGOUT=)
MIGOUT?=
.if !empty(MIGOUT)
.PATH:				${MIGOUT}
SRCS+=				directoryservicesServer.c
CFLAGS+=			-I${MIGOUT} -I${.CURDIR}
CFLAGS.directoryservicesServer.c+= -w
CFLAGS+=			-Wno-macro-redefined   # mach/notify.h vs mach/message.h MACH_NOTIFY_*
.endif

WARNS=		0                              # plugins pull the CF public header tree
CFLAGS+=	-O2 -pipe -fblocks
CXXFLAGS+=	-O2 -pipe -fblocks -std=c++17 -fno-exceptions -fno-rtti
CFLAGS+=	-I${.CURDIR}/../launchd/liblaunch -I${.CURDIR}/../launchd/freebsd-shims
CXXFLAGS+=	-I${.CURDIR}/../launchd/liblaunch -I${.CURDIR}/../launchd/freebsd-shims

SYSROOT?=
.if !empty(SYSROOT)
CFLAGS+=	-I${SYSROOT}/usr/include
CXXFLAGS+=	-I${SYSROOT}/usr/include
LDFLAGS+=	-L${SYSROOT}/usr/lib/system -Wl,-rpath,/usr/lib/system -Wl,--allow-shlib-undefined
.endif

# Link line — the WLAN-fixed system_* names, PLUS CFPlugIn + the C++ runtime.
LDADD+=		-llaunch -lsystem_kernel          # bootstrap_check_in + mach_msg
LDADD+=		-lCoreFoundation                  # CFBundle/CFPlugIn (in-proc plugin load)
LDADD+=		-lsystem_dispatch -l:libsystem_blocks.so
LDADD+=		-lc++ -lcxxrt                     # [R1] first C++ daemon in tree — UNVERIFIED link
# NOT -ldispatch / NOT -lBlocksRuntime  (#378 rtld collision)
LIBADD+=	pthread
.include <bsd.prog.mk>

The C API library libDirectoryService.so (api/DirServices.cpp + the public C header) is a sibling bsd.lib.mk target the daemon does not link but every CLI and libCFOpenDirectory does — same relationship as configdlibSystemConfiguration.


4Filesystem install layout

Four-domain rules: bare /Library/X/Local/Library/X; /System/Library & /var stay; libs → /usr/lib/system; daemons → /usr/sbin

Verified constraint There is no /System/Library/Frameworks on NextBSD — every Apple .framework ships as a versioned .so in /usr/lib/system with headers under /usr/include/<Name>/ (the libSystemConfiguration pattern). And there is no plugin loader — the DS plugins are statically linked into the daemon, so there are no .dsplug files on disk.
ArtifactApple originalNextBSD path
Directory daemon/usr/libexec/opendirectoryd (closed)/usr/sbin/directoryservicesd
Password daemon v0.3/usr/sbin/PasswordService (never released)/usr/sbin/passwordserviced (name = open decision)
OpenDirectory CF-C twin/System/Library/Frameworks/OpenDirectory.framework/usr/lib/system/libCFOpenDirectory.so + /usr/include/CFOpenDirectory/
OpenDirectory ObjC layersame frameworkGershwin /System/Library/Frameworks/OpenDirectory.framework
DirectoryService C API/System/Library/Frameworks/DirectoryService.framework/usr/lib/system/libDirectoryService.so + /usr/include/DirectoryService/
All DS plugins…/Resources/Plugins/*.dsplugstatically linked into the daemon — no on-disk file
launchd plists(closed)overlay/System/Library/LaunchDaemons/org.nextbsd.directoryservicesd.plist (+ …passworderviced…)
Mutable OD config/Library/Preferences/OpenDirectory//Local/Library/Preferences/OpenDirectory/ (runtime-created)
Local record store/var/db/dslocal/nodes/Default//var/db/dslocal/nodes/Default/ (unchanged — already /var-qualified)
LDAP mapping templates/System/Library/OpenDirectory/Templates//System/Library/OpenDirectory/Templates/ (System domain, read-only overlay)
CLIs/usr/bin/dscl/usr/bin/{dscl,dscacheutil,dsmbrutil,pwpolicy,dserr}
Mach service namescom.apple.system.opendirectoryd.*org.nextbsd.directoryservices (§1.3)

Overlay additions

overlay/System/Library/LaunchDaemons/org.nextbsd.directoryservicesd.plist   new
overlay/System/Library/LaunchDaemons/org.nextbsd.passwordserviced.plist     new (v0.3)
overlay/System/Library/OpenDirectory/Templates/LDAPv3/                       new, read-only
overlay/private/var/db/dslocal/nodes/Default/users/{root,daemon,nobody}.plist seed [see R below]
overlay/private/var/db/dslocal/nodes/Default/groups/{wheel,staff,…}.plist    seed
overlay/private/etc/openldap/schema/apple.schema                             only if OD-server (v0.4+)

The plist mirrors org.nextbsd.wland.plist (RunAtLoad, KeepAlive, a MachServices dict the daemon claims via bootstrap_check_in). Since there's no launchd ordering and no first-boot provisioner, the dslocal tree must be seeded at image-assembly time via the overlay, not by the daemon on first run — [U] confirm assemble-image.sh recurses into overlay/private/var].


5Compat: drop / replace / add (PAM & NSS)

Where the auth glue actually lands — and it's not in nextbsd-freebsd-compat

Repo topology finding nextbsd-freebsd-compat is fbsdglue — scaffold-only, ships no PAM/pam.d/OpenPAM. All directory-auth work lands in nextbsd-userland, which already ships Apple OpenPAM-35 as libpam.so.6 (replacing FreeBSD-pam in place) and Apple OpenBSM libbsm. OpenPAM on both sides ⇒ Apple's PAM modules port with zero PAM-API shim.

5.1 PAM — write pam_directoryservices.so

Apple's pam_opendirectory.c is open (apple-oss-distributions/pam_modules) and is an OpenPAM module — but it calls the OpenDirectory framework, a thin XPC client to the closed opendirectoryd. Since our daemon is the net-new directoryservicesd, there's no closed schema to match. Verdict: keep pam_opendirectory.c as the structural template (the auth/account/password/session function-class layout, authtok conversation), but replace every od_record_* call with a ds_* client call into directoryservicesd. Critically, the module knows nothing about ShadowHash vs PasswordServer vs Kerberos — it calls one verb, ds_record_verify_password(), and the daemon dispatches on AuthenticationAuthority (§7). Installs to /usr/lib/pam/pam_directoryservices.so.6 ([U] confirm OPENPAM_MODULES_DIR).

# overlay/private/etc/pam.d/login  — pam_unix kept as flat-file fallback
auth       optional   pam_directoryservices.so   # try directory first
auth       required   pam_unix.so                # /etc/master.passwd fallback
account    required   pam_nologin.so
account    required   pam_directoryservices.so
password   required   pam_directoryservices.so
session    required   pam_uwtmp.so
session    optional   pam_directoryservices.so

5.2 Name service — nsswitch, not Apple libinfo

macOS routes getpwnam through Libinfo→lookupd→the ds module. NextBSD has deliberately not ported that path (the libinfo.h shim's si_search_module_set_flags is a no-op). So visibility comes from FreeBSD nsswitch: add nss_directoryservices.so (template = the in-tree nss_mdns.c) implementing getpwnam_r/getpwuid_r/getgrnam_r/getgrgid_r/getgrouplist as a thin MIG client to the daemon, plus an /etc/nsswitch.conf overlay: passwd: files directoryservices (files first = single-user safety).

ComponentActionDetail
pam_directoryservices.soADDCloned from open pam_opendirectory.c; od_record_*ds_*. → /usr/lib/pam/
pam.d {login,su,sshd,passwd,other}ADDOverlay policy; pam_unix retained as fallback
nss_directoryservices.soADDnsswitch module, template nss_mdns.c/usr/lib/nss_directoryservices.so.1
/etc/nsswitch.confADDpasswd/group: files directoryservices
membership.h mbr_* stubsREPLACECurrently ENOENT; make them daemon clients (uid↔UUID↔group) — needed for ACL/nested groups & Apple id
libinfo.h shimKEEP (no-op)Apple libinfo path stays abandoned
FreeBSD /etc/pam.d/*DROPReference un-vendored FreeBSD-only modules; superseded by overlay set
flat-file master.passwd/pwKEEP, demoteStay as the files source + bootstrap authority; lose primacy, not presence
MIT libkrb5.so.122KEEPcurl/git ABI; fine for local ShadowHash (no KDC) & plain network krb5 (§7)

5.3 Kerberos: MIT (shipped) vs Heimdal (Apple)

NextBSD ships MIT krb5 1.22.1; Apple's directory-Kerberos stack is Heimdal-coupled (LKDC, hdb, the Password Server replica protocol). Impact: non-issue for ;ShadowHash; local login (no KDC involved — the whole v0.1 case); fine for plain ;Kerberosv5; against any RFC-4120 KDC (link MIT libkrb5, call krb5_get_init_creds_password); a hard blocker only for Apple LKDC / Password Server SSO (Heimdal-only) — which is deferred anyway. If ever needed, add FreeBSD's Heimdal port beside MIT for the daemon only (keep MIT's SONAME for curl/git).

5.4 FreeBSD account-management tools

The principle is split by store: system/service accounts stay in /etc/master.passwd (the flat-file fallback), while directory/human accounts live in the directory (plists), managed by dscli/directoryservicesd. So each base account tool is keep / remove / route depending on which store it writes. The actual removals are narrow — only the human-account convenience layer (adduser/rmuser). The low-level flat-file tools stay, because system/service accounts still live in master.passwd and the base + ports depend on them.

ToolDispositionWhy
pwKEEPLoad-bearing — pkg/ports install scripts create service accounts with pw useradd -n _foo. Remove it and package installs break. Writes /etc/master.passwd (system/service accounts = correct target). Just not the tool for human users.
adduser / rmuserREMOVEInteractive wrappers around pw that create human accounts in the flat file — the wrong store in this model, and a confusing second path. Replaced by dscli user add / dscli user delete.
/etc/adduser.conf, /etc/adduser.msgREMOVEConfig for adduser; goes with it.
chpass / chfn / chshKEEP (system) / route (directory)Edit master.passwd gecos/shell for system accounts. A directory user changes those via dscli edit, not these.
passwd(1)KEEP (system) / route (directory)For system accounts, stock passwdmaster.passwd. For a directory user it must hit the daemon — either dscli passwd, or the pam.d/passwd stack routes them through pam_directoryservices.
vipw, pwd_mkdbKEEPLow-level master.passwd edit / db rebuild — system accounts only.
Do not remove pw — it is the single most load-bearing account tool for a FreeBSD-derived base; every service account a package creates goes through it.

These tools come from the FreeBSD base closure via nextbsd-freebsd-compat; the exact in-closure set and the ports USES= that depend on pw should be verified against the live srclist before the removals land. dscli vs the Apple dscl naming is an open unification question.

5.5 Serving macOS clients — home-path translation

If a Mac binds to the (future, deferred) LDAP front-end, it authenticates fine, but its home must be presented in macOS's namespace, not NextBSD's /Local convention. A Mac is not a NextBSD client: it has no /Local domain, doesn't run directoryservicesd, and doesn't NFS-mount /Local as /Network. Hand a Mac NFSHomeDirectory: /Local/Users/jmaloney and it logs in with a broken/missing home — the path is meaningless there. So the plist→LDAP bridge must rewrite the home per client; the raw /Local path never reaches a Mac.

Gotcha — NFSHomeDirectory is a path, not a protocol Despite the name, NFSHomeDirectory (native homeDirectory) is just the local POSIX home path (what $HOME/pw_dir becomes) — the "NFS" is vestigial. The mount protocol comes from the separate HomeDirectory attribute (native apple-user-homeurl), whose URL scheme selects the transport: afp:// / nfs:// / smb://. No URL at all → local home, nothing mounts. So a home can sit at an NFSHomeDirectory path yet mount over AFP, or not mount at all.
ClientNFSHomeDirectory (path)HomeDirectory URL (protocol)Result
NextBSD (directoryservicesd)/Local/Users/jmaloney (→ /Network/Users/… on a client)Gershwin four-domain convention
Modern Mac (recommended)/Users/jmaloneynoneLocal home — centralized login, no network home (the realistic modern pattern)
Vintage Mac (network home)/Network/Servers/nextbsdserver/Users/jmaloneyafp://nextbsdserver/Users (or nfs://)autofs mounts AFP/NFS home there

uid must match: a network home over NFS/AFP uses AUTH_SYS ownership — if jmaloney is uid 5001 on NextBSD, the Mac must act as uid 5001 (it will, from the directory) or it gets permission-denied on its own home. This section describes the deferred Mac-facing LDAP front-end; it does not affect the NextBSD/Linux plist path.


6AuthenticationAuthority dispatch

The daemon reads each record's authority attribute and routes the credential — the PAM/NSS clients never see this

Authority tagRouteComponentPhase
;ShadowHash;<slots>in-process hash verify vs ShadowHashDataDSPasswordServerPlugin hash-slot logic (SALTED-SHA512 / PBKDF2)v0.1
;ApplePasswordServer;…IPC to passwordservicednet-new daemon + Cyrus SASL (CRAM/DIGEST-MD5, NTLM, MS-CHAPv2) + policy enginev0.3
;Kerberosv5;…link libkrb5, get_init_credsMIT krb5 (library call; KDC external)v0.3
;ActiveDirectory;closed pluginnever

v0.1 needs only the ;ShadowHash; path — reusing the hash logic in-process, with no passwordserviced at all. That keeps v0.1 to "daemon shell + Local plugin + local hash verify."


7Phased roadmap

Each phase mapped to the deferred scoping doc's end-states (§5.1 OD client / §5.2 OD+AD / §5.3 OD server)

v0.1 Local-only self-hosting box

Goal: boot and log in with directory-backed local accounts — getpwnam() and PAM both served by directoryservicesd, not /etc/master.passwd (except system accounts).

  1. directoryservicesd C shell — clone mach_service.c; bootstrap_check_in("org.nextbsd.directoryservices"); raw mach_msg loop.
  2. directoryservices.defs MIG subsystem (base id ~31000, clear of configd 20000 / wland 30000) — flat-scalar, no OOL (R4): ds_open_node, ds_get_record, ds_get_attr_value (one value per round-trip), ds_verify_password, ds_query_begin/next.
  3. Embed Local (dslocal) + thin BSD-flat plugins behind DirServices.h; seed /var/db/dslocal from /etc/master.passwd.
  4. In-process ;ShadowHash; verify.
  5. nss_directoryservices → getpw*/getgr* over MIG (non-blocking with flat-file fallback, R7).
  6. pam_directoryservices.so swapped into the login / LoginWindow policy.
  7. C dscl CLI over MIG — the ground-truth admin tool.

Exit test: dscl . -create /Users/alicegetent passwd alice resolves → LoginWindow authenticates alice, with no line in /etc/master.passwd.

v0.2 Network directory — the full "OD client" (§5.1)

Embed LDAPv3 plugin; dsconfigldap binds a node; search-policy ordering (Local then LDAP); ObjC OpenDirectory.framework ships in Gershwin on the CF-C twin (Accounts / Directory-Utility UI). Exit: a network LDAP user logs into the box — exactly the scoping doc's §5.1 end-state, executed.

v0.3 Password Server + Kerberos reality

passwordserviced net-new daemon (Cyrus SASL mechanisms + auth-file store + password policy: lockout/expiry/history/complexity — the biggest gap the scoping doc named); ;ApplePasswordServer; + ;Kerberosv5; dispatch wired; nested groups via a memberd-equivalent folded into the daemon.

Beyond & permanently out


8Risks & open decisions

#Risk / questionAssessment
R1C++ in this treedirectoryservicesd is the first bsd.prog.mk C++ daemonHighest build risk. Smoke-test a one-file C++ stub (-lc++ -lcxxrt, -fno-exceptions/-rtti) against vendored CF/Mach headers under -fblocks before vendoring Server/. Keep the shell in C; C++ confined to plugin bodies behind a C ABI.
R4MIG OOL broken + records are big — a full directory record easily exceeds an inline messageCentral IPC design problem. Never return a whole record — one attribute value per round-trip + paging tokens; big blobs (hash/cert) via chunked offset+len RPCs or a 0600 file-handoff (the wland run-dir pattern). Softener: the daemon has its own store, not bound by configd's 8 KiB — the real ceiling is the mach_msg buffer (configd proves 32 KiB works). [U] measure it.
R7No launchd ordering — NSS/PAM callers (even launchd itself) may run before the daemonNSS module must be non-blocking with a flat-file fallback, or a boot-time getpwnam deadlocks. v0.1 gating design item.
gershwin-DS role conflict — NextBSD may already ship a nss_gershwin user/group backend (plaintext, TCP/4721)Unresolved authority overlap. The dslocal path is free, but two directory backends answering getpwnam will race. Pick one authority (this plan assumes directoryservicesd supersedes gershwin-DS) — decide before v0.1 seeds root/wheel.
R5No keychain — LDAP bind creds / machine secret have nowhere encrypted to livev0.1 needs none (ShadowHashData is already a hash, 0600 in dslocal — matches macOS). v0.2 bind cred → plaintext 0600, documented (wland precedent). Real secret-store is a separate future project; don't block.
R6/R9Open naming decision — the password daemon; and reused DSTools baked-in Mach labelsPassword daemon working name passwordserviced (parallel to directoryservicesd) — your call, v0.3 so not urgent. Grep DSTools at port time for com.apple.private.opendirectoryd.rpc; patch or alias if found.

9Naming & provenance

The naming boundary is drawn to line up with the authorship and open-source boundaries, which keeps trademark exposure on the parts that are entirely ours: