Skip to content

Instantly share code, notes, and snippets.

@elijahc
Last active March 6, 2025 19:15
Show Gist options
  • Save elijahc/c6227bdc2389850b15ef5eba9398cb28 to your computer and use it in GitHub Desktop.
Save elijahc/c6227bdc2389850b15ef5eba9398cb28 to your computer and use it in GitHub Desktop.
Split tunneling my vpn connection

Install Openconnect for more flexible Cisco vpn client

Overview

I have a workstation behind a VPN at work that I like to remotely access for queuing jobs or data analysis over hosted jupyter notebooks.

Usually I just connect to using the Cisco Anyconnect client but it's caused some headaches.

I want to be able to route to these workstations using the VPN but since they throttle bandwidth use my local gateway for everything else (i.e. looking up docs, streaming spotify etc)

$ brew install openconnect

openconnect uses a straightforward syntax for connecting. Our VPN is configured to use user/pass for login with an authgroup.

I tested it can connect with the following command

$ openconnect --user=<USERNAME> --authgroup=<GROUP> <VPNHOST> [modifiers]

Setup bash functions to manage connections in the background

Through some googling I found some cool examples of using bash functions to streamline most of this and manage staring and killing the process.

Change sudoer's so that openconnect and kill to not require sudo for starting and stopping the process

function vpnsetup() {
    sudo sh -c 'echo "%admin ALL=(ALL) NOPASSWD: /usr/local/bin/openconnect, /bin/kill" > /etc/sudoers.d/openconnect'
}

Connect using openconnect and read in your vpn password from a local file and passing it to openconnect via stdin.

function vpnstart() {
    cat ~/.vpn_pass | sudo openconnect \
        --background \
        --pid-file="$HOME/.openconnect.pid" \
        --user=$VPNUSER \
        --authgroup=$AUTHGROUP $VPNHOST \
        --passwd-on-stdin
}

Storing the process pid to a file, and then killing the pid loaded from that file when you want to disconnect

function vpnstop() {
    if [[ -f "$HOME/.openconnect.pid" ]]; then
        sudo kill -2 $(cat "$HOME/.openconnect.pid") && rm -f "$HOME/.openconnect.pid"
    else
        echo "openconnect pid file does not exist, probably not running"
    fi
}

Storing passwords in plaintext for loading into scripts makes me nervous so lets make an encrypted password file and load the decrypted version of that.

There's a lot of ways to do this but I chose to use gpg and this is what I did specifically.

$ touch ~/.vpn_pass
$ nano ~/.vpn_pass #Store your plaintext password
$ gpg --encrypt --armor -o ~/.vpn_pass.gpg
$ rm ~/.vpn_pass

I modified the helper functions to decrypt encrypted password files so the password can now be loaded into stdin inline using gpg

$ gpg --decrypt -a -o- ~/.vpn_pass.gpg

Configure VPN Split tunneling

Create a new vpnc script that will be executed on when connecting

on MacOS these files are usually located in /usr/local/etc

$ touch /usr/local/etc/vpnc-script-split
$ sudo chmod 755 /usr/local/etc/vpnc-script-split

Modify to only route traffic to specific hosts over the VPN

# Add one IP to the list of split tunnel
add_ip ()
{
    export CISCO_SPLIT_INC_${CISCO_SPLIT_INC}_ADDR=$1
    export CISCO_SPLIT_INC_${CISCO_SPLIT_INC}_MASK=255.255.255.255
    export CISCO_SPLIT_INC_${CISCO_SPLIT_INC}_MASKLEN=32
    export CISCO_SPLIT_INC=$(($CISCO_SPLIT_INC + 1))
}

# Initialize empty split tunnel list
export CISCO_SPLIT_INC=0

# Delete DNS info provided by VPN server to use internet DNS
# Comment following line to use DNS beyond VPN tunnel
unset INTERNAL_IP4_DNS

# List of workstations beyond VPN tunnel
add_ip xxx.xxx.xxx.xxx  # Workstation 1
add_ip yyy.yyy.yyy.yyy  # Other workstation
add_ip 10.1.0.5         # proxy.mycom.com

# Execute default script
. /etc/vpnc/vpnc-script

# End of script

Finally, pass the --script=</path/to/vpnc-script-split> flag in the openconnect command to restrict vpn traffic to just the ip addresses we denoted in vpnc-script-split

After connecting to the vpn...

$ source bash_funcs.sh
$ vpnsetup
$ vpnstart

... we can use traceroute or route to confirm split tunneling is working correctly

$ route -n get 140.226.100.xxx
   route to: 140.226.100.xxx
destination: 140.226.100.xxx
    gateway: 140.226.4.57
  interface: utun0
      flags: <UP,GATEWAY,HOST,DONE,WASCLONED,IFSCOPE,IFREF>

Notice it uses the remote gateway and the vpn interface utun0

But if we want to hit youtube and stream some phat 4k music videos...

$ route -n get www.youtube.com
   route to: 172.217.3.14
destination: 172.217.3.14
    gateway: 192.168.1.1
  interface: en5
      flags: <UP,GATEWAY,HOST,DONE,WASCLONED,IFSCOPE,IFREF>
 recvpipe  sendpipe  ssthresh  rtt,msec    rttvar  hopcount      mtu     expire
       0         0         0         0         0         0      1500         0

It connects directly over our hardware interface en5 and our local router gateway.

Final form of bash functions attached in separate file.

function vpnsetup() {
sudo sh -c 'echo "%admin ALL=(ALL) NOPASSWD: /usr/local/bin/openconnect, /bin/kill" > /etc/sudoers.d/openconnect'
}
function vpnstart() {
gpg --decrypt -a ~/.vpn_pass.gpg 2>/dev/null | sudo openconnect \
--background \
--pid-file="$HOME/.openconnect.pid" \
--script=/usr/local/etc/vpnc-script-split \
--user=$VPNUSER \
--servercert=sha256:1a341debc187f588029878f6d884182f41a92013e9297b8eaa6dce88b797e65d \
--authgroup=$AUTHGROUP $VPNHOST \
--passwd-on-stdin
}
function vpnstop() {
if [[ -f "$HOME/.openconnect.pid" ]]; then
sudo kill -2 $(cat "$HOME/.openconnect.pid") && rm -f "$HOME/.openconnect.pid"
else
echo "openconnect pid file does not exist, probably not running"
fi
}
# Initialize empty split tunnel list
export CISCO_SPLIT_INC=0
# Delete DNS info provided by VPN server to use internet DNS
# Comment following line to use DNS beyond VPN tunnel
unset INTERNAL_IP4_DNS
# List of workstations beyond VPN tunnel
add_ip xxx.xxx.xxx.xxx # Workstation 1
add_ip yyy.yyy.yyy.yyy # Other workstation
add_ip 10.1.0.5 # proxy.mycom.com
# Execute default script
. /etc/vpnc/vpnc-script
# End of script
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment