Frank DENIS random thoughts.

Programmatically changing network configuration on OSX

Programmatically changing DNS resolvers, IP addresses and proxies on OSX isn’t rocket science but finding documentation on that is like looking for a needle in a haystack.

Changing the DNS configuration could have been as simple as updating /etc/resolv.conf, as OSX does provide an /etc/resolv.conf file. But unlike every other Unix variant out there, OSX doesn’t actually read this file, as it is only there for compatibility with some legacy tools.

Meet configd

“The configd daemon is responsible for many configuration aspects of the local system. configd maintains data reflecting the desired and current state of the system, provides notifications to applications when this data changes, and hosts a number of configuration agents in the form of loadable bundles.”

Think about it as a centralized key/value registry. Applications can watch for changes, add new key/value pairs and modify existing entries.

The scutil(8) command provides a command-line interface to this registry:

$ scutil
> list
  subKey [0] = Plugin:IPConfiguration
  subKey [1] = Plugin:InterfaceNamer
  subKey [2] = Setup:
  subKey [3] = Setup:/
  subKey [4] = Setup:/Network/BackToMyMac
  subKey [5] = Setup:/Network/Global/IPv4
  subKey [6] = Setup:/Network/HostNames
  subKey [7] = Setup:/Network/Interface/en0/AirPort
  subKey [8] = Setup:/Network/Service/0CE7A64C-9174-41AC-B634-41F4B6B5FE02
  subKey [9] = Setup:/Network/Service/0CE7A64C-9174-41AC-B634-41F4B6B5FE02/IPv4
  subKey [10] = Setup:/Network/Service/0CE7A64C-9174-41AC-B634-41F4B6B5FE02/IPv6
  subKey [11] = Setup:/Network/Service/0CE7A64C-9174-41AC-B634-41F4B6B5FE02/Interface
  subKey [12] = Setup:/Network/Service/0CE7A64C-9174-41AC-B634-41F4B6B5FE02/Proxies
...  
  subKey [32] = Setup:/Network/Service/527FC752-8B1D-4111-A205-4E81F757704E
  subKey [33] = Setup:/Network/Service/527FC752-8B1D-4111-A205-4E81F757704E/IPv4
  subKey [34] = Setup:/Network/Service/527FC752-8B1D-4111-A205-4E81F757704E/IPv6
  subKey [35] = Setup:/Network/Service/527FC752-8B1D-4111-A205-4E81F757704E/Interface
  subKey [36] = Setup:/Network/Service/527FC752-8B1D-4111-A205-4E81F757704E/Proxies
...
  subKey [58] = Setup:/System
  subKey [59] = State:/IOKit/LowBatteryWarning
  subKey [60] = State:/IOKit/Power/CPUPower
  subKey [61] = State:/IOKit/PowerAdapter
  subKey [62] = State:/IOKit/PowerManagement/Assertions
  subKey [63] = State:/IOKit/PowerManagement/CurrentSettings
  subKey [64] = State:/IOKit/PowerManagement/SystemLoad
  subKey [65] = State:/IOKit/PowerManagement/SystemLoad/Detailed
  subKey [66] = State:/IOKit/PowerSources/InternalBattery-0
  subKey [67] = State:/IOKit/SystemPowerCapabilities
  subKey [68] = State:/Network/BackToMyMac
  subKey [69] = State:/Network/Connectivity
  subKey [70] = State:/Network/Global/DNS
  subKey [71] = State:/Network/Global/IPv4
  subKey [72] = State:/Network/Global/Proxies
  subKey [73] = State:/Network/Interface
  subKey [74] = State:/Network/Interface/en0/AirPort
  subKey [75] = State:/Network/Interface/en0/CaptiveNetwork
  subKey [76] = State:/Network/Interface/en0/IPv4
  subKey [77] = State:/Network/Interface/en0/IPv6
  subKey [78] = State:/Network/Interface/en0/Link
  subKey [79] = State:/Network/Interface/lo0/IPv4
  subKey [80] = State:/Network/Interface/lo0/IPv6
  subKey [81] = State:/Network/Interface/p2p0/Link
  subKey [82] = State:/Network/Interface/utun0/IPv6
  subKey [83] = State:/Network/MulticastDNS
  subKey [84] = State:/Network/NetBIOS
  subKey [85] = State:/Network/PrivateDNS
  subKey [86] = State:/Network/Service/527FC752-8B1D-4111-A205-4E81F757704E/DHCP
  subKey [87] = State:/Network/Service/527FC752-8B1D-4111-A205-4E81F757704E/DNS
  subKey [88] = State:/Network/Service/527FC752-8B1D-4111-A205-4E81F757704E/IPv4
  subKey [89] = State:/Users/ConsoleUser
  subKey [90] = com.apple.DirectoryService.NotifyTypeStandard:DirectoryNodeAdded
  subKey [91] = com.apple.network.identification
  subKey [92] = com.apple.opendirectoryd.node:/Search
  
> get State:/Network/Global/DNS

> d.show
<dictionary> {
  ServerAddresses : <array> {
    0 : 208.67.220.220
    1 : 208.67.222.222
  }
}

As we can see, the registry holds most of the network settings, including DNS settings. The Setup:/* entries contain every available network configuration (as configured in the preferences pane), whereas State:/* entries include only what’s currently active.

There are global DNS settings and configuration-specific DNS setings.

Programmatically accessing the registry

The overlooked DynamicStore API, which is part of the SystemConfiguration framework, let us access the configd registry.

The following code snippet opens the registry, looks for active DNS-related entries, and updates them with a new property list containing OpenDNS resolvers.

#include <SystemConfiguration/SystemConfiguration.h>

static bool setDNS(CFStringRef *resolvers, CFIndex resolvers_count)
{
    SCDynamicStoreRef ds = SCDynamicStoreCreate(NULL, CFSTR("setDNS"), NULL, NULL);
    
    CFArrayRef array = CFArrayCreate(NULL, (const void **) resolvers,
        resolvers_count, &kCFTypeArrayCallBacks);
    
    CFDictionaryRef dict = CFDictionaryCreate(NULL,
        (const void **) (CFStringRef []) { CFSTR("ServerAddresses") },
        (const void **) &array, 1, &kCFTypeDictionaryKeyCallBacks,
        &kCFTypeDictionaryValueCallBacks);    
    
    CFArrayRef list = SCDynamicStoreCopyKeyList(ds,
        CFSTR("State:/Network/(Service/.+|Global)/DNS"));
    
    CFIndex i = 0, j = CFArrayGetCount(list);
    if (j <= 0) {
        return FALSE;
    }
    bool ret = TRUE;
    while (i < j) {
        ret &= SCDynamicStoreSetValue(ds, CFArrayGetValueAtIndex(list, i), dict);
        i++;
    }
    return ret;
}

int main(int argc, const char * argv[])
{
    CFStringRef resolvers[] = {
        CFSTR("208.67.220.220"),
        CFSTR("208.67.222.222")
    };
    setDNS(resolvers, (CFIndex) (sizeof resolvers / sizeof resolvers[0]));
    
    return 0;
}

Of course, changing the registry requires root privileges, but that’s all it takes.

As new entries can be seamlessly created, you can use the registry for centralizing configuration in your own apps, or just to save previous entries before altering them.

Changing network services

The technique describe above works fine. But the new settings aren’t persistent. More often than once, this actually comes in handy, in order to apply temporary settings and to rest assured that once the user reboots his machine, network settings will always be back to a sane state.

Tweaking the system configuration database is not recomended by Apple because some daemons may not be aware of a configuration change. In addition, you may want to permanently apply the new settings.

Another way to tackle this problem is to actually alter the network services.

Under OSX, a network service is a configuration for a network interface. To apply a change globally, we need to step over all network services, check whether DNS (or whatever we want to change) is a support protocol, and make the related changes.

This achieves the same thing as what one can do from the Network preferences pane.

Almost.

After changing settings this way, you will notice that ths Network preferences pane properly reflects them. However, some apps won’t have caught up with the changes. For example, the /etc/resolv.conf isn’t automatically updated.

The trick here is to open the system configuration registry as described above, and then to close it without having done any actual change. Just “touching” the registry will trigger observers like configd so that they can do their own magic.

An empty DNS servers list or an empty IP/network address means that DHCP will be used in order to retrieve this information. But this doesn’t happen automatically as the new settings are applied. Explicitly renewing the DHCP lease is required, by calling SCNetworkInterfaceForceConfigurationRefresh() on each interface.

@implementation DNSGlobalSettings

- (BOOL) DNSSupportForNetworkService: (SCNetworkServiceRef) networkService
{
    if (!SCNetworkServiceGetEnabled(networkService)) {
        return NO;
    }
    SCNetworkInterfaceRef interface = SCNetworkServiceGetInterface(networkService);
    if (!interface) {
        return NO;
    }
    NSArray *supportedProtocols = (__bridge_transfer NSArray *) SCNetworkInterfaceGetSupportedProtocolTypes(interface);
    if ([supportedProtocols indexOfObject: (__bridge NSString *) kSCNetworkProtocolTypeDNS] == NSNotFound) {
        return NO;
    }
    return YES;
}

- (BOOL) setResolversTo: (NSArray *) resolvers forNetworkService: (SCNetworkServiceRef) networkService
{
    if ([self DNSSupportForNetworkService: networkService] == NO) {
        return 0;
    }
    SCNetworkProtocolRef networkProtocol = SCNetworkServiceCopyProtocol(networkService, kSCNetworkProtocolTypeDNS);
    if (networkProtocol == nil || ! SCNetworkProtocolGetEnabled(networkProtocol)) {
        NSLog(@"disabled");
        return FALSE;
    }
    if (![resolvers isKindOfClass: [NSArray class]] || resolvers.count == 0) {
        resolvers = nil;
    }
    if (resolvers == nil) {
        NSLog(@"Setting this service's resolvers to DHCP");
    } else {
        NSLog(@"Setting this service's resolvers to [%@]", resolvers);
    }
    NSDictionary *DNSDict = (__bridge NSDictionary *) SCNetworkProtocolGetConfiguration(networkProtocol);
    NSMutableDictionary *newDNSDict;    
    if (DNSDict) {
        NSLog(@"This interface had an existing configuration");
        if (resolvers == nil) {
            SCNetworkProtocolRef networkProtocol = SCNetworkServiceCopyProtocol(networkService, kSCNetworkProtocolTypeDNS);
            if (networkProtocol == nil || ! SCNetworkProtocolGetEnabled(networkProtocol)) {
                return TRUE;
            }
            NSLog(@"Removing protocol from configuration");
            SCNetworkProtocolSetConfiguration(networkProtocol, NULL);
            return TRUE;
        }
        newDNSDict = [NSMutableDictionary dictionaryWithDictionary: DNSDict];
        if (resolvers == nil) {
            [newDNSDict removeObjectForKey: (__bridge NSString *) kSCPropNetDNSServerAddresses];
        } else {
            [newDNSDict setValue: resolvers forKey: (__bridge NSString *) kSCPropNetDNSServerAddresses];
        }
    } else {
        NSLog(@"This interface had no existing configuration");
        if (resolvers == nil) {            
            return TRUE;
        }
        newDNSDict = [NSMutableDictionary dictionaryWithObject: resolvers forKey: (__bridge NSString *) kSCPropNetDNSServerAddresses];
    }
    
    return SCNetworkProtocolSetConfiguration(networkProtocol, (__bridge CFDictionaryRef) newDNSDict);
}

- (BOOL) touchDynamicStore
{
    SCDynamicStoreRef ds = SCDynamicStoreCreate(NULL, CFSTR("myapp"), NULL, NULL);
    CFRelease(ds);
    NSLog(@"DynamicStore updated");
    
    return TRUE;
}

- (BOOL) forceDHCPUpdate
{
    NSLog(@"Forcing DHCP update");
    NSArray *interfaces = (__bridge_transfer NSArray *) SCNetworkInterfaceCopyAll();
    for (id interface_ in interfaces) {
        SCNetworkInterfaceRef interface = (__bridge SCNetworkInterfaceRef) interface_;
        SCNetworkInterfaceForceConfigurationRefresh(interface);
    }
    return TRUE;
}

- (BOOL) applyToNetworkServices: (BOOL (^)(SCNetworkServiceRef networkService)) cb andCommit: (BOOL) commit
{
    SCPreferencesRef preferences = SCPreferencesCreate(NULL, CFSTR("myapp"), NULL);
    SCPreferencesLock(preferences, TRUE);
    SCNetworkSetRef networkSet = SCNetworkSetCopyCurrent(preferences); 
    NSArray *networkSetServices = (__bridge_transfer NSArray *) SCNetworkSetCopyServices(networkSet);
    BOOL ret = TRUE;
    for (id networkService_ in networkSetServices) {
        SCNetworkServiceRef networkService = (__bridge SCNetworkServiceRef) networkService_;
        ret &= cb(networkService);
    }
    SCPreferencesUnlock(preferences);
    if (commit) {
        SCPreferencesCommitChanges(preferences);
        SCPreferencesApplyChanges(preferences);
    }
    CFRelease(networkSet);    
    CFRelease(preferences);
    if (commit) {
        [self touchDynamicStore];
    }
    return ret;
}

- (BOOL) setResolvers: (NSArray *) resolvers
{
    [self applyToNetworkServices: ^BOOL(SCNetworkServiceRef networkService) {
        return [self setResolversTo: resolvers forNetworkService: networkService];
    } andCommit: YES];   
    [[NSNotificationCenter defaultCenter] postNotificationName: @"CONFIGURATION_CHANGED" object: self userInfo: nil];

    return TRUE;
}

@end