Last active
November 5, 2020 15:00
-
-
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.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/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