Skip to content
macOSDeep Dive Published Updated 8 min readViews unavailable

SCDynamicStore on macOS: Observe Network Configuration Without Guessing

Monitor macOS network configuration changes with SCDynamicStore keys, bounded callbacks, fresh snapshots, and a clear distinction from reachability.

SCDynamicStore exposes key-value state maintained by macOS’s System Configuration service. That store includes active configuration and network state, and it can notify an application when selected keys or key patterns change. This is useful for status displays, diagnostics, and tools that must react when a proxy, computer name, service, or interface configuration changes. It is not a guarantee that a host is reachable, that DNS works, or that an application connection will succeed.

Keep this distinction clear. A configuration notification says that a relevant value changed; it does not describe whether a remote service is healthy. Network.framework connection states and URLSession transfers answer different operational questions. If the goal is to start a request, attempt the request and handle its state. If the goal is to refresh a local configuration view, observe the dynamic store and read the current snapshot after notification.

Create one store session and monitor precise keys

Create an SCDynamicStore session with a stable descriptive name and a callback context whose lifetime exceeds the store. Choose exact keys or narrowly scoped patterns using the documented key constructors and schema. A monitor interested in the active proxy configuration should not subscribe to every key in the store. Narrow subscriptions reduce wakeups and make it clear why a callback fired.

Many System Configuration functions follow Core Foundation ownership conventions. A function named Create or Copy returns a reference that your code must release. Store the session and dispatch queue together in a small owner object, and clear the queue during teardown. Check every Boolean or optional creation result before installing notifications; a failed monitor should not be represented as an empty but healthy configuration.

With the dispatch-queue interface, the overall pattern is:

#include <SystemConfiguration/SystemConfiguration.h>
#include <dispatch/dispatch.h>

static void ConfigurationChanged(SCDynamicStoreRef store,
                                 CFArrayRef changedKeys,
                                 void *context) {
    (void)store;
    (void)changedKeys;
    (void)context;
    /* Re-read the current values; this callback is not a durable event log. */
}

int main(void) {
    SCDynamicStoreContext context = {0, NULL, NULL, NULL, NULL};
    SCDynamicStoreRef store = SCDynamicStoreCreate(
        NULL, CFSTR("com.example.network-config"), ConfigurationChanged, &context);
    if (store == NULL) return 1;

    CFStringRef pattern = SCDynamicStoreKeyCreateNetworkGlobalEntity(
        NULL, kSCDynamicStoreDomainState, kSCEntNetProxies);
    if (pattern == NULL) {
        CFRelease(store);
        return 2;
    }
    const void *values[] = {pattern};
    CFArrayRef patterns = CFArrayCreate(
        NULL, values, 1, &kCFTypeArrayCallBacks);
    if (patterns == NULL || !SCDynamicStoreSetNotificationKeys(store, NULL, patterns)) {
        if (patterns != NULL) CFRelease(patterns);
        CFRelease(pattern);
        CFRelease(store);
        return 3;
    }

    dispatch_queue_t queue = dispatch_queue_create(
        "com.example.network-config", DISPATCH_QUEUE_SERIAL);
    if (!SCDynamicStoreSetDispatchQueue(store, queue)) {
        dispatch_release(queue);
        CFRelease(patterns);
        CFRelease(pattern);
        CFRelease(store);
        return 4;
    }

    /* Keep store and queue alive for the monitor's intended lifetime. */
    dispatch_main();
}

This example monitors a documented global proxy-state key. A production owner should remove the dispatch queue with SCDynamicStoreSetDispatchQueue(store, NULL) before releasing the store and queue. Keep callback context alive until teardown completes. Use a schema-defined pattern for the exact configuration the feature needs; do not subscribe to every key.

Re-read the store as a snapshot

Notifications tell you which keys changed, but they should trigger a fresh read rather than serve as a complete change record. Between notification delivery and processing, another value may have changed. Query the latest values for the relevant keys and publish a coherent snapshot to the UI. If several keys jointly describe the network service, account for partial or temporarily inconsistent states during reconfiguration.

Avoid treating an absent key as a concrete “offline” state unless the schema defines it that way. A key can be missing because no service is configured, because the interface is changing, or because the caller lacks the relevant value. Represent unknown, unavailable, and configured-but-empty states separately where users need to distinguish them.

Keep the callback short. Do not perform DNS lookups, network probes, heavy parsing, or UI updates inline. Enqueue a bounded refresh on the store’s serial queue, coalesce repeated change notifications, and dispatch a normalized immutable value to the main actor. A burst of key changes during Wi-Fi roaming should result in one useful refresh, not dozens of UI reloads.

Treat network configuration and reachability as separate signals

SCNetworkReachability has been deprecated in current documentation because network conditions change too frequently for preflight reachability tests to be reliably useful. Prefer Network.framework path monitoring for limited path observations, and use URLSession’s connectivity behavior or a real request to determine whether a service operation can proceed. A route or interface observation is not proof that a remote host accepts packets.

Use SCDynamicStore when the actual question concerns system configuration, such as “did the active proxy setting change?” Use NWPathMonitor when the feature needs a current path summary. Use a connection state when the product cares whether an endpoint connection is ready. Avoid turning one into a substitute for the others. A VPN can alter routes while the proxy configuration remains constant, and a proxy value can change without making a specific remote service available.

When your product must alter network settings, use the appropriate documented configuration APIs and authorization model. Observing dynamic-store keys is not a supported way to bypass Network Extension, MDM policy, or System Settings. Do not write arbitrary keys into the dynamic store to force network behavior. Configuration writes should go through a managed or user-approved API with a rollback plan.

Use the key-construction APIs and documented schema constants over hard-coded strings wherever available. Patterns are useful when the application must observe a family of per-interface or per-service keys, but an overly broad pattern can make the callback noisy and couple the feature to configuration details it does not understand. Keep parsing behind a small adapter that translates documented key/value combinations into a stable app model. When Apple adds a new field, an unrecognized value should not crash the observer.

Store a generation counter with each refresh. When a second notification arrives while a slow refresh is running, increment the generation and let the old result be discarded before publication. This prevents a stale network snapshot from overwriting a newer one. The refresh may coalesce multiple keys, but it should preserve enough information to update every part of the UI that depends on them. If a key disappears during a transition, publish an explicit unknown or absent value instead of retaining a misleading previous proxy or interface.

Handle callback ownership and shutdown

The store retains or refers to callback context according to the supplied context callbacks. Treat this as a memory-management contract. Avoid a retain cycle in which an owner retains the store and the context retains the owner indefinitely. On stop, unschedule or clear the dispatch queue before dropping the store reference. If callbacks can still be queued, guard them with a generation token or a stopped flag so an old event cannot publish stale state after teardown.

If using a run-loop source rather than a dispatch queue, create the source from the store and add it to a run loop you own. Remove it before releasing the store. Do not schedule a source on a thread that may exit while the observer is still expected to run. Choose one delivery model and make its lifecycle explicit.

Any Core Foundation references copied from the store need clear ownership and conversion rules. Convert CFDictionary values to known types defensively; network configuration dictionaries can contain nested values. Never assume a key’s value is always a string or log the full dictionary. Proxy credentials and other sensitive configuration should not be emitted in diagnostics.

The dynamic store is a system-maintained view, not a durable configuration database for your product. Do not use a key observed on one macOS release as a private persistence contract, and do not build a polling loop that repeatedly copies the entire store. Keep values in memory only as long as the feature needs them. When you need to save user preference, store your own preference separately and never confuse it with the effective network configuration that macOS currently publishes.

Observability should explain both setup and cleanup. Log when the subscription is established, how many relevant keys changed, whether a refresh completed, and why the monitor stopped. Redact the values. Test teardown during a callback and app shutdown during queued refresh work; an observer that remains registered after its owner is gone can leak resources or call into invalid state. A narrow subscription with a defined owner is easier to reason about than a process-wide singleton whose lifecycle is implicit.

Test changes, races, and privacy boundaries

Test Wi-Fi to Ethernet handoff, VPN activation and removal, proxy changes, DHCP renewal, service reorder, sleep/wake, network location changes where applicable, and rapid repeated configuration changes. Confirm the observer converges to the latest state and that notification storms are coalesced. Inject store creation failure and queue setup failure to verify the app displays a degraded but understandable state.

For each monitored key, record why the feature needs it and what user-visible action follows. Avoid gathering interface names, IP addresses, host names, proxy URLs, or VPN details unless required. A privacy-preserving diagnostic can record that a key family changed and the refresh succeeded without exporting its value.

Finally, test the actual network request separately from the configuration observer. A configuration UI can update successfully while the remote service remains offline; that is not an observer failure. Conversely, a request might succeed using a cached connection after configuration changes. A clear separation of snapshots, path status, and endpoint results makes the app more correct and keeps diagnostics honest.

Related:

Sources:

Comments