Customizing VPN Routing in Mac OS X

I recently had to figure out how to add custom routes through a VPN connection under Mac OS X. It took me awhile to work through the problem, but I was thankfully able to find a reliable solution. Hopefully the notes below will help anyone else facing the same problem.

Configuring the Mac OS X VPN Client

Ethernet cable coming out of a keyhole I was working on a project that required me to use a VPN to connect to a remote network. I was provided with a pre-configured VPN setup to install on a Windows machine, but I also wanted to be able to connect using my Mac.

I was able to use the VPN client built into Mac OS X to setup the connection. (I also added the search domain remote.main for the VPN connection under the Advanced DNS settings.) The client is easy to use and even supports using an RSA SecurID token for authentication, but it does not provide any options for advanced networking routing. Everything would work fine if I configured the machine to send all traffic over the VPN connection, either by setting the service order or in the Advanced Options for the VPN connection.

The problem was that I didn’t want to send all of my traffic over the VPN, just the traffic between my machine and the remote network. The rest of my internet traffic for streaming music, instant messaging, Skype and web browsing would unnecessarily burden the VPN connection. This would not only slow down those activities of mine, but also slow down other people using the VPN.

If I didn’t tell the VPN client to send all traffic over the VPN, then I wasn’t able to connect to all of the machines on the remote network. I needed to explicitly control the network routing.

Analyzing the Problem at the Command-line

The first step was to open up the Terminal and understand what was happening. (I have omitted some of the extraneous text below, only the key lines are shown.) First I looked at how my network interfaces were configured:

%: ifconfig
…
en1: flags=8863 mtu 1500 inet 192.168.1.20 netmask 0xffffff00 broadcast 192.168.1.255
…
ppp0: flags=8051> mtu 1396 inet 172.19.25.11 --> 172.19.25.1 netmask 0xffff0000

The en1 interface is my wireless connection with a local IP of 192.168.1.20 and the ppp0 interface is the VPN connection with an IP on the remote network of 172.19.25.11.

Next, I wanted to see how my various internet traffic was being routed. Were my connections to the internet using the VPN?

%: traceroute www.google.com
traceroute to www.l.google.com (74.125.230.112), 64 hops max, 52 byte packets
 1 192.168.1.1 (192.168.1.1) 55.163 ms 35.808 ms 14.201 ms
 2 83-169-169-58-isp.superkabel.de (83.169.169.58) 227.858 ms 29.495 ms 43.557 ms
 …

That looks good, it goes directly to my local gateway and then out to the internet via my ISP.

What about my connections to the remote network? Is the VPN properly connected? For this I just used my assigned VPN IP address from above.

%: traceroute 172.19.25.11
traceroute to 172.19.25.11 (172.19.25.11), 64 hops max, 52 byte packets
 1 172.19.25.1 (172.19.25.1) 193.050 ms 164.557 ms 196.617 ms
 2 172.19.25.11 (172.19.25.11) 321.015 ms 538.553 ms 371.819 ms

That looks good too, no mention of my local gateway or my ISP. Now to look at a machine on the remote network that I was having trouble connecting to, I’ll call it host.remote.main:

%: traceroute host.remote.main
traceroute to host.remote.main (172.20.50.44), 64 hops max, 52 byte packets
 1 192.168.1.1 (192.168.1.1) 40.987 ms 26.728 ms 50.998 ms
 2 * * *
 ^C

  That confirmed that my machine knew the IP address of the remote machine (172.20.50.44), but that instead of using the VPN connection it was trying to use my local gateway. To understand why I looked at the routing tables:

%: netstat -r
Routing tables
Internet:
Destination  Gateway      Flags  Refs  Use  Netif  Expire
default      192.168.1.1  UGSc     68    51   en1   
default      172.19.25.1  UGScI     0     0  ppp0
…
172.19       ppp0         USc       2    11  ppp0
172.19.25.1  172.19.25.11 UH        1     9  ppp0
…

I had two default entries, one for my wireless connection and one for my VPN. The wireless connection (en1) was listed first, meaning it took precedence. Unless any of the specific rules below applied, my traffic would be sent over the wireless connection. There were two rules that specified the use of my VPN connection (ppp0). The first of these rules had the Destination 172.19, meaning that it applied to any IP address that started with that value. I had seen that route work when I did a traceroute to 172.19.25.11. This rule did not apply to the machine I was having trouble connecting to, since its IP started with 172.20.

Adding a Route Over the VPN

To fix the problem I added a new route to the table to apply to all IPs starting with 172.20:

%: sudo route -v add -net 172.20 -interface ppp0
u: inet 172.20.0.0; u: link ppp0; RTM_ADD: Add Route: len 136, pid: 0, seq 1, …
locks: inits:
sockaddrs: 
 172.20.0.0 ppp0 (0) 0 ffff
add net 172.20: gateway ppp0

%: traceroute host.remote.main
traceroute to host.remote.main (172.20.50.44), 64 hops max, 52 byte packets
 1 172.19.25.1 (172.19.25.1) 61.289 ms 67.725 ms 126.306 ms
 2 172.19.1.1 (172.19.1.1) 364.456 ms 69.946 ms 106.473 ms
 3 192.168.100.9 (192.168.100.9) 98.471 ms 83.573 ms 120.419 ms  
 4 host.remote.main (172.20.50.44) 74.289 ms 282.272 ms 90.462 ms

Now my connection was working! To get to the remote machine, the traffic was now being sent to the ppp0 Gateway of 172.19.25.1 and successfully routed through the remote network. The new route can be viewed by running netstat again and will be deleted when the VPN connection is lost. I didn’t find anyway to automatically recreate this route when the VPN connection is made, so I just keep the command handy and run it every time I reconnect the VPN.