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