Skip to content

Instantly share code, notes, and snippets.

@KatelynHaworth
Created March 26, 2018 02:33
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save KatelynHaworth/d50ce9167fe0377cd20d6585d77a70b4 to your computer and use it in GitHub Desktop.
Save KatelynHaworth/d50ce9167fe0377cd20d6585d77a70b4 to your computer and use it in GitHub Desktop.
Network change detection on macOS
package main
/*
#cgo CFLAGS: -mmacosx-version-min=10.6 -D__MAC_OS_X_VERSION_MAX_ALLOWED=1080
#cgo LDFLAGS: -framework CoreFoundation -framework SystemConfiguration
#include <SystemConfiguration/SystemConfiguration.h>
*/
import "C"
import "unsafe"
//export nativeNetworkChangeCallback
func nativeNetworkChangeCallback(_ unsafe.Pointer, changedKeys C.CFArrayRef) {
callback(changedKeys)
}
package main
/*
#cgo CFLAGS: -mmacosx-version-min=10.6 -D__MAC_OS_X_VERSION_MAX_ALLOWED=1080
#cgo LDFLAGS: -framework CoreFoundation -framework SystemConfiguration
#include <SystemConfiguration/SystemConfiguration.h>
extern void nativeNetworkChangeCallback(void * context, CFArrayRef changedKeys);
// cfstring_utf8_length returns the number of characters successfully converted to UTF-8 and
// the bytes required to store them.
static inline CFIndex cfstring_utf8_length(CFStringRef str, CFIndex *need) {
CFIndex n, usedBufLen;
CFRange rng = CFRangeMake(0, CFStringGetLength(str));
return CFStringGetBytes(str, rng, kCFStringEncodingUTF8, 0, 0, NULL, 0, need);
}
// MacOS/X Code taken from http://developer.apple.com/technotes/tn/tn1145.html
static OSStatus MoreSCErrorBoolean(Boolean success)
{
OSStatus err = noErr;
if (!success)
{
int scErr = SCError();
if (scErr == kSCStatusOK) scErr = kSCStatusFailed;
err = scErr;
}
return err;
}
static OSStatus MoreSCError(const void *value) {return MoreSCErrorBoolean(value != NULL);}
static OSStatus CFQError(CFTypeRef cf) {return (cf == NULL) ? -1 : noErr;}
static void CFQRelease(CFTypeRef cf) {if (cf != NULL) CFRelease(cf);}
// Create a SCF dynamic store reference and a corresponding CFRunLoop source. If you add the
// run loop source to your run loop then the supplied callback function will be called when local IP
// address list changes.
static OSStatus CreateIPAddressListChangeCallbackSCF(SCDynamicStoreCallBack callback, void *contextPtr, SCDynamicStoreRef *storeRef, CFRunLoopSourceRef *sourceRef)
{
OSStatus err;
SCDynamicStoreContext context = {0, NULL, NULL, NULL, NULL};
SCDynamicStoreRef ref = NULL;
CFStringRef patterns[2] = {NULL, NULL};
CFArrayRef patternList = NULL;
CFRunLoopSourceRef rls = NULL;
assert(callback != NULL);
assert( storeRef != NULL);
assert(*storeRef == NULL);
assert( sourceRef != NULL);
assert(*sourceRef == NULL);
// Create a connection to the dynamic store, then create
// a search pattern that finds all entities.
context.info = contextPtr;
ref = SCDynamicStoreCreate(NULL, CFSTR("AddIPAddressListChangeCallbackSCF"), callback, &context);
err = MoreSCError(ref);
if (err == noErr)
{
// This pattern is "State:/Network/Service/[^/]+/IPv4".
patterns[0] = SCDynamicStoreKeyCreateNetworkInterfaceEntity(NULL, kSCDynamicStoreDomainState, kSCCompAnyRegex, kSCEntNetIPv4);
err = MoreSCError(patterns[0]);
if (err == noErr)
{
// This pattern is "State:/Network/Service/[^/]+/IPv6".
patterns[1] = SCDynamicStoreKeyCreateNetworkInterfaceEntity(NULL, kSCDynamicStoreDomainState, kSCCompAnyRegex, kSCEntNetIPv6);
err = MoreSCError(patterns[1]);
}
}
// Create a pattern list containing just one pattern,
// then tell SCF that we want to watch changes in keys
// that match that pattern list, then create our run loop
// source.
if (err == noErr)
{
patternList = CFArrayCreate(NULL, (const void **) patterns, 2, &kCFTypeArrayCallBacks);
err = CFQError(patternList);
}
if (err == noErr) err = MoreSCErrorBoolean(SCDynamicStoreSetNotificationKeys(ref, NULL, patternList));
if (err == noErr)
{
rls = SCDynamicStoreCreateRunLoopSource(NULL, ref, 0);
err = MoreSCError(rls);
}
// Clean up.
CFQRelease(patterns[0]);
CFQRelease(patterns[1]);
CFQRelease(patternList);
if (err != noErr)
{
CFQRelease(ref);
ref = NULL;
}
*storeRef = ref;
*sourceRef = rls;
assert( (err == noErr) == (*storeRef != NULL) );
assert( (err == noErr) == (*sourceRef != NULL) );
return err;
}
void IPConfigChangedCallback(SCDynamicStoreRef store, CFArrayRef changedKeys, void * context)
{
nativeNetworkChangeCallback(context, changedKeys);
}
*/
import "C"
import (
"log"
"net"
"os"
"os/signal"
"reflect"
"runtime"
"strings"
"unsafe"
)
var (
running bool
storeRef C.SCDynamicStoreRef
sourceRef C.CFRunLoopSourceRef
)
func main() {
log.Println("Starting network change detection")
returnCode := C.CreateIPAddressListChangeCallbackSCF((*[0]byte)(C.IPConfigChangedCallback), nil, &storeRef, &sourceRef)
if returnCode == C.noErr {
go nativeLoop()
} else {
log.Fatalf("failed to setup native hook: error code %#v", returnCode)
}
interruptHandle := make(chan os.Signal)
signal.Notify(interruptHandle, os.Interrupt)
<-interruptHandle
running = false
log.Println("Stopping network change detection")
}
func nativeLoop() {
runtime.LockOSThread()
defer runtime.UnlockOSThread()
C.CFRunLoopAddSource(C.CFRunLoopGetCurrent(), sourceRef, C.kCFRunLoopDefaultMode)
running = true
for running {
C.CFRunLoopRun()
}
C.CFRunLoopRemoveSource(C.CFRunLoopGetCurrent(), sourceRef, C.kCFRunLoopDefaultMode)
C.CFRelease((C.CFTypeRef)(storeRef))
C.CFRelease((C.CFTypeRef)(sourceRef))
}
func callback(changedKeys C.CFArrayRef) {
if (int)(C.CFArrayGetCount(changedKeys)) == 0 {
return
}
changeDetails := cfstringGo((C.CFStringRef)(
C.CFArrayGetValueAtIndex(changedKeys, (C.CFIndex)(0)),
))
ipInterfaceName := strings.Split(changeDetails, "/")[3]
ipInterface, _ := net.InterfaceByName(ipInterfaceName)
log.Printf("Interface changed state: %#v", ipInterface)
}
// cfstring will convert a OSX
// CFStringRef string into a Golang
// compatible string
func cfstringGo(cfs C.CFStringRef) string {
var usedBufLen C.CFIndex
n := C.cfstring_utf8_length(cfs, &usedBufLen)
if n <= 0 {
return ""
}
rng := C.CFRange{location: C.CFIndex(0), length: n}
buf := make([]byte, int(usedBufLen))
bufp := unsafe.Pointer(&buf[0])
C.CFStringGetBytes(cfs, rng, C.kCFStringEncodingUTF8, 0, 0, (*C.UInt8)(bufp), C.CFIndex(len(buf)), &usedBufLen)
sh := &reflect.StringHeader{
Data: uintptr(bufp),
Len: int(usedBufLen),
}
return *(*string)(unsafe.Pointer(sh))
}
@burke
Copy link

burke commented Jun 12, 2019

Hey, does this code have a license? I'm using it to fix an issue with pf and would like to be able to make the hack open-source.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment