#!/usr/bin/env python3 """OSX-based script to watch for changes to network state and write out a second resolv.conf file containing the DHCP provided nameservers, intended for use with a local resolver such as dnsmasq. This is to workaround the changes in Snow Leopard from Leopard with regards to DNS resolution. ie: the inability to have both manually configured nameservers and DHCP provided ones as well as the issues with split-DNS. usage: python automasq.py /path/to/second/resolv.conf original source: https://gist.github.com/edwardgeorge/270506 """ import optparse import sys from SystemConfiguration import * GLOBAL_KEY = 'State:/Network/Global/IPv4' class Watcher(object): def __init__(self, filename, defaults_filename=None, append_defaults=False): self.filename = filename self.defaults = defaults_filename self.append = append_defaults store = self.store = SCDynamicStoreCreate(None, "automasq", self.dynamicStoreChanged, None) SCDynamicStoreSetNotificationKeys(store, None, [GLOBAL_KEY]) source = self.source = SCDynamicStoreCreateRunLoopSource(None, store, 0) self.write_file(self.get_primary_dns(store)) loop = self.loop = CFRunLoopGetCurrent() CFRunLoopAddSource(loop, source, kCFRunLoopCommonModes) CFRunLoopRun() def write_file(self, servers=[]): with open(self.filename, 'w+') as f: for server in servers: f.write('nameserver %s\n' % server) if (self.append or not servers) and self.defaults is not None: with open(self.defaults) as d: f.write(d.read()) def process_dns_for_service(self, store, service): key = 'State:/Network/Service/%s/DNS' % service val = SCDynamicStoreCopyValue(store, key) data = list(dict(val)['ServerAddresses']) return data def get_primary_dns(self, store=None): store = store or self.store val = SCDynamicStoreCopyValue(store, GLOBAL_KEY) if val: data = dict(val) svcid = data['PrimaryService'] return self.process_dns_for_service(store, svcid) else: return [] def dynamicStoreChanged(self, store, changedKeys, info): servers = [] for key in list(changedKeys): #if key == GLOBAL_KEY: servers = self.get_primary_dns(store) self.write_file(servers) def dummy_timer(*args): pass def main(filename, options): # this gives us a callback into python every 1s for signal handling CFRunLoopAddTimer(CFRunLoopGetCurrent(), CFRunLoopTimerCreate(None, CFAbsoluteTimeGetCurrent(), 1.0, 0, 0, dummy_timer, None), kCFRunLoopCommonModes) try: watcher = Watcher(filename, defaults_filename=options.default, append_defaults=options.append_defaults) except KeyboardInterrupt as e: # exiting pass if __name__ == '__main__': usage = "usage: %prog [options] output-file" parser = optparse.OptionParser(usage) parser.add_option('-d', '--default-conf', dest='default', help='default conf if no resolvers provided', metavar='RESOLVCONF') parser.add_option('-a', '--append', dest='append_defaults', action='store_true', help='always append defaults to generated resolv.conf') opts, args = parser.parse_args() if len(args) != 1: parser.error("specify a single output-file") if opts.append_defaults and not opts.default: parser.error("default conf must be specified to be able to append") main(args[0], opts)