Here's the solution I came up with:
I'm using Growl and HardwareGrowler to trigger a script that writes out a resolver file that dnsmasq is configured to use.
Maybe I'm misunderstanding (Growl's documentation situation is pretty lacking), but I thought Growl 2.1 had the ability to trigger a shell script, in the correct directory, directly. I could not get that to work, however. So, instead, I'm using an Applescript rules script to run the shell script as follows...
In ~/Library/Application Scripts/com.Growl.GrowlHelperApp/Rules.scpt:
using terms from application "Growl"
on evaluate notification with notification
--Rules go in here
if notification's note title contains "IP Address" then
set shell_result to do shell script "/usr/local/sbin/set_nameservers.sh"
end if
--Ultimately return what you want Growl to do with the notification
end evaluate notification
end using terms from
Then, my script /usr/local/sbin/set_nameservers.sh:
#!/bin/bash
# locking variables
LOCKDIR="/tmp/nameservers.lock"
PIDFILE="${LOCKDIR}/nameservers.pid"
# Temporary file for discovered nameservers
TMP_NAMESERVERS_FILE="${LOCKDIR}/nameservers.conf"
# List of public nameservers to add to the list
PUB_NAMESERVERS_FILE="/etc/nameservers_public.conf"
# The nameserver list read in by dnsmasq
SYS_NAMESERVERS_FILE="/etc/nameservers.conf"
# Commands
GROWL="/usr/local/bin/growlnotify"
# Setup a lock in case Growl triggers this script multiple times due to multiple network changes
if mkdir "${LOCKDIR}"; then
echo "$$" >"${PIDFILE}";
echo "# DHCP supplied nameserves:" > "${TMP_NAMESERVERS_FILE}";
# 1) For each interface listed by the networksetup command...
while IFS=" " read -r -a interfaces;
do
((int_count = ${#interfaces[@]} - 1));
for ((i = 0; i <= int_count; i++));
do
# 2) Try to get a nameserver from DHCP. (Only the first of multiple is returned)
nameserver=`/usr/sbin/ipconfig getoption ${interfaces[i]} domain_name_server`
# 3) If a nameserver was returned, add it to our configuration.
if [ ${nameserver} ]; then
echo "nameserver" ${nameserver} >> ${TMP_NAMESERVERS_FILE};
fi
done
done <<< `networksetup -listallhardwareports | grep Device | sed 's/Device: //g'`
# 4) If a file of public nameservers exists, add these to our configuration.
if [ -e ${PUB_NAMESERVERS_FILE} ]; then
cat ${PUB_NAMESERVERS_FILE} >> ${TMP_NAMESERVERS_FILE};
fi
cp ${TMP_NAMESERVERS_FILE} ${SYS_NAMESERVERS_FILE};
# Display a Growl notification showing what our new nameserver config looks like.
${GROWL} -d "us.loranz.steve.set_nameservers" \
-N "Nameserver Configuration" \
-m "`cat ${SYS_NAMESERVERS_FILE}`" \
"System nameservers set to:";
rm -rf "${LOCKDIR}";
else
# Display an informational message if we failed to establish a lock.
${GROWL} -d "us.loranz.steve.set_nameservers" \
-N "Nameserver Configuration Failed" \
-m "$0 ($$) failed to run, lock already established by process: " `cat ${PIDFILE}` \
"Failed to set nameservers:";
exit 1;
fi
exit 0
Then I configured HardwareGrowler to show network events and Growl to fire a ScriptAction for IP Address Changed, Network Link Down, and Network Link Up.
Finally, I set my nameserver in network preferences to be 127.0.0.1 so that I'm hitting dnsmasq.
dnsmasq is then setup with the following options:
resolv-file=/etc/nameservers.conf
all-servers
The first line points dnsmasq to the file the above script is populating with nameservers discovered via DHCP and any public servers you want.
The second line is supposed to enable dnsmasq to send a query to all of the nameservers it knows about and accepts the first response. The Mac OS X resolver should obviate the need for this given resolver files and DHCP domain-search... and relying on that would make you a better netizen than querying every server in your list each time. I may remove that option after some more testing and would appreciate any insight anybody else has on this bit.