A clean-room daemon that picks a sensible default hostname from hardware identity at first boot, sets kern.hostname, and publishes Apple's three-name shape (ComputerName / LocalHostName / HostName) to configd's SCDynamicStore so mDNSResponder and other consumers can subscribe. Closest port we can do today of Apple's configd SetHostname plugin, kept as a standalone daemon because this repo's configd has no plugin loader (see configd plan's "deferred" list). Companion to launchd, configd, hwregd, mDNSResponder, IPConfiguration.
src/hostnamed/.FreeBSD/amd64 (Amnesiac). Synthesize a hardware-derived default name (ThinkPad-T420-8AB123, Mac-mini-F12345, …), publish via SCDynamicStore so mDNSResponder has a real .local name to announce, and respect user overrides via SCPreferences once that surface exists.hwregd / ipconfigd / diskarbitrationd took — this repo's configd doesn't load .bundle plugins yet (deferred per the configd plan). Standalone daemon is the consistent shape; if a plugin loader lands later, hostnamed's body lifts into a plugin in <200 LOC of glue.Plugins/IPMonitor/set-hostname.c (rolled into IPMonitor in recent configd; there is no standalone SetHostname plugin anymore). That file is APSL 2.0, ~500 LOC, and implements precedence-based decision + DHCP option 12 + reverse-DNS PTR fallback + the notify_post broadcast. What it does not do: synthesize a default name from hardware identity — that lives in Setup Assistant (closed source) on real Macs. So our daemon ports the open-source piece verbatim and clean-rooms the synthesis fallback that fills Setup Assistant's role.kenv + SMBIOS for synthesis fallback → (iter 1) writes synthesized name to SCDynamicStore Setup:/System + Setup:/Network/HostNames via libSystemConfiguration → calls sethostname(2) → notify_post("com.apple.system.hostname") → exits. Later iters add SCPreferences awareness, the vendored set-hostname.c precedence/DHCP integration, and Bonjour-conflict feedback.set-hostname.c, possibly bits of SCDHostName.c's validation helpers) stay APSL 2.0 with their original headers, exactly like configd / IPConfiguration. New clean-room synthesis + glue stays BSD-2-Clause. Same per-file pattern the rest of freebsd-launchd-mach uses.Provide a working /usr/sbin/hostnamed on freebsd-launchd-mach so:
FreeBSD/amd64 (ThinkPad-T420-8AB123) instead of (Amnesiac).$(hostname) returns the synthesized name (not Amnesiac).State:/Network/HostNames and announce <name>.local over Bonjour without rolling its own gethostname-and-hope path.kern.hostuuid).ComputerName from the iCloud account name during first boot — closed source, Apple-OOBE specific. Our synthesis fallback fills the same role; the closest user-facing knob is a kenv hostname.override set in /boot/loader.conf before imaging.ComputerName is already in preferences.plist, skip synthesis» check. iter 1's synthesis is stable across reboots (inputs are persistent factory identity) so the user-visible name doesn't flap; iter 2 just adds the user-set-beats-synthesis precedence.set-hostname.c adopts the DHCP-server-provided hostname (RFC 2132 option 12) when the user hasn't manually set one. iter 3 vendors that file — brings DHCP option 12, reverse-DNS PTR fallback, and Apple's exact precedence logic in one port. iter 1 just does synthesis + write; iter 3 is the "decision engine" port.set-hostname.c falls back to a reverse-DNS PTR lookup on the primary IP if no preferences / DHCP name is available. iter 3 brings this with the rest of the vendored decision engine..local name collision on the LAN and renames itself foo-2.local, it should push the new name back into SCPreferences so the next boot picks the conflict-free name. Apple does this; deferred to iter 4 (depends on iter 2's SCPreferences write surface).ComputerName from the boot-volume label. Not a real path for us (we mount UFS).| Piece | Apple source | Strategy |
|---|---|---|
| Precedence-based hostname decision (prefs > DHCP > reverse-DNS PTR > mDNS > localhost) | Plugins/IPMonitor/set-hostname.c (~500 LOC, APSL 2.0) |
Vendor in iter 3. Apple-canonical. DHCP option 12 + PTR lookup come along for free. |
sethostname(2) + notify_post("com.apple.system.hostname") |
same file | Vendor as part of iter 3. iter 1 calls sethostname(2) + the same notify_post directly — tiny enough to clean-room until iter 3 lands. |
SCDynamicStore key paths + key-creation helpers (SCDynamicStoreKeyCreateComputerName, SCDynamicStoreKeyCreateHostNames) |
SystemConfiguration.fproj/SCDHostName.c + the SCSchemaDefinitions constants |
Already vendored as part of our libSystemConfiguration. iter 1 calls the existing helpers — Setup:/System and Setup:/Network/HostNames. |
| LocalHostName validation (RFC 1035 LDH, reject dots, length cap) | SCDHostName.c uses _SC_CFStringIsValidDNSName() |
Reuse from libSystemConfiguration if exported; else copy the ~30 LOC helper directly (APSL 2.0). Avoids a buggy clean-room sanitizer. |
SCPreferences ComputerName read |
_SCPreferencesCopyComputerName() in SCDHostName.c |
Reuse via existing libSystemConfiguration. iter 2 calls it. Sees through preferences.plist. |
SCPreferences ComputerName write (System Preferences-side) |
SCPreferencesSetComputerName() in SCDHostName.c |
Reuse. iter 4 (Bonjour conflict feedback) needs the write side. |
PreferencesMonitor plugin — bridges preferences.plist → SCDynamicStore Setup: keys |
Plugins/PreferencesMonitor/ |
Skip. Apple's PreferencesMonitor is a configd plugin we'd need a plugin loader for. iter 1's hostnamed writes the Setup: keys directly, bypassing this layer — one daemon does the synthesis + publish + sethostname. |
| Synthesize default name from hardware identity (model + serial / MAC) | Setup Assistant — closed source. | Clean-room. ~150 LOC C against kenv(2) + getifaddrs(3) + sysctl kern.hostuuid. This is the "Setup Assistant equivalent" piece. Detailed in §5 below. |
Monorepo. New directory src/hostnamed/ alongside the other Mach-track daemons:
freebsd-launchd-mach/
├── src/
│ ├── launchd/
│ ├── configd/
│ ├── hwregd/
│ ├── IPConfiguration/
│ ├── DiskArbitration/
│ ├── mDNSResponder/
│ ├── hostnamed/ <-- this plan
│ │ ├── hostnamed.c ~400 LOC, the whole daemon
│ │ ├── Makefile BSD bsd.prog.mk
│ │ └── hostnametest.c smoke-test client (read SCDynamicStore keys)
│ ├── libSystemConfiguration/ already shipped (SCDynamicStore client API)
│ └── ...
├── overlays/
│ └── System/Library/LaunchDaemons/
│ └── com.apple.hostnamed.plist <-- new
└── overlays/usr/tests/freebsd-launchd-mach/
└── run.sh <-- new HOSTNAMED-OK marker
No new pkg deps. Builds against base FreeBSD libc + the in-tree libSystemConfiguration + libCoreFoundation.
macOS tracks three related but distinct names, all owned by configd. We copy this shape verbatim because mDNSResponder is already coded to it:
| Name | Purpose | Where it lands on macOS | Where it lands on us |
|---|---|---|---|
ComputerName |
Human-readable; shown in About This Mac, Sharing prefs, Bonjour service names. Allows spaces, mixed case, apostrophes (John's MacBook Pro). |
preferences.plist → System → System → ComputerName; SCDynamicStore Setup:/System mirror. |
iter 1: synthesized only, published to SCDynamicStore Setup:/System/ComputerName. iter 2: also read from SCPreferences if present. |
LocalHostName |
The Bonjour .local name. RFC 1035 LDH-safe (letters / digits / hyphens; no leading or trailing hyphen; ≤63 chars). Derived from ComputerName by sanitization. |
SCDynamicStore State:/Network/HostNames → LocalHostName. |
Same key, written by hostnamed via SCDynamicStoreSetValue. |
HostName |
BSD-style FQDN; often unset on consumer Macs. Becomes kern.hostname — what hostname(1) prints, what gethostname(3) returns. |
sysctlbyname("kern.hostname", …); SCDynamicStore State:/Network/HostNames → HostName. |
Same: sysctlbyname + SCDynamicStore write. |
For a synthesized name like ThinkPad-T420-8AB123 all three end up identical — the name is already DNS-safe so the LDH sanitization is a no-op. For a user-set name like John's MacBook Pro (spaces and apostrophe), ComputerName keeps them and the other two become Johns-MacBook-Pro.
iter 1 is a one-shot because synthesis inputs are persistent (factory hardware identity), so re-running synthesis on every boot produces the same name. There's nothing to watch until iter 2 / 3 / 4 add SCPreferences, DHCP-option-12, and Bonjour-conflict event sources. iter 3 promotes hostnamed to KeepAlive=true when the vendored set-hostname.c decision engine arrives — it needs to subscribe to SCDynamicStore notifications for primary-service changes, DHCP-lease changes, and reverse-DNS query callbacks.
Two Apple-canonical syscalls on the publish path:
sethostname(2) — what Apple's set-hostname.c calls. On FreeBSD, sethostname(2) internally writes kern.hostname, so the visible effect is identical. We pick the syscall Apple uses for fidelity.notify_post("com.apple.system.hostname") — the documented hostname-change broadcast. Goes through our notifyd (already shipped, Phase J2). Apple-canonical subscribers (mDNSResponder, anyone else) listen via notify_register_* for this key and re-fetch the new value.Three precedence tiers, highest wins. Stops at the first tier that yields a non-empty name.
kenv hostname.overrideIf kenv(2) returns a non-empty value for the key hostname.override, use it verbatim. This is the "Setup Assistant pre-populated the name" equivalent — set in /boot/loader.conf or /boot/loader.conf.d/local.conf at install time:
hostname.override="my-t420"
Bypasses synthesis entirely; LDH sanitization still applied for LocalHostName / HostName. Whitelist for characters allowed in the override: [A-Za-z0-9 _.\-]{1,253} (UTF-8 letters allowed in ComputerName only; sanitized out of the LDH names).
ComputerName iter 2+Not in iter 1. Once iter 2 lands, hostnamed opens preferences.plist via SCPreferences, checks System → System → ComputerName. If present, use it verbatim (sanitize for the other two names) and skip Tier 3. Matches Apple's user-set name beats synthesis behavior.
slug-suffixDefault path. Compose ${slug}-${suffix} from hardware identity:
slug — the model partFirst non-empty of:
kenv smbios.system.version — this is the human-readable model on most laptops. ThinkPad T420 shows "ThinkPad T420" here. Apple shows "MacBookPro16,1"; Dell shows "Latitude E7470"; ASUS shows "ROG STRIX G15 G513QY_G513QY".kenv smbios.system.product — the SKU / MTM string. Lenovo's MTM is "4236AB1", Apple's is "Mac-mini". Used as fallback when .version is empty / placeholder."freebsd" — final fallback (no SMBIOS data at all; VMs or weird embedded boards).Sanitization: trim whitespace, replace runs of whitespace with single -, strip anything not in [A-Za-z0-9-], collapse double-hyphens, trim leading / trailing -, truncate to 40 chars. The result: "ThinkPad T420" → "ThinkPad-T420".
suffix — the per-machine saltFirst non-empty of:
kenv smbios.system.serial. Skip if value matches any of the well-known empty-firmware placeholders: "", "None", "To be filled by O.E.M.", "Default string", "0", "0123456789", "System Serial Number". Lenovo serials always pass (factory-set, 7-char alphanumerics like R8AB123; suffix = last 6 = 8AB123).getifaddrs(AF_LINK) for the first non-loopback Ethernet (IFT_ETHER, MAC != 00:00:00:00:00:00). em0 / re0 / bge0 / etc.sysctl kern.hostuuid. Always exists (the kernel synthesizes a v4 UUID on first boot if absent and persists it). Final fallback.Concatenate as "${slug}-${suffix}"; truncate composite to 63 chars (the RFC 1035 label cap that LocalHostName needs anyway). Empty slug + non-empty suffix → "freebsd-${suffix}". Both empty (impossible in practice) → literal "freebsd".
| Machine | smbios.system.version | smbios.system.serial | Synthesized name |
|---|---|---|---|
| ThinkPad T420 #1 | ThinkPad T420 | R8AB123 | ThinkPad-T420-8AB123 |
| ThinkPad T420 #2 | ThinkPad T420 | R7CD456 | ThinkPad-T420-7CD456 |
| Mac mini 2018 | Macmini8,1 | C07XXXX1234 | Macmini81-XXX1234 → Macmini81-X1234 (last 6) |
| Dell Latitude E7470 | Latitude E7470 | CXYZ123 | Latitude-E7470-YZ123 (last 6 of CXYZ123 = XYZ123... wait, 6 chars = XYZ123; Latitude-E7470-XYZ123) |
| QEMU/SLIRP (CI) | Standard PC (Q35 + ICH9, 2009) | Not Specified (skipped) | Standard-PC-Q35--ICH9-2009-345678 (NIC suffix from 52:54:00:12:34:56) → truncated to 63 |
| Bare board w/ no NIC, no SMBIOS | — | — | freebsd-${hostuuid_first6} |
Three answers in order of effort — the user shouldn't need to plug in a monitor to find their machine on the LAN.
S/N: R8AB123, the name is ThinkPad-T420-8AB123. Predictable standing in the room with the laptop.hostnamed 2026-MM-DDTHH:MM:SSZ ComputerName: ThinkPad-T420-8AB123
hostnamed 2026-MM-DDTHH:MM:SSZ source: smbios.system.version="ThinkPad T420" smbios.system.serial="R8AB123"
hostnamed 2026-MM-DDTHH:MM:SSZ override via: kenv hostname.override="<name>" in /boot/loader.conf
and the getty login banner becomes FreeBSD/amd64 (ThinkPad-T420-8AB123). The synthesis is deterministic so the name is the same on subsequent boots./boot/loader.conf.d/local.conf:
hostname.override="my-t420"
Tier 1 wins, synthesis doesn't run. Same effect as Apple's Setup-Assistant pre-fill.iter 1 writes exactly two SCDynamicStore keys via SCDynamicStoreSetValue, plus the sethostname(2) + notify_post() pair. The key paths are constructed using Apple's own helpers from libSystemConfiguration (SCDynamicStoreKeyCreateComputerName, SCDynamicStoreKeyCreateHostNames), so they're guaranteed to match what mDNSResponder's subscriber code expects without us hard-coding paths.
| Key (path resolves to) | Type | Value |
|---|---|---|
Setup:/System — built by SCDynamicStoreKeyCreateComputerName() as "/${kSCDynamicStoreDomainSetup}/${kSCCompSystem}" |
CFDictionary | |
Setup:/Network/HostNames — built by SCDynamicStoreKeyCreateHostNames() as "/${kSCDynamicStoreDomainSetup}/${kSCCompNetwork}/${kSCCompHostNames}" |
CFDictionary | |
(kern.hostname — not SCDynamicStore, set via libc) |
kernel string | sethostname("ThinkPad-T420-8AB123", 20). Apple's set-hostname.c uses this syscall; it ends up at kern.hostname internally. We match Apple's choice of sethostname(2) over sysctlbyname("kern.hostname", …) for fidelity — visible effect is identical. |
(com.apple.system.hostname — not SCDynamicStore, notifyd broadcast) |
notify token | notify_post("com.apple.system.hostname"). This is the documented Apple-canonical hostname-change broadcast; mDNSResponder + future SCDHostName-aware consumers re-fetch when they see this token fire. |
Note: Apple uses the Setup: domain (not State:) for both these keys — double-checked against SCDHostName.c source. Setup: is the "intended / persistent configuration" domain that PreferencesMonitor normally writes after reading preferences.plist; we bypass PreferencesMonitor and write directly because the read-from-prefs path is iter 2, and iter 1 is solely the synthesis-fallback role.
Notification semantics: SCDynamicStoreSetValue on the existing configd session sends the standard SCDynamicStore notification on any key that's subscribed via SCDynamicStoreSetNotificationKeys. The notify_post is the Apple-canonical higher-level signal for the same event — both fire from iter 1 because Apple's set-hostname.c does both, and we want subscribers to be able to use whichever notification surface fits them.
The visible-on-the-console "I am <name>" banner has three layers:
/var/log/hostnamed.stderr; dumped at the end of CI's run.sh like every other daemon's log.FreeBSD/amd64 (Amnesiac) (console). After iter 1: getty reads gethostname(3) which goes through kern.hostname — so as long as hostnamed runs before getty's plist is dispatched, the banner reads the new name automatically. launchd has no ordering guarantee (everything RunAtLoad=true starts in parallel), so the first banner print may race the kern.hostname write. Mitigation: hostnamed is a one-shot that exits quickly (<50ms typical), and getty respawns its prompt periodically; even if the very first prompt shows Amnesiac, the next one (and every subsequent connection) shows the right name.hostnamed write a one-liner directly to /dev/console after setting kern.hostname, so the boot transcript shows the synthesized name regardless of getty's timing. Defer this to iter 1 if it ends up needed; the stderr log is the primary signal.Deferring the "getty re-renders on hostname change" wiring is fine because synthesis is deterministic across reboots — the user sees the right name on the second boot regardless of any first-boot getty race.
src/hostnamed/ with the daemon, a Makefile, and a smoke-test client.overlays/System/Library/LaunchDaemons/com.apple.hostnamed.plist: RunAtLoad=true, KeepAlive=false (one-shot), no MachServices (iter 1 daemon has no RPC surface of its own — it's purely a producer of SCDynamicStore values).kenv hostname.override > synthesized slug+suffix. (SCPrefs check is deferred to iter 2.)SCDynamicStoreSetValue for Setup:/System + Setup:/Network/HostNames via existing libSystemConfiguration helpers.sethostname(2) — the libc syscall Apple's set-hostname.c uses.notify_post("com.apple.system.hostname") — Apple-canonical hostname-change broadcast. Uses our existing libnotify (Phase J2 iter 1).HOSTNAMED-OK emitted after all four operations succeed; HOSTNAMED-FAIL: <why> otherwise.tests/boot-test.sh; verifier in run.sh.set-hostname.c's decision-engine code (that's iter 3). The LDH-sanitization helper (_SC_CFStringIsValidDNSName equivalent) is reused if exported by libSystemConfiguration; if not, copy the ~30 LOC from SCDHostName.c (APSL 2.0, preserve header).preferences.plist via the existing SCPreferences API (already in our libSystemConfiguration); call _SCPreferencesCopyComputerName().ComputerName and the sanitized form as LocalHostName / HostName.SCPreferencesSetComputerName) is iter 4's dependency.set-hostname.c as the decision engine vendor + freebsd-shim layerPlugins/IPMonitor/set-hostname.c into src/hostnamed/vendored/ with its APSL 2.0 header preserved — same per-file licensing pattern we use for configd / IPConfiguration vendored files.freebsd-shim.{c,h} that maps the file's IPMonitor-internal dependencies to standalone equivalents:
copy_dhcp_hostname() — in IPMonitor this reads internal lease state; for us it queries ipconfigd over SCDynamicStore (ipconfigd needs to publish DHCP option 12 in State:/Network/Service/<UUID>/DHCP/Option_12 first — a small additive iter 10 on the IPConfiguration plan).check_if_service_expensive() — on iOS this gates PTR queries to avoid metered-network charges. For us: always return false (no metered concept on FreeBSD).load_hostname() entry point gets called from hostnamed's main instead of from a plugin-load hook.kenv.override > SCPrefs.ComputerName > DHCP option 12 > reverse-DNS PTR > mDNS local name > synthesized slug+suffix (our fallback below Apple's "localhost").hostnamed from one-shot to persistent (KeepAlive=true, libdispatch event loop subscribing to SCDynamicStore notify keys + DNS resolver callbacks)..local conflict on the LAN and renames itself foo-2.local per RFC 6762 §9.SCPreferencesSetLocalHostName() (the vendored Apple surface from SCDHostName.c) so the rename is persisted.QEMU/SLIRP has no SMBIOS-meaningful data — smbios.system.version = "Standard PC (Q35 + ICH9, 2009)", smbios.system.serial = "Not Specified" (skipped per the placeholder filter). So in CI the synthesis falls through to the MAC-suffix tier. The em0 MAC under QEMU SLIRP is the deterministic 52:54:00:12:34:56, so the expected CI name suffix is 123456.
Expected CI marker:
hostnamed 2026-MM-DDTHH:MM:SSZ ComputerName: Standard-PC-Q35--ICH9-2009-123456
hostnamed 2026-MM-DDTHH:MM:SSZ HOSTNAMED-OK
The CI test verifies the name matches the regex [A-Za-z0-9-]+-[0-9a-f]{6} rather than pinning the exact string — the slug derivation may evolve and we don't want CI to fight that. The regex enforces the load-bearing property: a non-empty slug, a hyphen separator, a 6-hex per-machine suffix.
Verifier checks in run.sh:
$(hostname) equals $(grep '^hostnamed.*ComputerName:' /var/log/hostnamed.stderr | sed 's/.*ComputerName: //')hostnametest, reuses configd's MIG client patterns) reads State:/Network/HostNames and confirms LocalHostName + HostName are populated and DNS-safe.FreeBSD/amd64 (<synthesized>) not (Amnesiac) on the second-and-later getty prompt.KeepAlive=true that sits forever. One-shot is cleaner now; KeepAlive is what iter 3 (vendored decision engine) needs anyway. Going one-shot first means a small refactor at iter 3 (mostly just adding the dispatch event loop around the existing call). OK with the small refactor, or skip straight to KeepAlive at iter 1?
smbios.system.version wins by default. For Apple hardware where .version shows "Macmini8,1" and .product shows "Mac-mini", the user-friendly name is .product. Worth swapping the precedence, or carrying a small per-vendor table (vendor=="Apple Inc." ? .product : .version)?
smbios.system.serial wins over MAC. Serial is more stable (NIC swap doesn't change it) but some firmwares ship garbage / blank serials. The placeholder filter handles the well-known offenders, but there's a long tail. Are we OK with the fallback chain (serial → MAC → kern.hostuuid), or should we use the MAC unconditionally for predictability ("the sticker has the MAC")?
/dev/console directly so the synthesized name appears in the boot transcript even if getty's first prompt races, or is the stderr log + getty's eventual respawn enough?
freebsd-shim redirections at the call boundary. Confirm this is the right call vs. a forked-and-rewritten approach.
set-hostname.c falls back to "localhost" if no prefs / DHCP / PTR / mDNS name is available. Our iter 3 inserts synthesized slug+suffix between mDNS and localhost — so the synthesized name only fires if every Apple-canonical source came up empty. Confirm this is the right insertion point (vs. "synthesized always wins as the last guaranteed-non-localhost source").
_SC_CFStringIsValidDNSName: reuse from libSystemConfiguration if it exports the symbol (it's a SPI; check with nm), else lift the ~30 LOC. The lift is trivial; the reuse means we don't drift from Apple's validation rules as they update. Default to "reuse if available, lift if not."