FreeBSD configd — porting plan

A two-layer network and system-configuration stack for FreeBSD. The lower layer (Phase 1, shipped) is dhcpcd in master mode + wpa_supplicant — both run directly by launchd, handling DHCPv4/v6/RA/SLAAC, link-state, hot-plug, and WiFi association. The upper layer (Phase 2+, planned) is a port of Apple's configd as netconfigd, providing a SystemConfiguration-like dynamic store, Distributed Objects IPC for scutil and GUI tooling, and a declarative Network.plist watcher that translates user intent into dhcpcd.conf overrides. Companion to the freebsd-launchd port.

Status: Phase 1 shipped (autoconfig only) v1

1. Goal & non-goals

1.1 Goal

A FreeBSD live ISO that boots with launchd as PID 1, gets a working network in seconds without user intervention (Phase 1 shipped), and grows over Phase 2+ a SystemConfiguration-equivalent layer on top — declarative config via Network.plist, a clean ObjC API for GUI tooling, and a scutil-equivalent CLI — while delegating the protocol-level work (DHCP, RA, WiFi association) to the well-maintained ports daemons that already handle it.

1.2 Non-goals (this iteration)

2. Architecture: layered model

Two layers, separable in time and ownership:

+-----------------------------------+ | GUI tooling, scutil, app code | +-----------------+-----------------+ | DO calls (Phase 2+) v +----------------------+----------------------+ | netconfigd (Phase 2+, planned) | | -------------------------------- | | - Reads /Local/Library/Preferences/ | | Network.plist (vnode source) | | - Maintains dynamic store | | - Exposes DO IPC: SCDynamicStore, | | SCNetworkReachability, scutil queries | | - Translates declarative intent into | | dhcpcd.conf overrides | | - Writes /etc/resolv.conf from merged | | DHCP+plist DNS state | | - Watches PF_ROUTE for OBSERVATION | | (action stays with dhcpcd) | +----------------------+----------------------+ | | (writes config files, | signals SIGHUP) v +----------------------+----------------------+ | dhcpcd (Phase 1, shipped) | | -------------------------------- | | - Master mode: discovers all ifaces, | | watches RTM_NEWLINK for hot-plug | | - DHCPv4 + DHCPv6 + RA/SLAAC + IPv4LL | | - Recognizes bridge/lagg/vlan layering | | - Link-state watcher: re-acquires lease | | on flap | | - Coordinates with wpa_supplicant for | | wifi-iface DHCP after association | | | | wpa_supplicant (Phase 1, shipped) | | - WiFi association per wlan iface | | - Exposes control socket dhcpcd reads | +----------------------+----------------------+ | v kernel network stack

2.1 Why two layers instead of one

Apple's configd has both layers fused — IPConfiguration agent (DHCP/RA) lives inside the configd process. That's twenty years of Apple-internal work. FreeBSD's stack has a different shape: dhcpcd already does what IPConfiguration does, including features (DHCPv6, IPv4LL, RDNSS in userspace, master-mode hot-plug) that took Apple a long time to add. We get those for free by running dhcpcd directly, and free our netconfigd work for what's actually missing on FreeBSD: the SystemConfiguration layer (dynamic store + DO IPC + declarative config) on top.

The boundary between layers is clean: dhcpcd owns actions (acquire lease, configure address, follow link state); netconfigd owns policy (read Network.plist, decide what to ask dhcpcd to do, expose state to clients). They communicate via dhcpcd's existing config files + signals + lease-state files; netconfigd doesn't need to talk a custom protocol to dhcpcd.

3. Layer 1 (Phase 1, shipped): dhcpcd + wpa_supplicant

3.1 dhcpcd master mode

Run directly by launchd via /System/Library/LaunchDaemons/org.freebsd.dhcpcd.plist. ProgramArguments=["/usr/local/sbin/dhcpcd", "-B"] — the -B flag keeps dhcpcd in foreground so launchd's KeepAlive=true can supervise it. Master mode (no iface argument) means dhcpcd:

3.2 wpa_supplicant from ports

Listed in pkglist.txt. Installs to /usr/local/sbin/wpa_supplicant, supersedes the base version. Reasons: faster WPA3-SAE / OWE feature uptake, current EAP method support, more aggressive CVE backports. dhcpcd has a built-in wpa_supplicant hook that detects the control socket and gates DHCP behind ASSOCIATED state — out-of-the-box pairing.

Phase 1 doesn't ship a launchd plist for wpa_supplicant. SSID configuration is still user-driven via /etc/wpa_supplicant.conf or wpa_cli. Phase 2+ will introduce a netconfigd-managed flow where Network.plist declares wpa_supplicant config that netconfigd materializes and starts wpa_supplicant per wifi iface.

3.3 What netconfigd is NOT doing in Phase 1

The Phase 1 NCDaemon is a stub:

- (void)start {
    NSLog(@"netconfigd: Phase 1 placeholder (pid=%d)", getpid());
    NSLog(@"netconfigd: network auto-config is dhcpcd's job; "
          @"this stub exits cleanly until Phase 2");
}

It doesn't get launched (no plist references it). It builds and installs to /usr/libexec/netconfigd just to keep the configd build pipeline live; the binary sits unused until Phase 2 swaps it for the real implementation. Apple's imported source under configd/src/ is intact and ready for Phase 2 work to begin against.

4. Layer 2 (Phase 2+, planned): netconfigd

The Apple configd port resumes in Phase 2. End state: a launchd-supervised long-running daemon that reads /Local/Library/Preferences/Network.plist, exposes DO IPC, and translates declarative config into dhcpcd / wpa_supplicant / kernel state. Distinct from the original plan in that it does not spawn DHCP/RA/WiFi children itself — it shapes what the lower layer does via files and signals.

4.1 Dynamic store, GNUstep-flavored

Apple's configd publishes state to the SystemConfiguration "dynamic store" — a key-value space (keys like State:/Network/Interface/en0/IPv4) that processes subscribe to. We keep the same key naming for compatibility-of-mental-model, back it with an NSMutableDictionary protected by a serial dispatch queue. Subscribers register interest via the DO connection; NSDistributedNotificationCenter-style fan-out notifies them when subscribed keys change.

Sources of state:

4.2 Translation: Network.plist → dhcpcd.conf

netconfigd's main outbound effect on the lower layer. When Network.plist changes:

  1. Read the plist via NSPropertyListSerialization.
  2. Compute desired dhcpcd config: per-iface static addresses (static ip_address=...), DNS overrides (static domain_name_servers=...), iface excludes (denyinterfaces ...), per-iface profiles.
  3. Write to /etc/dhcpcd.conf.d/netconfigd.conf (drop-in path dhcpcd reads).
  4. Send SIGHUP to dhcpcd's pid (in /var/run/dhcpcd.pid); dhcpcd re-reads config.

For DNS specifically: dhcpcd writes /etc/resolv.conf via resolvconf(8) by default. If Network.plist declares DNS.Servers, netconfigd merges those with DHCP-supplied servers and either uses dhcpcd's nooption domain_name_servers + static domain_name_servers=... shape, or takes resolv.conf ownership directly via nohook resolv.conf in dhcpcd.conf and writes resolv.conf itself.

4.3 Event sources

Source typeWatchesReaction
DISPATCH_SOURCE_TYPE_VNODE/Local/Library/Preferences/Network.plistreload via NSPropertyListSerialization; diff against last snapshot; regenerate /etc/dhcpcd.conf.d/netconfigd.conf; SIGHUP dhcpcd
DISPATCH_SOURCE_TYPE_READPF_ROUTE socketparse routing message; update dynamic store State:/Network/Interface/<iface>/...; fan out to DO subscribers
DISPATCH_SOURCE_TYPE_VNODEdhcpcd lease database directoryrefresh dynamic store DHCP-state keys when leases change
DISPATCH_SOURCE_TYPE_SIGNALSIGTERM / SIGHUPSIGTERM: clean shutdown. SIGHUP: force config reload from plist.

5. Repository

5.1 Layout under freebsd-launchd/configd/

freebsd-launchd/configd/
├── Makefile                       gmake build, installs to /usr/libexec/netconfigd
├── compat/                        FreeBSD shims for Apple-internal headers
├── scripts/import-source.sh       (Phase 0) clones apple-oss-distributions/configd
└── src/                           Apple configd-963.270.3 (post-amputation)
    ├── configd.tproj/
    │   ├── main.m                 (Phase 1) Foundation entry, calls NCDaemon
    │   ├── NCDaemon.{h,m}         (Phase 1) stub; Phase 2 grows it
    │   ├── _SC_dynamicStore_*.{c,h}  (Phase 2) ObjC rewrite
    │   └── ...                    Apple imports, Mach paths amputated
    ├── Plugins/
    │   ├── IPMonitor/             (Phase 2 port — repurposed for resolv.conf merge)
    │   ├── KernelEventMonitor/    (Phase 2 rewrite — PF_ROUTE source)
    │   ├── LinkConfiguration/     (Phase 2 port)
    │   ├── PreferencesMonitor/    (Phase 2 port — Network.plist watcher)
    │   └── SCNetworkReachability/ (Phase 2 port — DO API)
    ├── libSystemConfiguration/
    ├── nwi/
    ├── scutil.tproj/              (Phase 2 port — DO client)
    ├── SystemConfiguration.fproj/
    ├── common/
    └── logging/

Top-level (freebsd-launchd/) hosts:

5.2 How build.sh handles configd

Same chroot session that builds launchd + kmodloader. build.sh rsyncs configd/ into chroot:/tmp/configd/ and runs make-configd.sh against the just-installed Foundation/libdispatch. Installs to /usr/libexec/netconfigd.

6. Locked architectural decisions

DecisionChoice
Target kernelFreeBSD 14.x and 15.x. No Linux, no NetBSD.
DHCPv4 / DHCPv6 / RA / SLAAC / IPv4LLdhcpcd in master mode (Phase 1). NOT dhclient + rtsold + dhcp6c. NOT internal to netconfigd.
WiFi authenticationwpa_supplicant from ports. dhcpcd auto-coordinates via built-in wpa_supplicant hook.
Mach IPC / XPC / IOKit / AirPort / CoreWLANNone. All Mach paths in imported source amputated; ObjC NSConnection over AF_UNIX is the IPC for Phase 2+.
Service-to-service IPCGNUstep Distributed Objects (Phase 2+). No gdomap — direct NSSocketPort on a known path.
Event loop (Phase 2+)libdispatch dispatch sources only.
Plist parsingGNUstep NSPropertyListSerialization (XML + binary).
Source of truth for declarative config/Local/Library/Preferences/Network.plist (Phase 2+). No rc.conf parser. No manually-edited resolv.conf (netconfigd writes it).
Auto-config default (Phase 1)dhcpcd master mode covers everything — no Network.plist needed for working network. The plist's role begins in Phase 2.
License (top-level)BSD-2-Clause. Apple configd files retain Apache 2.0 per-file.
Build platformFreeBSD only, inside the same VM-action chroot the launchd build uses.

7. File-by-file plan (configd/src/)

Imported source: Apple configd-963.270.3. 289 source files; ~5.5 MB. Phase 0 amputation already done (drops the wholly-Mach files and explicit non-goals: AirPort, IOKit-based InterfaceNamer, SimulatorSupport, QoSMarking). What remains needs Phase 2 work.

7.1 Already deleted on import

7.2 Retained — Phase 2 fate

File / dirApple LOCActionTarget LOC
configd.tproj/configd.{c,h}~1.5kRewrite. dispatch_main loop; NSConnection-rooted DO server. Keep dynamic-store data shape.~600
configd.tproj/_SC_dynamicStore_*.{c,h}~3kRewrite as ObjC. NSMutableDictionary + serial dispatch queue. DO callback fan-out.~800
Plugins/IPMonitor/~8kRepurpose. Original Apple role: drive IPConfiguration agent. New role: read dhcpcd lease files, populate dynamic store DHCP keys, merge with Network.plist overrides, write resolv.conf. Drop AWD reporter.~2.5k
Plugins/KernelEventMonitor/~2kRewrite. Replace PF_SYSTEM / KEV_NETWORK_CLASS with PF_ROUTE socket parsing. Observation only — actions belong to dhcpcd.~600
Plugins/LinkConfiguration/~1kTrim heavily. dhcpcd handles link-state actions; this plugin's role shrinks to dynamic-store updates on link transitions.~300
Plugins/PreferencesMonitor/~2kPort. Watches Network.plist; diff against current; regenerate /etc/dhcpcd.conf.d/netconfigd.conf; SIGHUP dhcpcd. The new core.~700
Plugins/SCNetworkReachability/~3kPort. Provides reachability API to apps via DO. Uses PF_ROUTE for change events.~1.2k
libSystemConfiguration/~1kPrune Mach refs; back store-client API with DO calls.~600
nwi/~600Keep mostly as-is (pure data structures).~500
scutil.tproj/~5kSubstantial rewrite. DO client; shell-style command parsing (show, get, set, watch).~2k
SystemConfiguration.fproj/~10kTrim. Keep public framework API surface; drop Apple-specific bits.~3k
common/, logging/~1kKeep mostly as-is.~800

Total post-Phase-2: roughly 13-14k LOC vs Apple's ~37k pre-amputation. Slightly less than the original estimate because IPMonitor's role shrank with dhcpcd taking the IPConfiguration burden.

8. FreeBSD-only wins (vs Apple's Darwin-tied configd)

FeatureApple's configd doesThis port (FreeBSD-only)
DHCPInternal IPConfiguration agent (years of Apple-internal work)dhcpcd port. DHCPv4 + DHCPv6 + IPv4LL out of the box; netconfigd drives policy via dhcpcd.conf.
RA / SLAACInternal kernel nd6 integrationdhcpcd userspace RA processing — supports RDNSS / DNSSL the kernel doesn't.
Kernel iface eventsPF_SYSTEM + KEV_NETWORK_CLASS + kern_eventPF_ROUTE socket — standard, simpler, less Darwin-specific glue. Observation-only role; dhcpcd watches independently for action.
Service IPCMach ports + XPC + bootstrap serverGNUstep DO over AF_UNIX. Full ObjC messaging; NSPortCoder for marshalling.
WiFi authAirPort framework + eapolclientwpa_supplicant from ports. dhcpcd auto-coordinates via built-in hook.
Iface namingInterfaceNamer plugin: IOKit USB/PCI introspectionFreeBSD kernel handles this. Drop the plugin.
Plugin loadingMach host port + bundle dlopenNo plugins as separate dylibs; link statically into the daemon.
Build-system gatesiOS / macOS / sim / catalyst #if TARGET_OS_*Delete them all. One target.

9. Dependencies

Build-time: clang, lld (FreeBSD base); the gnustep-make framework already installed by the launchd build; pkgconf; system-domain headers/libraries at /System/Library/.

Runtime (Phase 1, on the ISO):

Runtime (Phase 2+, when netconfigd ships):

10. launchd integration

10.1 Phase 1: dhcpcd plist (in place)

<?xml version="1.0" encoding="UTF-8"?>
<plist version="1.0">
<dict>
    <key>Label</key>             <string>org.freebsd.dhcpcd</string>
    <key>ProgramArguments</key>  <array>
        <string>/usr/local/sbin/dhcpcd</string>
        <string>-B</string>
    </array>
    <key>RunAtLoad</key>         <true/>
    <key>KeepAlive</key>         <true/>
</dict>
</plist>

-B keeps dhcpcd in foreground for launchd supervision; without it dhcpcd would daemonize on its own and launchd would think the job ended. KeepAlive=true respawns dhcpcd if it dies.

10.2 Phase 2+: netconfigd plist (planned)

<plist version="1.0">
<dict>
    <key>Label</key>            <string>org.freebsd.netconfigd</string>
    <key>ProgramArguments</key> <array><string>/usr/libexec/netconfigd</string></array>
    <key>RunAtLoad</key>        <true/>
    <key>KeepAlive</key>        <true/>
    <key>Sockets</key>          <dict>
        <key>Listeners</key>    <dict>
            <key>SockPathName</key> <string>/var/run/netconfigd.sock</string>
            <key>SockType</key>     <string>stream</string>
            <key>SockPathMode</key> <integer>438</integer> <!-- 0666 -->
        </dict>
    </dict>
</dict>
</plist>

launchd opens the AF_UNIX listener at boot, hands the fd to netconfigd via the launchd-port's launch_activate_socket-equivalent. Until netconfigd is up, queued connections from scutil / GUI tools sit in the kernel's socket queue.

11. Network.plist schema (Phase 2+)

One canonical declarative configuration file. The GUI tooling reads/writes it; netconfigd watches it and translates to dhcpcd config + resolv.conf.

<?xml version="1.0" encoding="UTF-8"?>
<plist version="1.0">
<dict>
    <key>Hostname</key>
    <string>my-host</string>

    <key>Interfaces</key>
    <dict>
        <key>em0</key>
        <dict>
            <key>Method</key> <string>DHCP</string>
        </dict>

        <key>em1</key>
        <dict>
            <key>Method</key> <string>Static</string>
            <key>IPv4</key>
            <dict>
                <key>Address</key> <string>192.168.1.10/24</string>
                <key>Router</key>  <string>192.168.1.1</string>
            </dict>
        </dict>

        <key>wlan0</key>
        <dict>
            <key>Method</key> <string>WPA</string>
            <key>WPA</key>
            <dict>
                <key>ConfigFile</key> <string>/etc/wpa_supplicant.conf</string>
            </dict>
            <key>IPv4</key>
            <dict>
                <key>Method</key> <string>DHCP</string>
            </dict>
        </dict>
    </dict>

    <key>DNS</key>
    <dict>
        <!-- Optional. If absent, DNS is taken from DHCP. -->
        <key>Servers</key>
        <array>
            <string>1.1.1.1</string>
            <string>1.0.0.1</string>
        </array>
        <key>Search</key>
        <array><string>example.com</string></array>
    </dict>
</dict>
</plist>

Empty / absent Network.plist → dhcpcd's defaults apply (auto-DHCP everything, RA on every iface). This is the Phase 1 behavior; Phase 2+ keeps it as the no-config-needed default.

Per-iface dict missing → dhcpcd's defaults apply for that iface. A user who wants em0 to NOT auto-DHCP adds {"Method": "Disabled"}, which translates to denyinterfaces em0 in the generated dhcpcd.conf.

11.1 Schema versioning

Add a top-level "Version" key (integer) once we have post-v1 schema changes. v1 is implicit; v2+ ships the daemon's accepted version range.

12. scutil CLI tool (Phase 2+)

Apple's scutil is the system-configuration shell. Ported as a DO client of netconfigd. Initial command set:

The interactive Mach-shell mode is dropped (depended on Mach store semantics).

13. Licensing

Top-level BSD-2-Clause; Apple configd source files retain their Apache 2.0 headers per-file (inbound=outbound). New code is BSD-2-Clause with SPDX headers.

SourceLicenseHow we handle it
Apple configd-963.270.3Apache 2.0 (Apple OSRef)Keep Apple header verbatim. Files stay Apache regardless of top-level.
dhcpcd, wpa_supplicant (linked separately, not in tree)BSD-2-Clause / BSD-3-ClauseListed in NOTICE.
GNUstep base / corebase / libobjc2 (linked, not in tree)LGPL 2.1+ / LGPL 3 (linking exception) / MITListed in NOTICE.
libdispatch (linked, not in tree)Apache 2.0 with Runtime Library ExceptionListed in NOTICE.
This repo's new codeBSD-2-ClauseEach new file gets a BSD-2-Clause SPDX header.

14. Phased delivery

Phase 0 — repo scaffold + Apple source import DONE

Phase 1a-c — netconfigd shipped own dhclient + rtsold orchestration SUPERSEDED

Initial NCDaemon implementation enumerated ifaces via getifaddrs(3), ran ifconfig <iface> up, ifconfig <iface> inet6 accept_rtadv auto_linklocal, dhclient -b <iface> per ethernet, and rtsold -a once. Bench testing surfaced gaps: no IPv6 (we never cleared IFDISABLED), no link-flap recovery (devd-shaped flow we'd never built), no DHCPv6 path, no IPv4LL fallback. Dropped in Phase 1d.

Phase 1d — pivot to dhcpcd + ports wpa_supplicant DONE

Phase 2 — netconfigd resumes: dynamic store + DO IPC + Network.plist watcher PLANNED

Phase 3 — scutil port PLANNED

Phase 4 — SCNetworkReachability for apps PLANNED

Phase 5 — declarative WiFi config FUTURE

Phase 6+ — optional, demand-driven FUTURE

15. Open questions

RESOLVED — Internal DHCP client. Closed by Phase 1d. dhcpcd is the answer indefinitely; we don't reimplement DHCPv4/v6 inside the daemon. (Apple did, but it took years; we get the same outcome by using the right port.)
RESOLVED — rtsold for IPv6 RA. Closed by Phase 1d. dhcpcd handles RA in userspace, supports RDNSS and DNSSL the kernel can't. rtsold is gone.
RESOLVED — Repo layout. Monorepo: configd lives under configd/ at the top of freebsd-launchd.
RESOLVED — Daemon binary path. /usr/libexec/netconfigd for the daemon (Phase 2+); /usr/sbin/scutil for the CLI client.
Q. /etc/resolv.conf ownership in Phase 2+. dhcpcd writes resolv.conf via resolvconf(8) by default. When netconfigd ships, it has the option to take over (nohook resolv.conf in dhcpcd.conf, netconfigd writes resolv.conf merging dhcpcd state with plist overrides) or to keep dhcpcd in charge and pass overrides via static domain_name_servers=.... Decision: probably the latter (less work, dhcpcd already integrates with resolvconf cleanly). Settle when Phase 2 implementation begins.
Q. Network.plistdhcpcd.conf translation: drop-in file or whole-file rewrite? dhcpcd reads /etc/dhcpcd.conf.d/*.conf drop-ins after the main /etc/dhcpcd.conf. netconfigd should write /etc/dhcpcd.conf.d/netconfigd.conf and leave the main file untouched — lets advanced users tune dhcpcd globals without netconfigd clobbering. Decision deferrable.
Q. WiFi credentials storage in Phase 5. Inline in Network.plist (PSK in plist) is simple but plaintext. A keychain layer is more secure but more work. Apple did inline for the macOS first iterations; keychain came later. Probably the same path: Phase 5 inline, future phase keychain.
Q. gdomap or direct NSSocketPort. Direct NSSocketPort at /var/run/netconfigd.sock; no gdomap. If we end up with multiple DO services that need lookup, revisit.
Q. Reachability API in Phase 4 — public framework or DO-only? Public framework matches Apple's surface but adds linker-soname-stability concerns. DO-only is simpler. Settle when apps actually need it.
Q. Schema versioning gate. v1 is implicit. Add the "Version" key when v2 ships, or when a GUI tool needs to know which schema it's reading.

16. References