Skip to content

Instantly share code, notes, and snippets.

@johnhpatton
Created July 13, 2023 19:24
Show Gist options
  • Save johnhpatton/a0864bfc6f95a18875b545173e4f7d61 to your computer and use it in GitHub Desktop.
Save johnhpatton/a0864bfc6f95a18875b545173e4f7d61 to your computer and use it in GitHub Desktop.
#!/bin/bash
# May be required to avoid SSL certificate validation.
# NOTE: manually validate the CA in this case.
export INSECURE=${INSECURE:-true}
# Set to user id for access:
OPENCONNECT_USER="USERNAME"
# Set to server without scheme, use what's configured in client config:
OPENCONNECT_HOST="HOSTNAME"
# Set to VPN group, if needed -- may need to dig through the client
# configuration to get this.
OPENCONNECT_GROUP="GROUP"
# Set to protocol, script works with gp and anyconnect:
OPENCONNECT_PROTOCOL="anyconnect"
# 1. Update the OPENCONNECT_USER, OPENCONNECT_HOST, and OPENCONNECT_PROTOCOL
# variables above.
#
# 2. Place script under: /usr/local/sbin/openconnect.ctl
#
# 3. Set the following aliases:
#
# alias vpnconnect='sudo /usr/local/sbin/openconnect.ctl -a start vpn'
# alias vpndisconnect='sudo /usr/local/sbin/openconnect.ctl -a stop vpn'
#
# 4. Get the group for your user and add the following to /etc/sudoers.d/vpn:
#
# Cmnd_Alias OPENCONNECT_CMDS = /usr/local/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.
#
# 5. Logout/back in, run `vpnconnect` to connect.
#
# 6. VPN gateway not compliant with "secure" cyphers, lower cypher strength
# and enable unsafe renogotiation if experiencing issues. This is not a
# configuration that can be corrected on the client side. This configuration
# goes in the /etc/ssl/openssl.cnf file.
#
# In the [openssl_init] section, adjust the ssl_conf setting with the
# following setting if needed:
#
# ssl_conf = ssl_sect
#
# Add the following to the end of the file:
#
# [ssl_sect]
# system_default = system_default_sect
#
# [system_default_sect]
# Options = UnsafeLegacyRenegotiation
# CipherString = DEFAULT:@SECLEVEL=1
# MinProtocol = TLSv1.2
#
# Retry vpn connection.
# 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 ))
SCRIPT_EXIT_CODE=0
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
if (( $EUID != 0 )); then
echo "ERROR: Run as root or with sudo."
exit 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
function DATETIME() {
date "+%Y%m%d-%T"
}
# trim()
# Trims white space on passed in string
function 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
function logdebug() { (( DEBUG )) && logmsg "${WHITE}DEBUG: ${NC}$1${NC}"; }
function loginfo() { logmsg "${BICYAN} INFO: ${NC}$1${NC}"; }
function logok() { logmsg "${BIGREEN} OK: ${NC}$1${NC}"; }
function logwarn() { logmsg "${BIYELLOW} WARN: ${NC}$1${NC}"; }
function logcrit() { logmsg "${BIRED} CRIT: ${NC}$1${NC}"; }
function logmsg() { [[ ${FUNCNAME[1]} =~ log.* ]] && echo -e "$1" || loginfo "$1"; }
function stat() {
local pidfile="$1"
local retval=0
if [ ! -f "${pidfile}" ] || ! kill -0 $(cat ${pidfile}) &>/dev/null; then
[ -f "${pidfile}" ] && rm -f "${pidfile}"
retval=1
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
function display_usage() {
cat <<USAGE_MESSAGE
openconnect.ctl options:
$_CTL -a {ACTION} [-p] [-h]
For full usage help, use: $_CTL -a help
USAGE_MESSAGE
}
# display_help
# Displays a simple usage message
function display_help() {
cat <<HELP_MESSAGE
openconnect.ctl options:
$_CTL -a {ACTION} [-p] [-h]
-a ACTION options:
start - starts the VPN
stop - shuts the VPN down
restart - stops/starts the VPN
reconnect - reinititializes the VPN connection
status - prints the status of the VPN
help - this message
-p: Enable a forward proxy listener for host routing
over the VPN connection
NOTE: optional
-h: prints basic usage message
HELP_MESSAGE
}
function openconnect_start_opts() {
local openconnect_opts="--background "
local csd_script=""
local csd_source=""
(( DEBUG )) && openconnect_opts+="--verbose "
openconnect_opts+="--protocol=${OPENCONNECT_PROTOCOL} "
openconnect_opts+="--user=${OPENCONNECT_USER} "
openconnect_opts+="--pid-file=${OPENCONNECT_PID} "
[ -n "${OPENCONNECT_GROUP}" ] && openconnect_opts+="--authgroup ${OPENCONNECT_GROUP} "
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
(( DEBUG )) && openconnect_opts+="--printcookie "
(( DEBUG )) && openconnect_opts+="--dump-http-traffic "
openconnect_opts+="--os=win "
openconnect_opts+="${OPENCONNECT_HOST}"
echo "${openconnect_opts}" > /tmp/opts.log
echo "${openconnect_opts}"
}
function status() {
local vpn="$1"
local dns_proxy="$2"
if (( vpn )); then
stat "${OPENCONNECT_PID}" && { loginfo "VPN is running."; SCRIPT_EXIT_CODE=1; } || loginfo "VPN is stopped."
fi
if (( dns_proxy )); then
stat "${SOCAT_PID}" && { loginfo "DNS proxy is running."; SCRIPT_EXIT_CODE=1; } || loginfo "DNS proxy is stopped."
fi
}
function start_dns_proxy() {
type -P socat &?>/dev/null || { logwarn "Command not in path: socat. Unable to start DNS forwarding receiver."; 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 forwarding"; 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}"
loginfo "sshuttle can be re-enabled on host."
}
function start_vpn() {
local retval=0
stat "${OPENCONNECT_PID}" && { loginfo "VPN already running."; return 0; }
echo "$(openconnect_start_opts)"
openconnect $(openconnect_start_opts)
sleep 1
if stat "${OPENCONNECT_PID}"; then
if (( DNS_PROXY )); then
start_socat || 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))
}
function stop_dns_proxy() {
if stat "${SOCAT_PID}"; then
loginfo "Shutting down DNS proxy."
kill $(cat ${SOCAT_PID})
rm -f "${SOCAT_PID}"
loginfo "DNS proxy stopped, disable sshuttle routing on host."
fi
}
function stop_vpn() {
if stat "${OPENCONNECT_PID}"; then
loginfo "Shutting down VPN down."
kill -SIGINT $(cat "${OPENCONNECT_PID}")
loginfo "Removing VPN routes."
ip r | grep ppp0 && ip r | grep default | head -n1 | xargs sudo ip r del
sleep 1
loginfo "VPN is stopped and routes are reset."
else
loginfo "VPN not running"
fi
}
function requires_admin() {
if (( $EUID != 0 )); then
echo "ERROR: Run as root or with sudo."
exit 1
fi
}
ACTION="usage"
DNS_PROXY=0
VPN=0
DEBUG=0
function init_opts() {
local short_opts="a:dh"
local long_opts="action:,help"
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
-a|--action)
case "$2" in
"") shift 2 ;;
start|stop|restart|reconnect|status|help)
ACTION=$2
shift 2
;;
-- ) break ;;
*) shift 2 ;;
esac
;;
-d|--debug)
DEBUG=1
;;
-h|--help)
ACTION=usage
;;
-- ) shift; break ;;
esac
done
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
for svc in "$@"; do
case "$svc" in
vpn)
VPN=1
shift; break ;;
dnsproxy)
[ "${MACHINE_TYPE}" != "vm" ] && DNS_PROXY=1 || logwarn "unable to set up proxy, this only works for VMs."
shift; break ;;
*)
logcrit "Invalid service: $svc"
ACTION="usage"
shift; break ;;
esac
done
}
###################################################################
## ##
## M A I N ##
## ##
###################################################################
function init_opts $@
case "$ACTION" in
start)
if (( VPN )) || (( DNS_PROXY )); then
(( VPN )) && { start_vpn || exit 27; }
(( DNS_PROXY )) && { start_dns_proxy || exit 96; }
else
display_usage
fi
;;
stop)
if (( VPN )) || (( DNS_PROXY )); then
(( VPN )) && { stop_vpn || exit 27; }
(( DNS_PROXY )) && { stop_dns_proxy || exit 96; }
else
display_usage
fi
;;
restart)
if (( VPN )) || (( DNS_PROXY )); then
(( DNS_PROXY )) && stop_dns || { logwarn "DNS proxy didn't stop, will not attempt to restart"; DNS_PROXY=0; }
(( VPN )) && { stop_vpn || exit 27; } && { start_vpn || exit 27; }
(( DNS_PROXY )) && { start_dns_proxy || exit 96; }
else
display_usage
fi
;;
status)
status ${VPN} ${DNS_PROXY}
;;
help|usage)
display_${ACTION}
;;
*)
display_usage
exit 1
;;
esac
exit ${SCRIPT_EXIT_CODE}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment