Austin Adams

Running Select Applications through a Cisco AnyConnect VPN

August 18, 2016

In an earlier article specific to OpenVPN, I wrote:

To isolate VPN applications from applications running ‘bare,’ I use the following method, which involves nsdo. It has the handy effect of putting each instance of the openvpn client in its own network namespace, allowing you to run a bunch of VPNs at the same time, each available only to programs you choose.

This article covers the same idea, except for Cisco AnyConnect VPNs using OpenConnect, a libre implementation of AnyConnect. (I haven’t tried the official nonfree client, but I’m assuming it’s not worth the trouble, for this purpose at least.)

First, I use the same netns@.service systemd service I wrote in the OpenVPN article by putting it in /etc/systemd/system/:

[Unit]
Description=network namespace %I

[Service]
Type=oneshot
ExecStart=/bin/ip netns add %I
ExecStop=/bin/ip netns del %I
RemainAfterExit=yes

By putting network namespace creation/deletion in a separate unit (rather than a script launched by OpenConnect/OpenVPN at connection time), the VPN uses the same network namespace across restarts. For example, if I have a browser running in a VPN namespace and restart my VPN client, my browser doesn’t stay in an outdated netns after my VPN client starts up, creates a new netns, and changes the netns to which /var/run/netns/X points.

I also created an openconnect@.service, which depends on netns@.service:

[Unit]
Description=OpenConnect VPN Connection: Profile %I
Requires=network.target netns@%i.service
After=network.target netns@%i.service

[Service]
Type=simple
WorkingDirectory=/usr/local/etc/openconnect/
ExecStart=/usr/local/etc/openconnect/openconnect-wrapper %I

[Install]
WantedBy=multi-user.target

Notice that it executes openconnect-wrapper, a wrapper shell script, which I (arbitrarily) chose to put in /usr/local/etc/openconnect/. Here it is:

#!/bin/bash
set -e

[[ -z $1 ]] && {
    printf "$0: usage: $0 <preset>\n" >&2
    exit 1
}
preset="$1"

[[ ! -f $preset.conf ]] && {
    printf "$0: error: '$(pwd)/$preset.conf' does not exist!\n" >&2
    exit 2
}

# Expect the hostname to be the first line in the file, immediately
# preceded by `# '
host="$(head -n 1 "$preset.conf" | cut -b 3-)"
pass="$(cat "$preset.pass" || systemd-ask-password "Password for AnyConnect VPN $preset:")"

# vpnc-script-netns expects this to be set
export NETNS="$preset"
exec openconnect --config "$preset.conf" --script ./vpnc-script-netns --passwd-on-stdin --non-inter "$host" <<<"$pass"

I wrote the script to allow me to have presets for different servers. It reads the hostname from the first line of the config file (after # and a space) and calls systemd-ask-password to ask for the password so that I don’t have to write my password in plaintext anywhere. (When you start the service, you can enter the password by running systemd-tty-ask-password-agent as root.)

Here’s my configuration file for Georgia Tech’s VPN, gatech.conf (the wrapper script reads the comment at the top to determine the hostname):

# https://anyc.vpn.gatech.edu
authgroup=gatech
user=aadams80

The wrapper script also tells OpenConnect to configure the network interfaces using vpnc-script-netns, another shell script I hacked together:

#!/bin/bash
set -e

[[ -z $NETNS ]] && {
    printf "$0"': $NETNS is not set! Please set it to the name of the desired namespace\n' >&2
    exit 1
}

case $reason in
    connect)
        ip netns exec "$NETNS" ip link set dev lo up
        ip link set dev "$TUNDEV" up netns "$NETNS" mtu "$INTERNAL_IP4_MTU"
        # Setup only IPv4 for now
        ip netns exec "$NETNS" ip addr add "$INTERNAL_IP4_ADDRESS" dev "$TUNDEV"
        ip netns exec "$NETNS" ip route add default dev "$TUNDEV"

        # Put DNS servers in /etc/netns/$NETNS/resolv.conf
        mkdir -p "/etc/netns/$NETNS"
        {
            printf '# Generated by vpnc-script-netns\n'
            for dns_server in $INTERNAL_IP4_DNS; do
                printf 'nameserver %s\n' "$dns_server"
            done
        } >"/etc/netns/$NETNS/resolv.conf"
    ;;
esac

Currently, it handles only IPv4 because my university’s VPN doesn’t appear to support IPv6 yet.

I guess that’s it. With a config file named gatech.conf in the same directory as vpnc-script-netns and openconnect-wrapper, I start the VPN with:

# systemctl start openconnect@gatech
# systemd-tty-password-agent
Password for AnyConnect VPN gatech: ***********************

and then run applications in it like:

$ nsdo gatech firefox

Or, using iproute2 instead of nsdo:

# ip netns exec gatech sudo -u $USER firefox

Or using nsenter from util-linux:

# nsenter -n/var/run/netns/gatech sudo -u $USER firefox

Preventing Inactivity Timeouts

After a period of inactivity, the Georgia Tech VPN will close my connection. To prevent this, I just leave an instance of ping running in the namespace:

$ nsdo gatech ping -i 60 gatech.edu &

But that could easily be a systemd unit, so I’ll try to update this post later with one.