Skip to content

Instantly share code, notes, and snippets.

@agilecreativity
Created May 16, 2019 03:46
Show Gist options
  • Save agilecreativity/697235ec3f4b20715871596594ace700 to your computer and use it in GitHub Desktop.
Save agilecreativity/697235ec3f4b20715871596594ace700 to your computer and use it in GitHub Desktop.
#!/usr/bin/env bash
#
# DESC
# Bash script to manage Cisco AnyConnect VPN connections.
#
# Integrates with OS X KeyChain for password accesss.
#
# USAGE
# To use this script to manage VPN connections, create the following:
#
# Keychain Entries for each VPN
#
# - Open the Keychain App
# - Select "File" > "New Password Item..."
# - Specify the Keychain Item Name (becomes KEYCHAIN_ID in our conf file, see below)
# - Specify the Account Name (becomes KEYCHAIN_ACCOUNT in our conf file, see below)
# - Specify the Password using the password you use to connect to this VPN.
#
# I believe you could set this up to use your existing user's keychain
# entry so that your VPN creds will update as you change the password on
# your local account. As it is, I am currently going in and manually
# updating the password on my DLX vpn Keychain entry as my password
# changes.
#
# ~/.vpn/vpns.conf
# This file defines your available VPNs. It should look something like
# this:
#
# [dlx]
# VPN_HOSTNAME=vpn1.datalogix.com
# VPN_GROUP=odc-user
# VPN_USERNAME=user.name
# KEYCHAIN_ID=dlx-vpn
# KEYCHAIN_ACCOUNT=user.name
#
# [ora]
# VPN_HOSTNAME=myaccess.oraclevpn.com
# VPN_GROUP=NONE
# VPN_USERNAME=username_us
# KEYCHAIN_ID=ora-vpn
# KEYCHAIN_ACCOUNT=user.name
#
# The above file defines two VPN connections that the script will
# understand as 'dlx' and 'ora' (e.g. vpn connect dlx).
#
# TODO:
# - Improve 'usage', particularly around args for commands that support them.
#
cmds=('connect' 'con' 'disconnect' 'dis' 'status' 'stat' 'list' 'ls')
keys=(
'VPN_ID'
'VPN_HOSTNAME'
'VPN_GROUP'
'VPN_USERNAME'
'KEYCHAIN_ID'
'KEYCHAIN_ACCOUNT'
)
declare -r VPN_ID=0
declare -r VPN_HOSTNAME=1
declare -r VPN_GROUP=2
declare -r VPN_USERNAME=3
declare -r KEYCHAIN_ID=4
declare -r KEYCHAIN_ACCOUNT=5
die() {
echo -e >&2 "$@"
exit 1
}
msg() {
echo -e >&2 "$@"
}
usage() {
if [ -n "$1" ]; then echo -e >&2 "$1"; fi
die "Usage: vpn $(IFS=\|; echo "${cmds[*]}") [args]"
}
array_contains () {
local el="$1"
local arr=("${@:2}")
for e in "${arr[@]}"; do [[ "${e}" == "${el}" ]] && return 0; done
return 1
}
load_vpns() {
local config_file=~/.vpn/vpns.conf
local array_name=''
local line_num=0
while read line; do
((++line_num))
if [[ $line =~ ^[[:space:]]*$ ]]; then
continue
elif [[ $line =~ ^\[([[:alpha:]][[:alnum:]]*)\]$ ]]; then
array_name=${BASH_REMATCH[1]}
vpn_names+=($array_name)
# Need the arrays declared here to be global (i.e. no declare -a ...).
eval "${array_name}=()"
# Set the VPN_ID in the config.
eval ${array_name}[${VPN_ID}]=${array_name}
elif [[ $line =~ ^([^=]+)=(.*)$ ]]; then
[[ -n array_name ]] || die "Config file error line $line_num: no array name defined"
key="${BASH_REMATCH[1]}"
val="${BASH_REMATCH[2]}"
[[ ${keys[*]} =~ ${key} ]] || die "Config file error line $line_num: illegal key '${key}'"
eval ${array_name}[${key}]=${val}
else
die "*** Error line $line_num: $line"
fi
done < $config_file
# TODO: finish this...
for vpn_name in "${vpn_names[@]}"; do
eval config=( \${$vpn_name[@]} )
if (( ${#config[@]} != ${#keys[@]} )); then
echo 'bad config...'
fi
done
}
get_password() {
local keychain_account=$1
local keychain_id=$2
echo $(security find-generic-password \
-gw -l "${keychain_id}")
}
get_active_connection() {
local expr='notice: Connected to '
local con=$(/opt/cisco/anyconnect/bin/vpn stats |grep "${expr}")
local vpn_host=$(echo $con |sed -e "s/.*${expr}//" -e 's/\.*$//')
echo $vpn_host
}
get_vpn_by_connection() {
local vpn_host=$1
local domain=$(echo $vpn_host |awk -F. '{print $(NF-1)"."$NF}')
for vpn_name in "${vpn_names[@]}"; do
eval config=( \${$vpn_name[@]} )
if [[ ${config[$VPN_HOSTNAME]} =~ .*${domain}$ ]]; then
echo ${config[$VPN_ID]}
return
fi
done
die "vpn not found for vpn host '${vpn_hostname}'"
}
get_active_vpn() {
vpn_host=$(get_active_connection)
local vpn
if [ -n "${vpn_host}" ]; then
vpn=$(get_vpn_by_connection ${vpn_host})
else
vpn=none
fi
echo "${vpn}"
}
connected() {
[ -n "$(get_active_connection)" ] && return 0
return 1
}
connect() {
local vpn_name=$1
array_contains "${vpn_name}" "${vpn_names[@]}" || die "Invalid vpn id '${vpn_name}'"
local current_vpn=$(get_active_vpn)
if [[ "${current_vpn}" == "${vpn_name}" ]]; then
echo "Already connected to '${vpn_name}'"
return
elif [[ "${current_vpn}" != 'none' ]]; then
echo "Disconnecting from '${current_vpn}'..."
disconnect
sleep 1
fi
# Get the vpn configuration array from the vpn name.
eval local vpn=( \${$vpn_name[@]} )
local hostname=${vpn[$VPN_HOSTNAME]}
local group=${vpn[$VPN_GROUP]}
local username=${vpn[$VPN_USERNAME]}
local password=$(get_password ${vpn[$KEYCHAIN_ACCOUNT]} ${vpn[$KEYCHAIN_ID]})
#
# Build up format string and args for printf. We can't simply embed '\n'
# between the args and pass it diretly to printf as an argument may contain
# a printf formatting character in it (e.g. %).
#
[ ${group} != "NONE" ] && { format+="%s\n"; options+=("${group}"); }
[ -n ${username} ] && { format+="%s\n"; options+=("${username}"); }
[ -n ${password} ] && { format+="%s\n"; options+=("${password}"); }
#
# Note the follwoing below:
#
# - The awk command. For some reason, rather than simply exiting on a
# failed login attempt when reading from stdin, the Cisco vpn client just
# keeps trying over an over again until the user kills it. This can have
# some fairly unpleasant side affects (e.g. if you're login is tied to an
# AD or LDAP provider, your account may get locked out on multiple failed
# login attempts). The awk command below will look for the string 'Login
# failed' and force an exit.
#
# - The sed command. The command line output inclued the password in
# plain text, which obviusly we don't want. It outputs the password on
# the same line as the username, which _would_ be worth seeing in the
# output. The sed command removes the password from the output while
# preserveng the username.
#
# - The grep command.
# Just playing it safe.
#
printf "${format}" $(IFS=' ';echo "${options[*]}") \
| /opt/cisco/anyconnect/bin/vpn -s connect ${hostname} \
| awk -v s="Login failed" '$0~s{print $0; exit(1)} 1' \
| sed 's/^\(Username: \[.*\]\).*/\1/' \
| grep -vi 'password:'
}
disconnect() {
if connected; then
/opt/cisco/anyconnect/bin/vpn disconnect
else
echo 'No connection found'
fi
}
status() {
echo "connection: $(get_active_vpn)"
}
list() {
local arg=$1
for vpn_name in "${vpn_names[@]}"; do
echo ${vpn_name}
if [[ "${arg}" == '-d' || "${arg}" == '--details' ]]; then
eval vpn_config=( \${$vpn_name[@]} )
echo " VPN_ID: ${vpn_config[$VPN_ID]}"
echo " VPN_HOSTNAME: ${vpn_config[$VPN_HOSTNAME]}"
echo " VPN_GROUP: ${vpn_config[$VPN_GROUP]}"
echo " VPN_USERNAME: ${vpn_config[$VPN_USERNAME]}"
echo " KEYCHAIN_ID: ${vpn_config[$KEYCHAIN_ID]}"
echo " KEYCHAIN_ACCOUNT: ${vpn_config[$KEYCHAIN_ACCOUNT]}"
fi
done
}
main() {
local cmd=$1
local cmd_args=${@:2}
case "${cmd}" in
connect|con)
load_vpns
connect "${cmd_args[0]}"
;;
disconnect|dis)
disconnect
;;
status|stat)
load_vpns
status
;;
list|ls)
load_vpns
list "${cmd_args[0]}"
;;
esac
# for vpn_name in "${vpn_names[@]}"; do
# eval this_arr=( \${$vpn_name[@]} )
# echo 'vpn_name: ' $vpn_name
# echo 'config: ' ${this_arr[@]}
# echo "VPN_ID: ${this_arr[$VPN_ID]}"
# echo "VPN_GROUP: ${this_arr[$VPN_GROUP]}"
# echo "GROUP: ${this_arr[$GROUP]}"
# echo "USERNAME: ${this_arr[$USERNAME]}"
# echo "KEYCHAIN_ID: ${this_arr[$KEYCHAIN_ID]}"
# echo "KEYCHAIN_ACCOUNT: ${this_arr[$KEYCHAIN_ACCOUNT]}"
# done
}
[ $# -ge 1 ] || usage
cmd=${1}
cmd_args=${@:2}
if [[ ! ${cmds[*]} =~ ${cmd} ]]; then
usage "Illegal command (${cmd})."
fi
vpn_names=()
main $cmd $cmd_args
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment