mDNS Network Browse over Mach Plan ready

Why the Workspace File Viewer's Network list is empty on NextBSD, and the Mach-IPC fix that lights it up. Drop the AF_UNIX socket transport; finish the dns_sd client protocol over the com.apple.mDNSResponder Mach service — the Apple way.

TL;DR

The File Viewer's Network node and the standalone NetworkBrowser app both browse mDNS via GNUstep NSNetServiceBrowser, which calls libdns_sd underneath. On NextBSD that browse silently returns nothing — the Network list is empty — while a Debian box on the same LAN lists ThinkPad-T460s, Joseph's Mac mini, and VirtualBox-ed65a877.

Root cause: the mDNSResponder daemon advertises its own .local name over multicast (so peers see it), but there is no working client request/response transport. The installed libdns_sd is the pure AF_UNIX client stub that connects to /var/run/mDNSResponder — a socket that is never created (the daemon drops to nobody against root-owned /var/run). The intended Mach transport (mach_bridge.c + a dns_sd.defs MIG IDL) was scoped but never implemented: the daemon claims the com.apple.mDNSResponder Mach service and never reads a message from it.

Fix, per the “Mach not sockets” doctrine: implement the dns_sd wire protocol over Mach on both ends — a MIG-served Mach receive loop in the daemon (wired through EVFILT_MACHPORT, per #168) and a Mach transport in libdns_sd's client stub. The UI needs zero changes. Almost all the work is one repo: nextbsd-redux/nextbsd.

1. Symptom

On the live NextBSD machine (root@thinkpad-t460s, display :0, user admin), GWorkspace's File Viewer shows the sidebar tree Local User / Domains / Volumes / Network. The Network section is expanded but empty. The Sharing preference pane lists every service (SSH, SFTP, AFP, SMB, VNC) as Off.

For comparison, admin@debian on the same LAN shows a populated Network section. Same File Viewer, same sidebar tree (Local User / Domains / Volumes / Network) — only the Network node differs:

NextBSD File Viewer sidebar with an empty Network section
NextBSD (ThinkPad-T460s) — the Network node is expanded but empty.
Debian File Viewer sidebar listing three discovered network hosts
Debian (reference)Network lists Joseph’s Mac mini, VirtualBox-ed65a877, and ThinkPad-T460s.
HostNextBSD (ThinkPad-T460s)Debian (reference)
Network sidebaremptyJoseph's Mac mini, VirtualBox-ed65a877, ThinkPad-T460s
Advertises self via mDNSyes — peers see ThinkPad-T460syes
Can browse / discover peersnoyes

The asymmetry is the key clue: NextBSD advertises (Debian sees it) but cannot browse. Advertisement is the multicast engine talking to the wire; browse requires a client API call into the daemon — and that path is broken.

2. How discovery is supposed to flow

Both UI consumers are platform-agnostic and converge on a single transport that lives below Gershwin, inside GNUstep-base's NSNetServicelibdns_sd linkage:

File Viewer "Network" node NetworkBrowser.app (gershwin-workspace) (gershwin-components) NetworkServiceManager NetworkBrowser browses _sftp-ssh/_afpovertcp/ browses _services._dns-sd._udp _webdav/_webdavs ._tcp then <type>._tcp \ / \ / GNUstep NSNetServiceBrowser / NSNetService | libdns_sd.so (DNSServiceBrowse / Resolve) | == IPC transport == <-- BROKEN on NextBSD | mDNSResponder daemon (mDNSCore + mDNSPosix) | multicast on the LAN (advertise works)

Confirmed in source: the File Viewer's NetworkServiceManager is declared <NSNetServiceBrowserDelegate, NSNetServiceDelegate> and gates on NSClassFromString(@"NSNetServiceBrowser"); its /Network node is a synthetic NetworkFSNode whose children come straight from [[NetworkServiceManager sharedManager] allServices] — no SMB browse, no autofs, no pre-mounted filesystem. The standalone NetworkBrowser.app uses the same NSNetServiceBrowser API independently. Same backend, same transport. Fix the transport once and both light up.

3. Root cause

Two compounding defects, both verified by reading the port at nextbsd-redux/nextbsd src/mDNSResponder/ and probing the running daemon.

3a. No Mach dns_sd transport exists on either side

3b. The one transport that does exist (AF_UNIX) is dead at runtime

Decision (confirmed): do not repair the AF_UNIX socket. Apple's stack uses Mach for the dns_sd client protocol, and NextBSD is going Mach-native. Finish the Mach transport and retire (or demote to a disabled fallback) the AF_UNIX path. This aligns with the existing EVFILT_MACHPORT native migration (#168) doctrine.

4. Which repos to touch

The fix is overwhelmingly concentrated in one repo. Mapping each source tree to its GitHub home:

RepoOrgWhat changesScope
nextbsd
primary
nextbsd-redux The whole transport fix: new dns_sd.defs MIG IDL; real Mach server in mach_bridge.c; wire the Mach port into the event loop via EVFILT_MACHPORT; add a Mach transport to libdns_sd's client stub; MIG codegen + link flags in both Makefiles; update com.apple.mDNSResponder.plist. Lives under src/mDNSResponder/ and overlays/System/Library/LaunchDaemons/. (Active branch today: mdns-device-info.) Heavy
gershwin-components
follow-on
gershwin-desktop Nothing for the Network list. Separate follow-on only: the Sharing pane's sharing-helper.c uses FreeBSD sysrc/service/rc.conf for hostname-set and service status/start-stop — needs a launchd-native shim. (Branch: feat/nextbsd.) See §8. Medium (independent)
gershwin-workspace
no change
gershwin-desktop None. The File Viewer's /Network node (NetworkServiceManager / NetworkFSNode) is correct as written — it just needs the transport beneath it to work. (Source ships in developer-display-server.tar.gz.) None
GNUstep base
rebuild/relink
gnustep Likely no source changeNSNetService links libdns_sd at the .so level. If the new Mach client stub keeps the libdns_sd ABI, a rebuilt libdns_sd.so drops in. Verify the NSNetService run-loop integration still works once DNSServiceRefSockFD is replaced by a Mach reply port (see §6). Verify only
nextbsd-freebsd-compat
n/a
nextbsd-redux None expected. srclist only references mDNSResponder in a comment; packaging of the daemon/lib is driven from the nextbsd build, not the compat srclist. None
Issue tracking. Per project doctrine, file the tracking issue in nextbsd-redux/nextbsd even though a follow-on touches gershwin-components. Cross-repo PRs won't auto-close it; close manually.

5. Daemon-side fix (nextbsd-redux/nextbsd)

  1. Add a MIG IDL — a new dns_sd.defs (or revive Apple's DNSServiceDiscovery.defs) defining the request routines (browse / register / resolve / queryrecord / enumerate) plus the DNSServiceDiscoveryReply callback subsystem. Generate server (*Server.c) and user (*User.c) stubs with mig.
  2. Implement a real Mach server in mach_bridge.c — after bootstrap_check_in, keep the receive right and dispatch incoming messages through the generated MIG demux, translating each routine into the existing uds_daemon.c request handlers (handle_browse_request, the request_state machinery) so mDNSCore is driven exactly as the socket path drove it. Replies go back to the client's reply port via the generated DNSServiceDiscoveryReply user stub.
  3. Wire the Mach port into the event loop — the central missing piece. Bridge the receive right to a kqueue EVFILT_MACHPORT source (consistent with #168), rather than a separate mach_msg server thread, so it folds into mDNSPosixRunEventLoopOnce.
  4. Retire / demote AF_UNIX — drop udsserver_init (or keep behind a build flag, disabled by default). If kept, it must be created by launchd with correct ownership, not bound by the post-drop nobody daemon.

6. Client-side fix (libdns_sd, same repo)

  1. Add a Mach transport to dnssd_clientstub.c (or a Mach-backed alternate stub). ConnectToServer must bootstrap_look_up("com.apple.mDNSResponder") for the send right instead of socket()/connect(); deliver_request must mach_msg(MACH_SEND) via the generated MIG user stub instead of sendmsg.
  2. Reply delivery — allocate a per-DNSServiceRef reply receive port. DNSServiceRefSockFD / DNSServiceProcessResult assume a socket FD today; they need a Mach analog (a port that integrates with client run loops, as Apple's classic lib did). This is the piece GNUstep's NSNetService run-loop integration depends on — verify it there.
  3. Guard the transport with a build define (analogous to the existing USE_TCP_LOOPBACK switch), e.g. USE_MACH_DNSSD, selecting Mach vs AF_LOCAL at compile time.

7. Build & packaging (same repo)

8. Follow-on: Sharing pane (gershwin-desktop/gershwin-components)

Independent of the browse fix, but part of “hostname, enabled services, etc.” The Sharing pane's mDNS announcement rides the same NSNetServicelibdns_sd path, so it starts working the moment the transport is fixed — no Sharing code change for announce. The non-mDNS pieces (hostname + service control) need launchd-native rework.

8a. The hostname symptom

NextBSD Sharing pane with an empty Name field and all services Off
NextBSD Sharing pane — the Name (hostname) field is blank and every service reads Off, even though the host's name is ThinkPad-T460s.

The blank field is not a helper bug: run as the same admin user that owns the pane, sharing-helper get-hostname returns ThinkPad-T460s with exit code 0. So the value is available but never reaches the text field — a UI-layer defect in SharingController (the NSTask output capture / field assignment around the get-hostname call), independent of the platform rework below. The “Service Discovery: Available (NSNetService)” line is also misleading: the class resolves, but the underlying transport is the broken one from §3, so nothing is actually announced or browsed.

8b. Hostname + service control: what needs launchd-native rework

FunctionToday (FreeBSD branch)NextBSD fix
Hostname displaygethostname() via helper — helper worksUI bug field stays blank; fix SharingController output capture (8a)
Hostname setsysrc hostname= / edits /etc/rc.conflaunchd-native persistence (hostnamed / a scutil-style setter — scutil is absent today), not rc.conf
Service statusservice <name> statuslaunchctl query shim
Service start/stopsysrc <name>_enable=YES + service start/stoplaunchctl load/enable shim
Boot re-announcesystemd unit gershwin-sharing-mdns.service (Linux-only)LaunchDaemon equivalent, or rely on Sharing UI re-announce on login
mDNS announceNSNetService -publishfixed by the transport work

8c. Conditional on NextBSD — do not break FreeBSD / Linux / OpenBSD / NetBSD

The platform split in sharing-helper.c is the existing PLATFORM_* ladder. The trap: NextBSD compiles as __FreeBSD__, so it silently falls into PLATFORM_FREEBSD — the sysrc/service/rc.conf branch. Two rules follow:

/* checked BEFORE __FreeBSD__ — NextBSD also defines __FreeBSD__ */
#if defined(PLATFORM_NEXTBSD) || defined(NEXTBSD)   /* -DNEXTBSD from the NextBSD build only */
#  define PLATFORM_NEXTBSD 1
#elif defined(__linux__)
#  define PLATFORM_LINUX 1
#elif defined(__FreeBSD__)
#  define PLATFORM_FREEBSD 1      /* unchanged — real FreeBSD lands here */
#elif defined(__OpenBSD__)
#  define PLATFORM_OPENBSD 1
#elif defined(__NetBSD__)
#  define PLATFORM_NETBSD 1
#endif

Belt-and-suspenders (recommended): also pick the service-control backend at runtime — prefer launchctl when it is present, else fall back to service/sysrc (FreeBSD/NetBSD) or systemd (Linux). That way even a mis-built binary degrades gracefully and regular OSes are never affected regardless of defines. (A __has_include(<servers/bootstrap.h>) Mach-header probe is an alternative compile-time auto-detect, but the build define is more explicit.)

Scope note: this conditionalization touches only sharing-helper.c (service + hostname control). The discovery / Network-list fix needs no conditionals in Gershwin at all — it lives entirely in libdns_sd (the nextbsd repo). GNUstep's NSNetService links whatever libdns_sd the platform ships (avahi-compat on Linux, the Mach-backed one on NextBSD), so the Gershwin discovery code stays 100% platform-agnostic.

9. Phased rollout (PR-gated)

Phase 1 — MIG IDL + daemon Mach server

Add dns_sd.defs, codegen, real receive loop in mach_bridge.c dispatching into uds_daemon handlers. Wire via EVFILT_MACHPORT. Gate: a daemon-side smoke test proves a mach_msg round-trip into mDNSCore and back.

Phase 2 — libdns_sd Mach client transport

USE_MACH_DNSSD stub: bootstrap_look_up + mach_msg send, reply-port model replacing DNSServiceRefSockFD. Build/link wiring. Gate: in-tree dnssdtest/mdnstest adapted to the Mach model passes DNSServiceBrowse end-to-end.

Phase 3 — GNUstep + UI validation

Rebuild/relink GNUstep libdns_sd; verify NSNetService run-loop integration. Gate: File Viewer Network node and NetworkBrowser.app both populate on real hardware (the thinkpad-t460s box), matching the Debian reference.

Phase 4 — Sharing pane launchd-native helper

(gershwin-components.) Replace sysrc/service calls with launchctl shims; NextBSD hostname-set path; LaunchDaemon re-announce. Gate: Sharing pane toggles a service, status reflects it, and the service appears in another host's Network list.

10. Verification

# On the NextBSD box, after the fix:
# 1. Mach service is claimed AND served (not just claimed)
launchctl print system/com.apple.mDNSResponder | grep -i mach

# 2. A browse from libdns_sd returns results (CLI, if dns-sd is built)
dns-sd -B _services._dns-sd._udp local.

# 3. The File Viewer Network node populates (screenshot display :0 as admin)
DISPLAY=:0 import -window root /tmp/after.png    # compare to the empty "before"

# 4. Cross-check from a peer: NextBSD's own advertised services now resolve
avahi-browse -art   # (from the Debian reference box)

Success = the File Viewer Network list on NextBSD shows the same peers the Debian box sees, and toggling a service in the Sharing pane makes it appear on other hosts.

11. References


Filed 2026-06-21. Root-caused from a 3-agent investigation (daemon/lib Mach gap, Gershwin UI consumer map, existing-plan review) plus live probing of root@thinkpad-t460s. Primary work: nextbsd-redux/nextbsd (src/mDNSResponder/). UI: no change. Follow-on: gershwin-desktop/gershwin-components Sharing pane.