How to force split tunnel routing on Mac to a Cisco VPN

Any one know how to hack the routing table (on a mac) to defeat the forcing of VPN routing for every thing over a cisco VPN? pretty much what I want to do is have only 10.121.* and 10.122.* addresses over the VPN and everything else straight to the internet.


The following works for me. Run these after connecting to the Cisco VPN. (I'm using OS X's built-in Cisco client, not the Cisco branded client.)

sudo route -nv add -net 10 -interface utun0
sudo route change default 192.168.0.1

Replace 10 in the first command with the network that's on the other side of the tunnel.

Replace 192.168.0.1 with your local network's gateway.

I put it into a bash script, like this:

$ cat vpn.sh 
#!/bin/bash

if [[ $EUID -ne 0 ]]; then
    echo "Run this as root"
    exit 1
fi

route -nv add -net 10 -interface utun0
route change default 192.168.0.1

I also found an explanation on how to run this automatically when you connect the VPN, but it's late on Friday and I don't feel like trying it :)

Edit:

I have since left the job where I was using the Cisco VPN, so this is from memory.

The 10 in the first command is the network that you want to route over the VPN. 10 is short hand for 10.0.0.0/8. In Tuan Anh Tran's case, it looks like the network is 192.168.5.0/24.

As for which gateway to specify in the second command, it should be your local gateway. When you log into a VPN that prevents split-tunneling, it is enforcing that policy by changing your routing tables so that all packets are routed on the virtual interface. So you want to change your default route back to what it was prior to getting on the VPN.

The easiest way to figure out the gateway is to run netstat -rn before logging into the VPN, and look at the IP address to the right of the "default" destination. For example, here's what it looks like on my box right now:

Internet:
Destination        Gateway            Flags        Refs      Use   Netif Expire
default            10.0.1.1           UGSc           29        0     en1
10.0.1/24          link#5             UCS             3        0     en1
10.0.1.1           0:1e:52:xx:xx:xx   UHLWIi         55   520896     en1    481
10.0.1.51          7c:c5:37:xx:xx:xx  UHLWIi          0     1083     en1    350
10.0.1.52          127.0.0.1          UHS             0        0     lo0

My gateway is 10.0.1.1 — it is to the right of the "default" destination.


Using the information from mehaase, I wrote a Python script that really simplifies this process on the Mac. When you run it, the script will save your firewall info, launch the AnyConnect client, wait for login, then fix the routes and firewall. Just run the script from 'terminal'.

#!/usr/bin/python

# The Cisco AnyConnect VPN Client is often configured on the server to block
# all other Internet traffic. So you can be on the VPN <b>OR</b> you can have
# access to Google, etc.
#
# This script will fix that problem by repairing your routing table and
# firewall after you connect.
#
# The script does require admin (super user) access. If you are prompted for
# a password at the start of the script, just enter your normal Mac login
# password.
#
# The only thing you should need to configure is the vpn_ip_network.
# Mine is 10.x.x.x so I just specify '10' but you could be more specific
# and use something like '172.16'
vpn_ip_network = '10'

import sys
import subprocess


def output_of(cmd):
    lines = subprocess.Popen(cmd if isinstance(cmd, list) else cmd.split(' '), stdout=subprocess.PIPE, stderr=subprocess.STDOUT).communicate()[0]
    try:
        lines = lines.decode('utf-8')
    except Exception:
        pass
    return [line.strip() for line in lines.strip().split('\n')]

sys.stdout.write("Mac Account Login ")
good_firewall_ids = set([line.partition(' ')[0] for line in output_of('sudo ipfw -a list')])
sys.stdout.write('Firewall Saved.\n')

gateway = None
for line in output_of('route get default'):
    name, delim, value = line.partition(':')
    if name == 'gateway':
        gateway = value.strip()
        p = subprocess.Popen(['/Applications/Cisco/Cisco AnyConnect VPN Client.app/Contents/MacOS/Cisco AnyConnect VPN Client'], stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
        was_disconnected = False
        while True:
            line = p.stdout.readline()
            if line == '' or p.poll():
                sys.stdout.write("Never connected!\n")
                break
            try:
                line = line.decode('utf-8')
            except Exception:
                pass
            if 'Disconnected' in line:
                sys.stdout.write('Waiting for you to enter your VPN password in the VPN client...\n')
                was_disconnected = True
            if 'Connected' in line:
                if was_disconnected:
                    subprocess.Popen(['sudo','route','-nv','add','-net',vpn_ip_network,'-interface','utun0'], stdout=subprocess.PIPE, stderr=subprocess.STDOUT).wait()
                    subprocess.Popen(['sudo','route','change','default',gateway], stdout=subprocess.PIPE, stderr=subprocess.STDOUT).wait()
                    unfriendly_firewall_ids = list(set([line.partition(' ')[0] for line in output_of('sudo ipfw -a list')])-good_firewall_ids)
                    extra = ''
                    if unfriendly_firewall_ids:
                        subprocess.Popen('sudo ipfw delete'.split(' ') + unfriendly_firewall_ids, stdout=subprocess.PIPE, stderr=subprocess.STDOUT).wait()
                        sys.stdout.write("VPN connection established, routing table repaired and %d unfriendly firewall rules removed!\n" % len(unfriendly_firewall_ids))
                    else:
                        sys.stdout.write("VPN connection established and routing table repaired!\n")
                else:
                    try:
                        subprocess.Popen.kill(p)
                        sys.stdout.write('VPN was already connected. Extra VPN client closed automatically.\n')
                    except Exception:
                        sys.stdout.write('VPN was already connected. Please close the extra VPN client.\n')
                break
        break
else:
    sys.stdout.write("Couldn't get gateway. :-(\n")

The Python script in this previous answer was helpful, however, it didn't take care of the routes that AnyConnect used to take over other interfaces on the device (such as VMware interfaces). It also wasn't able to handle multiple VPN networks.

Here is the script I use:

#!/bin/bash

HOME_NETWORK=192.168
HOME_GATEWAY=192.168.210.1
WORK_NETWORKS="X.X.X.X/12 10.0.0.0/8 X.X.X.X/16"

# What should the DNS servers be set to?
DNS_SERVERS="10.192.2.45 10.216.2.51 8.8.8.8"

##
## Do not edit below this line if you do not know what you are doing.
##
function valid_ip()
{
    local  ip=$1
    local  stat=1

    if [[ $ip =~ ^[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}$ ]]; then
        OIFS=$IFS
        IFS='.'
        ip=($ip)
        IFS=$OIFS
        [[ ${ip[0]} -le 255 && ${ip[1]} -le 255 \
            && ${ip[2]} -le 255 && ${ip[3]} -le 255 ]]
        stat=$?
    fi
    return $stat
}

# Nuke any DENY firewall rules
for RULE in `sudo ipfw list | grep deny | awk '{print $1}' | xargs`; do sudo ipfw delete $RULE; done

# Delete any routes for the home network that Anyconnect might have made
sudo route delete -net ${HOME_NETWORK}
sudo route add -net ${HOME_NETWORK} ${HOME_GATEWAY}

# Get the AnyConnect interface
ANYCONNECT_INTERFACE=`route get 0.0.0.0 | grep interface | awk '{print $2}'`

# Add the work routes
for NETWORK in ${WORK_NETWORKS}; do
    sudo route -nv add -net ${NETWORK} -interface ${ANYCONNECT_INTERFACE}
done

# Set the default gateway
sudo route change default ${HOME_GATEWAY}

# Mass route changes
for NET in `netstat -nr | grep -e ^${HOME_NETWORK} | grep utun1 | awk '{print $1}' | xargs`; do 
    if valid_ip ${NET}; then
        echo "Changing route for network"
        sudo route change ${NET} ${HOME_GATEWAY}
    else
        echo "Changing route for host"
        sudo route change -net ${NET} ${HOME_GATEWAY}
    fi      
done

# Set the nameservers
sudo scutil << EOF
open
d.init
d.add ServerAddresses * ${DNS_SERVERS}
set State:/Network/Service/com.cisco.anyconnect/DNS
quit
EOF

I wanted a native 'app' that I can run at logon (and keep running/hidden) to enable Split Tunnel routing, similar to a function of Locamatic. Perhaps I'll fork Locamatic at some point and play with it. I may also upload this AppleScript to Github. I didn't want to mess with a daemon as this answer suggests.

This script assumes VPN has default VPN (Cisco IPSec) name and VPN route is 10.10.10.1/22 > 10.10.20.10. These will need to be changed/additional routes added. Run terminal > netstat -rn when VPN is connected (prior to enabling this script) to see VPN-added routes.

This script also generates growl-style notifications in Notification Center :)

enter image description here

I ran into some issues with Mark E. Haase's answer as my Cisco VPN modifies the existing gateway from a UCSc to a UGScI (en0 interface specific) route and adds the VPN gateway as a UCS route, necessitating the deletion of two default gateways and adding back the original UGSc default gateway

Thank goodness for StackExchange/google, this is my first AppleScript and I wouldn't have been able to put it together without a few hours of googling.

Suggestions/corrections/optimizations welcome!

AppleScript (GitHubGist):

global done
set done to 0

on idle
    set status to do shell script "scutil --nc status "VPN (Cisco IPSec)" | sed -n 1p"
    # do shell script "scutil --nc start "VPN (Cisco IPSec)"
    if status is "Connected" then
        if done is not 1 then
            display notification "VPN Connected, splitting tunnel"
            set gateway to do shell script "( netstat -rn | awk '/default/ {if ( index($6, \"en\") > 0 ){print $2} }' ) # gets non-VPN default gateway"
            do shell script "sudo route delete default" # deletes VPN-assigned global (UCS) default gateway
            do shell script "sudo route delete default -ifscope en0" # deletes en0 interface-specific (UGScI) LOCAL non-vpn gateway that prevents it being re-added as global default gateway
            do shell script "sudo route add default " & gateway # re-adds LOCAL non-vpn gateway (from get command above) as global default gateway
            do shell script "sudo route add 10.10.10.1/22 10.10.20.10" # adds VPN route
            display notification "VPN tunnel has been split"
            set done to 1
        end if
    else
        if done is not 2 then
            display notification "VPN Disconnected"
            set done to 2

        end if
    end if
    return 5
end idle

save as an app:

Split Tunnel app save settings

right click>show package contents, add the following to info.plist (this hides the app icon from dock, necessitating the use of Activity Monitor or terminal > pkill -f 'Split Tunnel' to quit the app, omit if you WANT a dock icon:

<key>LSBackgroundOnly</key>
<string>1</string>

create a new one-line routeNOPASSWD file (no extension) using the following code EXACTLY (this can prevent sudo access if done incorrectly, google visudo for more info - this allows the sudo commands in the AppleScript to run WITHOUT a password prompt, omit if you WANT a password prompt when the routing table needs to be changed):

%admin ALL = (ALL) NOPASSWD: /sbin/route

copy this file to /etc/sudoers.d

run the following commands in terminal (second command will prompt for password - this allows the sudo route commands in the AppleScript to run WITHOUT prompting for password, omit if a password prompt is desired when script is changing routing table)

chmod 440 /private/etc/sudoers.d/routeNOPASSWD
sudo chown root /private/etc/sudoers.d/routeNOPASSWD

finally add the app to System Prefs > Users and Groups > login items

Split Tunnel Login Item


More than likely your admin should want to set up VPN connections to use local routing for the 10.121.* and 10.122.* subnets and let the remote (your home machine) route all the rest of the requests. (it saves them bandwidth and liability)

Are you using the Cisco's "VPN Client"? os OS X?

if you use OS X's VPN (set up via the networking Preference Pane) you should be able to click "advanced" and select the "VPN on Demand" tab. then supply the necessary subnets for the VPN to use.