How to detect IPv4 address change on macOS using SystemConfiguration framework

I am trying to use the SystemConfiguration on mac OS to get a notification when a new network interface appears on the mac and a new IP address is assigned for it.

I set it up to watch for the system configuration key

State:/Network/Interface
and it works that I get a notification whenever a new network interface appears or disappears.

However I would like to get a notification whenever the IPv4 address is assigned on the new network interface (e.g. by DHCP). I know that the key

State:/Network/Interface/en0/IPv4
is holding the IPv4 address for the en0 interface. But using regular expressions as depicted in the man page for all IPv4 addresses
State:/Network/Interface/.*/IPv4
does not work for the new interface.

I have put together a small minimal code example on github, however one can also use the

scutil
command line tool.


Link to demo repository


main.c

#import <Foundation/Foundation.h>
#import <SystemConfiguration/SystemConfiguration.h>

/* Callback used if a configuration change on monitored keys was detected.
 */
void dynamicStoreCallback(SCDynamicStoreRef store, CFArrayRef changedKeys, void* __nullable info) {
  CFIndex count = CFArrayGetCount(changedKeys);
  for (CFIndex i=0; i<count; i++) {
  NSLog(@"Key \"%@\" was changed", CFArrayGetValueAtIndex(changedKeys, i));
  }
}

int main(int argc, const char * argv[]) {
  NSArray *SCMonitoringInterfaceKeys = @[@"State:/Network/Interface.*"];
  @autoreleasepool {
  SCDynamicStoreRef dsr = SCDynamicStoreCreate(NULL, CFSTR("network_interface_detector"), &dynamicStoreCallback, NULL);
  SCDynamicStoreSetNotificationKeys(dsr, CFBridgingRetain(SCMonitoringInterfaceKeys), NULL);
  CFRunLoopAddSource(CFRunLoopGetCurrent(), SCDynamicStoreCreateRunLoopSource(NULL, dsr, 0), kCFRunLoopDefaultMode);
  NSLog(@"Starting RunLoop...");
  while([[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]]);
  }
  return 0;
}

Accepted Reply

What you’d normally do here is pass

NULL
to the
keys
parameter of
SCDynamicStoreSetNotificationKeys
and then pass an array containing a regex to the
patterns
parameter. The regex would look something like
State:/Network/Interface/[^/]+/IPv4
. If you do this then you can extract the interface name for each change from the keys passed to the
changedKeys
parameter of your callback.

Having said that, I generally avoid doing this and just use the notification as a trigger to re-read the dynamic store and re-generate my state. The problem with relies on the details of the notification is that it’s brittle: If something goes wrong you never recover.

Share and Enjoy

Quinn “The Eskimo!”
Apple Developer Relations, Developer Technical Support, Core OS/Hardware

let myEmail = "eskimo" + "1" + "@apple.com"

Replies

It seems like you are almost there. I looked at your code and changed the first two lines of main():


    NSArray *SCMonitoringInterfaceKeys = @[@"State:/Network/Interface"];
    NSArray * patterns = @[@".*/IPv4"];


Then I used the new patterns:

SCDynamicStoreSetNotificationKeys(dsr, CFBridgingRetain(SCMonitoringInterfaceKeys), CFBridgingRetain(patterns));


And finally, changed the callback:

        CFStringRef key = CFArrayGetValueAtIndex(changedKeys, i);
        NSLog(@"Key \"%@\" was changed", key);
        CFPropertyListRef ref = SCDynamicStoreCopyValue(store,key);
        NSDictionary * dict = (NSDictionary *)CFBridgingRelease(ref);
        NSLog(@"Value %@", dict);


It seems to work.

Thanks for your help. I did not get that the key and the pattern are two different arguments to the SCDynamicStoreSetNotificationKeys call.


For me I would like to find out on which network interface the IPv4 address was changed. When implementing it liek you proposed it worked in the sense that the callback is triggered when the new network interface got its new IPv4 address, but I only get the following values


2019-02-11 21:04:28.765922+0100 NetworkInterfaceDetector[4669:308498] Key "State:/Network/Interface" was changed
2019-02-11 21:04:28.771153+0100 NetworkInterfaceDetector[4669:308498] Value {
    Interfaces =     (
        lo0,
        gif0,
        stf0,
        XHC20,
        en1,
        en0,
        p2p0,
        awdl0,
        bridge0,
        utun0,
        en5,
        vlan0,
        XHC0,
        en6
    );
}


Is that all I can get? If that would be the case I would have to do the bookeeping of the interfaces myself, e.g. after boot getting the content of the State:/Network/Interface dict and when I get the callback I would compare the new dict to the old one. The added or created interfaces would be the ones I am looking for.


Or is there another smarter way?

What you’d normally do here is pass

NULL
to the
keys
parameter of
SCDynamicStoreSetNotificationKeys
and then pass an array containing a regex to the
patterns
parameter. The regex would look something like
State:/Network/Interface/[^/]+/IPv4
. If you do this then you can extract the interface name for each change from the keys passed to the
changedKeys
parameter of your callback.

Having said that, I generally avoid doing this and just use the notification as a trigger to re-read the dynamic store and re-generate my state. The problem with relies on the details of the notification is that it’s brittle: If something goes wrong you never recover.

Share and Enjoy

Quinn “The Eskimo!”
Apple Developer Relations, Developer Technical Support, Core OS/Hardware

let myEmail = "eskimo" + "1" + "@apple.com"

That's not what I get using your code and my modifications. I get this:


2019-02-12 11:54:44.099901-0500 NetworkInterfaceDetector[34974:3562167] Key "State:/Network/Interface/en0/IPv4" was changed
2019-02-12 11:54:44.101660-0500 NetworkInterfaceDetector[34974:3562167] Value {
    Addresses =     (
        "192.168.0.129"
    );
    BroadcastAddresses =     (
        "192.168.0.255"
    );
    SubnetMasks =     (
        "255.255.255.0"
    );
}
2019-02-12 11:54:44.103045-0500 NetworkInterfaceDetector[34974:3562167] Key "State:/Network/Service/D889D21F-FE3D-4DDB-BB03-A35A59A68D3E/IPv4" was changed
2019-02-12 11:54:44.103487-0500 NetworkInterfaceDetector[34974:3562167] Value {
    ARPResolvedHardwareAddress = "***";
    ARPResolvedIPAddress = "192.168.0.1";
    AdditionalRoutes =     (
                {
            DestinationAddress = "192.168.0.129";
            SubnetMask = "255.255.255.255";
        },
                {
            DestinationAddress = "169.254.0.0";
            SubnetMask = "255.255.0.0";
        }
    );
    Addresses =     (
        "192.168.0.129"
    );
    ConfirmedInterfaceName = en0;
    InterfaceName = en0;
    NetworkSignature = "IPv4.Router=192.168.0.1;IPv4.RouterHardwareAddress=***";
    Router = "192.168.0.1";
    SubnetMasks =     (
        "255.255.255.0"
    );
}
2019-02-12 11:54:44.118396-0500 NetworkInterfaceDetector[34974:3562167] Key "State:/Network/Global/IPv4" was changed
2019-02-12 11:54:44.119072-0500 NetworkInterfaceDetector[34974:3562167] Value {
    PrimaryInterface = en0;
    PrimaryService = "D889D21F-FE3D-4DDB-BB03-A35A59A68D3E";
    Router = "192.168.0.1";
}
2019-02-12 11:54:44.632036-0500 NetworkInterfaceDetector[34974:3562167] Key "State:/Network/Global/IPv4" was changed
2019-02-12 11:54:44.633991-0500 NetworkInterfaceDetector[34974:3562167] Value {
    PrimaryInterface = en0;
    PrimaryService = "D889D21F-FE3D-4DDB-BB03-A35A59A68D3E";
    Router = "192.168.0.1";
}
2019-02-12 11:54:47.550063-0500 NetworkInterfaceDetector[34974:3562167] Key "State:/Network/Global/IPv4" was changed
2019-02-12 11:54:47.551883-0500 NetworkInterfaceDetector[34974:3562167] Value {
    PrimaryInterface = en0;
    PrimaryService = "D889D21F-FE3D-4DDB-BB03-A35A59A68D3E";
    Router = "192.168.0.1";
}


I redacted my hardware address. That seems to be the information you need. I turned my wifi off and then back on. This is the result of turning it on.