I often find myself changing routes on my OS X system. Usually, however, I’m systematically adding the same routes over and over again, because they were automatically removed after a reboot, network change or whatever. Enter LocationChanger.
It’s basically just a LaunchAgent that gets called every time something network-related changes. Just add this content to ~/Library/LaunchAgents/LocationChanger.plist
(or another name, if you prefer).
Label tech.inhelsinki.nl.locationchanger ProgramArguments /Users/USERNAME/bin/locationchanger WatchPaths /Library/Preferences/SystemConfiguration
And either reboot or run launchctl load ~/Library/LaunchAgents/LocationChanger.plist
.
This will execute the named script (~/bin/locationchanger
) every time the network setup changes. In this script you can do whatever you want. Here are some snippets of code that I use:
#!/bin/bash GROWL_TITLE="Network location detected" PATH="$PATH:/usr/local/bin/" # for growlnotify sleep 2 # Wait for things to settle SSID="$( /System/Library/PrivateFrameworks/Apple80211.framework/Versions/A/Resources/airport -I | grep ' SSID:' | cut -d ':' -f 2 | tr -d ' ' )" EN0IP=`ifconfig en0 | grep 'inet ' | cut -d' ' -f 2` EN1IP=`ifconfig en1 | grep 'inet ' | cut -d' ' -f 2` SSIDS_AROUND="$( /System/Library/PrivateFrameworks/Apple80211.framework/Versions/A/Resources/airport -s | ( read HEADER; cat ) )" logger "Location changer: SSID=$SSID; en0=$EN0IP; en1=$EN1IP;" function bits_to_mask() { BITS="$1" if [ $BITS -le 0 ]; then echo "0" return fi case $BITS in 1) echo "128";; 2) echo "192";; 3) echo "224";; 4) echo "240";; 5) echo "248";; 6) echo "252";; 7) echo "254";; *) echo "255";; esac } function bits_to_subnet() { BITS="${1:-32}" SN="$( bits_to_mask $BITS )" BITS="$(( $BITS - 8 ))" SN="$SN.$( bits_to_mask $BITS )" BITS="$(( $BITS - 8 ))" SN="$SN.$( bits_to_mask $BITS )" BITS="$(( $BITS - 8 ))" SN="$SN.$( bits_to_mask $BITS )" echo $SN } function add_routes() { GW="$1" while read r; do r="${r/\#*}" # remove comments if [ -z "$r" ]; then : # Do nothing on empty lines else # Does it look like "IP subnetmask" ? if echo "$r" | grep "^[[:space:]]*[[:digit:]]\+\.[[:digit:]]\+\.[[:digit:]]\+\.[[:digit:]]\+[[:space:]]\+[[:digit:]]\+\.[[:digit:]]\+\.[[:digit:]]\+\.[[:digit:]]\+[[:space:]]*$"; then ADDR="$( echo "$r" | cut -d' ' -f1 )" MASK="$( echo "$r" | cut -d' ' -f2 )" sudo route add -net -proto1 $ADDR $GW $MASK # proto1 is to recognize them when cleaning up # Does it look like "IP/mask"? elif echo "$r" | grep "^[[:space:]]*[[:digit:]]\+\.[[:digit:]]\+\.[[:digit:]]\+\.[[:digit:]]\+/[[:digit:]]\+[[:space:]]*$"; then ADDR="$( echo "$r" | cut -d/ -f1 )" MASK="$( bits_to_subnet $( echo "$r" | cut -sd/ -f2 ) )" sudo route add -net -proto1 $ADDR $GW $MASK # proto1 is to recognize them when cleaning up # Does it look like "IP"? elif echo "$r" | grep "^[[:space:]]*[[:digit:]]\+\.[[:digit:]]\+\.[[:digit:]]\+\.[[:digit:]]\+[[:space:]]*$"; then ADDR="$r" sudo route add -host -proto1 $ADDR $GW # proto1 is to recognize them when cleaning up else # probably a hostname host "$r" | grep "has address" | sed 's/.*has address //' | while read ip; do sudo route add -host -proto1 $ip $GW done fi fi done } function cleanup_routes() { netstat -nrf inet | perl -ne '@f=split/ +/; print $_ if $f[2] =~ m/1/;' | while read r; do GW="$( echo "$r" | awk '{print $2}' )" if echo "$r" | awk '{print $1}' | grep '/' >/dev/null; then # IP/mask style ADDR="$( echo "$r" | cut -d' ' -f1 | cut -d/ -f1 )" ADDR="$( echo "$ADDR.0.0.0" | perl -pe 's/(\d+\.\d+\.\d+\.\d+)[\d.]*/$1/' )" BITS="$( echo "$r" | cut -d' ' -f1 | cut -d/ -f2 )" MASK="$( bits_to_subnet $BITS )" sudo route delete $ADDR $GW $MASK else # figure out mask from class ADDR="$( echo "$r" | cut -d' ' -f1 )" ADDR="$( echo "$ADDR.0.0.0" | perl -pe 's/(\d+\.\d+\.\d+\.\d+)[\d.]*/$1/' )" FIRST_OCTET="$( echo "$ADDR" | cut -d. -f1 )" if [ $FIRST_OCTET -lt 128 ]; then MASK="255.0.0.0" # Class A elif [ $FIRST_OCTET -lt 192 ]; then MASK="255.255.0.0" # Class B elif [ $FIRST_OCTET -lt 224 ]; then MASK="255.255.255.0" # Class C elif [ $FIRST_OCTET -lt 240 ]; then MASK="" # Class D else MASK="" # Class E fi sudo route delete $ADDR $GW $MASK fi done } cleanup_routes # Possible location scores LOCATIONS=(other HOME WORK) i=0; while [ $i -lt ${#LOCATIONS[@]} ]; do SCORE[$i]=0 i=$(($i+1)) done function location_to_index() { i=0; while [ $i -lt ${#LOCATIONS[@]} ]; do if [ "${LOCATIONS[$i]}" == "$1" ]; then echo $i return fi i=$(($i+1)) done echo "Invalid location $1" >&2 } function add_score() { LOC="$1" D="$2" I="$( location_to_index $LOC )" S="${SCORE[$I]}" S="$(( $S $D ))" SCORE[$I]=$S } # Am I connected to the WORK network? REPLY="$( dig +timeout=1 +tries=1 @10.1.1.1 intranet.work.org )" if [ $? -eq 0 ]; then # Got reply, is in NXDOMAIN or NOERROR? if echo "$REPLY" | grep "status: NOERROR" > /dev/null; then add_score WORK "+1" else add_score WORK "-1" fi fi # Is the WORK wifi around? if echo "$SSIDS_AROUND" | grep "WORK" > /dev/null; then add_score WORK "+1" fi # Am I associated to home if [ "$SSID" == "home" ]; then add_score HOME "+1" else add_score HOME "-1" fi # Is neighbour around if echo "$SSIDS_AROUND" | grep "neighbor" > /dev/null; then add_score HOME "+1" fi SCORES="" MAX_SCORE=0 MAX_SCORE_INDEX= i=0; while [ $i -lt ${#LOCATIONS[@]} ]; do SCORES="$SCORES ${LOCATIONS[$i]}=${SCORE[$i]}" if [ ${SCORE[$i]} -gt $MAX_SCORE ]; then MAX_SCORE=${SCORE[$i]} MAX_SCORE_INDEX=$i fi i=$(($i+1)) done LOCATION=${LOCATIONS[$MAX_SCORE_INDEX]} logger "Location scores:$SCORES" logger "Conclusion: $LOCATION" growlnotify -t "$GROWL_TITLE" -m - <<-EOT Scores:$SCORES Conclusion: $LOCATION EOT case $LOCATION in WORK) # Is the VPN tunnel up? if ps ax | grep 'openvpn.*OUTSIDE' | grep -v grep >/dev/null; then # Route around firewall add_routes 192.168.102.5 <<-EOR # Amazon Web Services # Source: https://forums.aws.amazon.com/ann.jspa?annID=1408 # US East (Northern Virginia): 72.44.32.0/19 67.202.0.0/18 # ... EOR fi # Do I have the guest wifi to evade the firewall? if [ "$SSID" == "guest" ]; then # Route around firewall add_routes 192.168.0.1 <<-EOR irc.freenode.org news.gmane.org EOR fi ;; esac exit 0