Skip to content

Instantly share code, notes, and snippets.

@johnhpatton
Last active November 5, 2020 15:00
Show Gist options
  • Save johnhpatton/30f91c13c3b6e249a7b0cf06918cb60f to your computer and use it in GitHub Desktop.
Save johnhpatton/30f91c13c3b6e249a7b0cf06918cb60f to your computer and use it in GitHub Desktop.
Wrapper for openconnect to assist with modern enterprise VPN connections that are unsupported by IT departments.
#!/bin/bash
# Pre-Requisites
#
# Install openconnect v8 or higher.
#
# Setup
#
# 1. Place script under: /usr/sbin/openconnect.ctl
#
# 2. Chown and chmod the script as follows:
#
# chown root:root /usr/sbin/openconnect.ctl
# chmod +x /usr/sbin/openconnect.ctl
#
# 3. Set the following aliases
#
# alias vpndisconnect='sudo /usr/sbin/openconnect.ctl -d'
# alias vpnstatus='sudo /usr/sbin/openconnect.ctl -s'
#
# 4a. Set the following aliases for anyconnect:
#
# alias vpnconnect='sudo /usr/sbin/openconnect.ctl -u -n USERNAME -a HOST'
# alias vpnreconnect='sudo /usr/sbin/openconnect.ctl -r -n USERNAME -a HOST'
#
# 4b. Set the following aliases for global protect:
#
# alias vpnconnect='sudo /usr/sbin/openconnect.ctl -u -n USERNAME -g HOST'
# alias vpnreconnect='sudo /usr/sbin/openconnect.ctl -r -n USERNAME -g HOST'
#
# 5. get the group for your user and add the following to /etc/sudoers.d/vpn:
#
# Cmnd_Alias OPENCONNECT_CMDS = /usr/sbin/openconnect.ctl
# %YOUR_GROUP_HERE ALL=(root) NOPASSWD: OPENCONNECT_CMDS
#
# NOTE: replace "YOUR_GROUP_HERE" in the above line with the primary group
# for your account.
#
# 6. logout/back in, run vpnconnect to connect.
#
# TROUBLESHOOTING
#
# * If the system is having trouble connecting to the VPN host, check the
# /etc/resolv.conf file to see if the DNS configuration was not cleaned
# after the previous VPN connection and reset the ethernet connection
# if it still has the VPN DNS configuration in it.
#
# * If the VPN configuration is set up with MFA, use the password for the
# account when prompted with "MFA Token:" and the token when prompted
# with "Challenge:".
# bin folder, do not dereference symlinks to maintain clean structures
_CTL=${0##*/}
CTL_BIN="$(cd "$(dirname -- "$0")" && pwd)"
# absolute path to control script and capture args in global var
_CMD=${CTL_BIN}/${_CTL}
_ARGS=$*
# Set INTERACTIVE to 1 if interactive, otherwise 0
INTERACTIVE=$(( ! $(expr index "$-" i) == 0 ))
# Needed for basic operation
OPENCONNECT_PID="/var/run/openconnect.pid"
OPENCONNECT_CSD_POST_SCRIPT="${CTL_BIN}/csd-post.sh"
CSD_POST_URL="https://gitlab.com/openconnect/openconnect/raw/master/trojans/csd-post.sh"
# Needed for GlobalProtect:
OPENCONNECT_HIP_REPORT_POST_SCRIPT="${CTL_BIN}/hipreport.sh"
HIP_REPORT_POST_URL="https://gitlab.com/openconnect/openconnect/raw/master/trojans/hipreport.sh"
SOCAT_PID="/var/run/socat.pid"
dmesg | grep -i hypervisor &>/dev/null && MACHINE_TYPE="vm" || MACHINE_TYPE="host"
if [ "${MACHINE_TYPE}" == "vm" ]; then
# use last NIC for SOCAT routing on VM if two NICs are provisioned
SOCAT_DEVICE="$(cat /proc/net/dev | grep enp | awk '{print substr($1, 1, length($1)-1)}' | tail -1)"
else
# use last NIC for SOCAT routing on host
SOCAT_DEVICE="$(cat /proc/net/dev | grep -E '(eth|en|wlp)' | awk '{print substr($1, 1, length($1)-1)}' | tail -1)"
fi
###################################################################
## ##
## L O W L E V E L F U N C T I O N S ##
## ##
###################################################################
# DATETIME "macro"
# Date/Time Stamp
# Returns now in format: YYYYMMDD-HH:MM:SS
DATETIME() {
date "+%Y%m%d-%T"
}
# trim()
# Trims white space on passed in string
trim() {
set -f
set -- $*
printf "%s\\n" "$*"
set +f
}
###################################################################
## ##
## U S E R M E S S A G E F U N C T I O N S ##
## ##
###################################################################
# Interactive or not?
[ -t "0" ] && INTERACTIVE=1 || INTERACTIVE=0
# If interactive, set output colors:
if (( INTERACTIVE )); then
# CONSTANTS
# foreground colors, use with echo -e
NC='\e[0m' # No Color, reset
# normal; bold; high intensity; bold + high intensity
BLACK='\e[0;30m'; BBLACK='\e[1;30m'; IBLACK='\e[0;90m'; BIBLACK='\e[1;90m';
RED='\e[0;31m'; BRED='\e[1;31m'; IRED='\e[0;91m'; BIRED='\e[1;91m';
GREEN='\e[0;32m'; BGREEN='\e[1;32m'; IGREEN='\e[0;92m'; BIGREEN='\e[1;92m';
YELLOW='\e[0;33m'; BYELLOW='\e[1;33m'; IYELLOW='\e[0;93m'; BIYELLOW='\e[1;93m';
BLUE='\e[0;34m'; BBLUE='\e[1;34m'; IBLUE='\e[0;94m'; BIBLUE='\e[1;94m';
PURPLE='\e[0;35m'; BPURPLE='\e[1;35m'; IPURPLE='\e[0;95m'; BIPURPLE='\e[1;95m';
CYAN='\e[0;36m'; BCYAN='\e[1;36m'; ICYAN='\e[0;96m'; BICYAN='\e[1;96m';
WHITE='\e[0;37m'; BWHITE='\e[1;37m'; IWHITE='\e[0;97m'; BIWHITE='\e[1;97m';
fi
# loggers
# adjust colors for terminal display needs
logdebug() { (( DEBUG )) && logmsg "${WHITE}DEBUG: ${NC}$1${NC}"; }
loginfo() { logmsg "${BICYAN} INFO: ${NC}$1${NC}"; }
logok() { logmsg "${BIGREEN} OK: ${NC}$1${NC}"; }
logwarn() { logmsg "${BIYELLOW} WARN: ${NC}$1${NC}"; }
logcrit() { logmsg "${BIRED} CRIT: ${NC}$1${NC}"; }
logmsg() { [[ ${FUNCNAME[1]} =~ log.* ]] && echo -e "$1" || loginfo "$1"; }
# is_running
# returns: 0 (success, running)
# 1 (failure, not running)
is_running() {
local pidfile="$1"
local retval=1
if [ -f "${pidfile}" ]; then
if pgrep --pidfile "${pidfile}" &>/dev/null; then
retval=0
elif (( $EUID == 0 )) && ! kill -0 $(<"${pidfile}") &>/dev/null; then
rm -f "${pidfile}"
fi
fi
return $((retval))
}
###################################################################
## ##
## C O N T R O L F U N C T I O N S ##
## ##
###################################################################
# display_usage
# Displays a simple usage message
display_usage() {
cat <<USAGE_MESSAGE
openconnect.ctl options:
$_CTL { {-u|--up} | {-d|--down} | {-r|--restart} | {-s|--status} }
{-p|--proxy} {-t|--troubleshoot} {-h|--help}
For full usage help, use: $_CTL -a help
USAGE_MESSAGE
}
# display_help
# Displays a simple usage message
display_help() {
cat <<HELP_MESSAGE
openconnect.ctl options:
$_CTL { {-u|--up} | {-d|--down} | {-r|--restart} | {-s|--status} }
{-p|--proxy} {-t|--troubleshoot} {-h|--help}
-u|--up - Bring the VPN up.
-d|--down - Tear the VPN/proxy down.
-r|--reconnect - Disconnect/Reconnect the VPN/proxy.
-s|--status - Display the runtime status of the VPN/proxy.
-p|--proxy - Enable a forward DNS proxy listener for host
routing DNS lookups over the VPN connection
-h|--help - this message
HELP_MESSAGE
}
requires_admin() {
if (( $EUID != 0 )); then
echo "ERROR: Run as root or with sudo."
exit 1
fi
}
openconnect_start_opts() {
local openconnect_opts="--background "
local csd_script=""
local csd_source=""
(( DEBUG )) && openconnect_opts+="-vvv -dump "
openconnect_opts+="--protocol=${OPENCONNECT_PROTOCOL} "
openconnect_opts+="--user=${OPENCONNECT_USER} "
openconnect_opts+="--pid-file=${OPENCONNECT_PID} "
if [ "${OPENCONNECT_PROTOCOL}" == "anyconnect" ]; then
csd_script="${OPENCONNECT_CSD_POST_SCRIPT}"
csd_source="${CSD_POST_URL}"
openconnect_opts+="--csd-user=${OPENCONNECT_USER} "
elif [ "${OPENCONNECT_PROTOCOL}" == "gp" ]; then
csd_script="${OPENCONNECT_HIP_REPORT_POST_SCRIPT}"
csd_source="${HIP_REPORT_POST_URL}"
fi
if [ -n "${csd_script}" ]; then
if [ ! -f "${csd_script}" ]; then
echo "WARN: ${csd_script} not found, attempting to download..."
if ! curl -kLs --fail -o ${csd_script} "${csd_source}"; then
logcrit "Unable to download the script from:\n\n\t\t${csd_source}\n\n\tCheck your internet connection and verify the resource is still available."
exit 1
elif [ ! -f "${csd_script}" ]; then
logcrit "Unable to write to ${csd_script}"
exit 1
fi
chmod +x ${csd_script}
fi
openconnect_opts+="--csd-wrapper=${csd_script} "
fi
openconnect_opts+="--os=win "
openconnect_opts+=" ${OPENCONNECT_HOST}"
echo "${openconnect_opts}"
}
status() {
is_running "${OPENCONNECT_PID}" && loginfo "VPN is up." || loginfo "VPN is down."
is_running "${SOCAT_PID}" && loginfo "DNS proxy is running." || loginfo "DNS proxy is not running."
}
start_dns_proxy() {
local retval=0
requires_admin
if is_running "${OPENCONNECT_PID}"; then
type -P socat &?>/dev/null || { logwarn "Command not in path: socat. Unable to start DNS proxy."; return 1; }
loginfo "Getting DNS bind IP from device: ${SOCAT_DEVICE}."
BIND_ADDRESS=$( ip -f inet addr show ${SOCAT_DEVICE} | sed -En -e 's/.*inet ([0-9.]+).*/\1/p' )
[ -z "${BIND_ADDRESS}" ] && { logcrit "Could not determine bind IP from ${SOCAT_DEVICE}, unable to configure DNS recevier."; return 1; }
NAMESERVER=$( cat /etc/resolv.conf | grep nameserver | head -1 | awk 'NF{ print $NF }' )
loginfo "Setting up host only DNS proxy listener..."
socat -T10 UDP4-LISTEN:53,fork,bind=${BIND_ADDRESS},range=192.168.56.1/32 UDP4:${NAMESERVER}:53 &>/dev/null &
echo $! > "${SOCAT_PID}"
if is_running "${SOCAT_PID}"; then
if [ "${MACHINE_TYPE}" == "vm" ]; then
loginfo "sshuttle can be re-enabled on forwarding host."
else
loginfo "VPN connection not running in a VM, ensure routing works on forwarding host to second network device with local-network IP."
fi
else
logcrit "DNS proxy did not start, check logs."
retval=1
fi
else
logcrit "VPN connection is not up, there's no VPN connection for forwarding DNS."
retval=1
fi
return 0
}
start_vpn() {
local retval=0
requires_admin
if is_running "${OPENCONNECT_PID}" && (( DNS_PROXY == 0 )); then
loginfo "VPN already running, nothing to do."
elif is_running "${OPENCONNECT_PID}"; then
loginfo "VPN already running."
else
logdebug "$(openconnect_start_opts)"
loginfo "** If VPN is using MFA, enter password for the account for 'MFA Token:' prompt."
loginfo "** If password is correct, use the MFA token for the 'Challenge:' prompt."
openconnect $(openconnect_start_opts)
sleep 2
fi
if is_running "${OPENCONNECT_PID}"; then
if (( DNS_PROXY )); then
start_dns_proxy || { logcrit "DNS proxy did not start correctly, check logs."; retval=1; }
fi
loginfo "VPN is up and running."
else
logcrit "VPN not started, check logs."
retval=1
fi
return $(( retval ))
}
stop_dns_proxy() {
local retval=0
requires_admin
if is_running "${SOCAT_PID}"; then
loginfo "Shutting down DNS proxy."
kill -SIGINT $(<"${SOCAT_PID}")
sleep 2
if is_running "${SOCAT_PID}"; then
logwarn "DNS proxy not stopped, attempting to force stop..."
kill -SIGKILL $(<"${SOCAT_PID}")
sleep 2
fi
if is_running "${SOCAT_PID}"; then
logcrit "DNS proxy not stopped"
retval=1
else
rm -f "${SOCAT_PID}"
loginfo "DNS proxy stopped, disable sshuttle routing on host system."
fi
fi
return $(( retval ))
}
stop_vpn() {
local retval=0
requires_admin
if is_running "${OPENCONNECT_PID}"; then
loginfo "Shutting VPN down."
kill -SIGINT $(<"${OPENCONNECT_PID}")
sleep 2
if is_running "${OPENCONNECT_PID}"; then
logwarn "VPN not stopped, attempting to force stop..."
kill -SIGKILL $(<"${OPENCONNECT_PID}")
sleep 2
fi
if is_running "${OPENCONNECT_PID}"; then
logcrit "VPN not stopped"
retval=1
else
loginfo "Resetting VPN routes."
ip r | grep ppp0 && ip r | grep default | head -n1 | xargs sudo ip r del
loginfo "VPN is stopped and routes are reset."
fi
else
loginfo "VPN not running"
fi
return $(( retval ))
}
################################################################################
# #
# M A I N P R O G R A M #
# #
################################################################################
ACTION="usage"
DNS_PROXY=0
DEBUG=0
init_opts() {
local short_opts="urdshtpg:a:n:"
local long_opts="up,restart,down,status,help,troubleshoot,proxy,globalprotect:,anyconnect:,name:"
local tmp;
local instance_config_set=0
OPTIONS=$( getopt -o "$short_opts" --long "$long_opts" -n "$_CTL" -- "$@" )
if (( $? )); then
logcrit "Failed parsing options ${OPTIONS}."
exit 1
fi
eval set -- "$OPTIONS"
# extract options and their arguments into variables.
while true ; do
case "$1" in
-u|--up)
ACTION="start"
logdebug "Action: ${ACTION}"
shift
;;
-r|--restart)
ACTION="restart"
is_running "${SOCAT_PID}" && DNS_PROXY=1
logdebug "Action: ${ACTION}"
shift
;;
-d|--down)
ACTION="stop"
is_running "${SOCAT_PID}" && DNS_PROXY=1
logdebug "Action: ${ACTION}"
shift
;;
-s|--status)
ACTION="status"
logdebug "Action: ${ACTION}"
shift
;;
-g|--globalprotect)
OPENCONNECT_PROTOCOL="gp"
OPENCONNECT_HOST="$2"
logdebug "Host: ${OPENCONNECT_HOST}"
logdebug "Host: ${OPENCONNECT_PROTOCOL}"
shift 2
;;
-a|--anyconnect)
OPENCONNECT_PROTOCOL="anyconnect"
OPENCONNECT_HOST="$2"
logdebug "Host: ${OPENCONNECT_HOST}"
logdebug "Host: ${OPENCONNECT_PROTOCOL}"
shift 2
;;
-n|--name)
OPENCONNECT_USER="$2"
logdebug "User: ${OPENCONNECT_USER}"
shift 2
;;
-p|--proxy)
DNS_PROXY=1
[ "${MACHINE_TYPE}" != "vm" ] || logwarn "Setting up a DNS proxy listener works best on a VM and provides little value on a host system."
shift
;;
-t|--troubleshoot)
DEBUG=1
shift
;;
-h|--help)
ACTION="help"
shift
;;
-- ) shift; break ;;
esac
done
}
###################################################################
## ##
## M A I N ##
## ##
###################################################################
init_opts $@
CTL_RET=0
case "$ACTION" in
start)
[ -z "${OPENCONNECT_PROTOCOL}" ] && { logcrit "Missing VPN protocol (-g or -a)."; CTL_RET=1; }
[ -z "${OPENCONNECT_USER}" ] && { logcrit "Missing user name."; CTL_RET=1; }
if (( CTL_RET == 0 )); then
start_vpn || CTL_RET=1
if (( DNS_PROXY )); then
start_dns_proxy || CTL_RET=3
fi
fi
;;
stop)
(( DNS_PROXY )) && { stop_dns_proxy || logwarn "DNS proxy didn't stop, clean up manually."; }
stop_vpn || CTL_RET=2
;;
restart)
[ -z "${OPENCONNECT_PROTOCOL}" ] && { logcrit "Missing VPN protocol (-g or -a)."; CTL_RET=1; }
[ -z "${OPENCONNECT_USER}" ] && { logcrit "Missing user name."; CTL_RET=1; }
if (( CTL_RET == 0 )); then
if (( DNS_PROXY )); then
stop_dns_proxy || { logwarn "DNS proxy didn't stop, will not attempt to restart proxy."; DNS_PROXY=0; CTL_RET=3; }
fi
stop_vpn || CTL_RET=2
start_vpn || CTL_RET=1
if (( DNS_PROXY )); then
start_dns_proxy || CTL_RET=3
fi
fi
;;
status)
status
;;
help|usage)
display_${ACTION}
;;
*)
display_usage
CTL_RET=1
;;
esac
exit $(( CTL_RET ))
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment