#!/bin/bash
|
|
# ---------------------------------------------------------------------------
|
|
# rssh - route ssh through a series of hosts
|
|
|
|
# This program is free software: you can redistribute it and/or modify
|
|
# it under the terms of the GNU General Public License as published by
|
|
# the Free Software Foundation, version 3.
|
|
|
|
# This program is distributed in the hope that it will be useful,
|
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
# GNU General Public License at <http://www.gnu.org/licenses/> for
|
|
# more details.
|
|
|
|
# Revision history:
|
|
# 2015-11-11 Created (v0.1)
|
|
# 2016-01-16 Modified license and uploaded to git for someone else to use. (v0.2)
|
|
# 2016-04-21 Incorporated sshrc copies your bashrc env to remote server (v0.3)
|
|
# 2016-05-20 updated sshrc and enabled direct use of ssh config if single hop (v0.4)
|
|
# 2016-06-29 Updated to allow user/ port on command line plus run commands (v0.5)
|
|
# 2016-06-20 Tidy code, and check with shellcheck (v0.6)
|
|
# 2017-01-09 Tidy code, and check with latest shellcheck (v.07)
|
|
# 2017-04-26 Allow proxycommand in config (0.8)
|
|
# 2018-08-28 Adding -F parameter to define the ssh_config file (0.9)
|
|
# 2018-09-19 Allow for Include statements in ssh config (1.0)
|
|
# ---------------------------------------------------------------------------
|
|
|
|
PROGNAME=${0##*/}
|
|
VERSION="1.0"
|
|
|
|
# define variables
|
|
hops=0
|
|
declare -a host
|
|
declare -a hostdata
|
|
declare -a hostname
|
|
declare -a port
|
|
declare -a user
|
|
declare -a options
|
|
SSH_CF="${HOME}/.ssh/config"
|
|
conffile=$(mktemp)
|
|
ignore_default_route=0
|
|
ignore_proxies=0
|
|
host_list=""
|
|
use_ssh=0
|
|
_USE_DEBUG=0
|
|
opt=""
|
|
Full_SSH_CF=$(mktemp)
|
|
|
|
while read -r line; do
|
|
if [[ "$line" = Include* ]] ; then
|
|
incfile=$(echo "$line" | awk '{print $2}')
|
|
cat "$incfile" >> "$Full_SSH_CF"
|
|
else
|
|
echo "$line" >> "$Full_SSH_CF"
|
|
fi
|
|
done < "$SSH_CF"
|
|
|
|
clean_up() { # Perform pre-exit housekeeping
|
|
debug ""
|
|
debug "removing $conffile"
|
|
debug ""
|
|
rm -f "$conffile"
|
|
rm -f "$Full_SSH_CF"
|
|
return
|
|
}
|
|
|
|
debug() { # write out debug info if the debug flag has been set
|
|
if [ ${_USE_DEBUG} -eq 1 ]; then
|
|
echo "$@"
|
|
fi
|
|
}
|
|
|
|
add_hop() {
|
|
((hops++));
|
|
l_host=$1
|
|
l_user=""
|
|
l_port=""
|
|
if [[ $l_host == *"@"* ]]; then
|
|
l_user=$(echo "$l_host" | awk -F@ '{print $1}')
|
|
l_host=$(echo "$l_host" | awk -F@ '{print $2}')
|
|
fi
|
|
if [[ $l_host == *":"* ]]; then
|
|
l_port=$(echo "$l_host" | awk -F: '{print $2}')
|
|
l_host=$(echo "$l_host" | awk -F: '{print $1}')
|
|
fi
|
|
host[${hops}]=$l_host;
|
|
hostdata[${hops}]=$(sed -n "/[Hh]ost.* ${l_host}\( \|$\)/,/^[ ]*$/p" "${Full_SSH_CF}")
|
|
hostname[${hops}]=$(echo "${hostdata[${hops}]}" | grep -i "Hostname" | awk '{print $2}' )
|
|
hostname[${hops}]=${hostname[${hops}]:=${l_host}}
|
|
f_port=$(echo "${hostdata[${hops}]}" | grep -i "^[ ]*port" | awk '{print $2}' )
|
|
port[${hops}]=${l_port:=$f_port}
|
|
port[${hops}]=${port[${hops}]:=22}
|
|
user[${hops}]=$l_user
|
|
options[${hops}]=$(echo "${hostdata[${hops}]}" | \
|
|
grep -iv "^[ ]*host " | \
|
|
grep -iv "^[ ]*hostname " | \
|
|
grep -iv "^[ ]*port " | \
|
|
grep -iv "^[ ]*#"| \
|
|
grep -iv "^[ ]*ProxyCommand");
|
|
debug "added ${hostname[${hops}]} with options ${options[${hops}]}"
|
|
}
|
|
|
|
error_exit() {
|
|
echo -e "${PROGNAME}: ${1:-"Unknown Error"}" >&2
|
|
clean_up
|
|
exit 1
|
|
}
|
|
|
|
graceful_exit() {
|
|
clean_up
|
|
exit
|
|
}
|
|
|
|
help_message() {
|
|
cat <<- _EOF_
|
|
$PROGNAME ver. $VERSION
|
|
route ssh through a series of hosts
|
|
|
|
$(usage)
|
|
|
|
Options:
|
|
-h, --help Display this help message and exit.
|
|
-v verbose output from ssh
|
|
-f ssh_config file
|
|
-id ignore default routing
|
|
-ip ignore proxies defined on config
|
|
-n use plain ssh, with no copying of rc files
|
|
-d debug
|
|
-s nnnn socks port
|
|
-c command command to run on remote server
|
|
|
|
note: This script assumes that any hosts in your ssh_config file have a non-indented Host
|
|
line and the rest of the items related to that host are indented.
|
|
|
|
The default routing is as defined by the variable DEFAULT_SSH_ROUTE
|
|
|
|
_EOF_
|
|
return
|
|
}
|
|
|
|
signal_exit() { # Handle trapped signals
|
|
case $1 in
|
|
INT)
|
|
error_exit "Program interrupted by user" ;;
|
|
TERM)
|
|
echo -e "\n$PROGNAME: Program terminated" >&2
|
|
graceful_exit ;;
|
|
*)
|
|
error_exit "$PROGNAME: Terminating on unknown signal" ;;
|
|
esac
|
|
}
|
|
|
|
function sshrc() {
|
|
local SSHHOME=${SSHHOME:=~}
|
|
if [ ! -f "$SSHHOME/.sshrc" ]; then
|
|
touch "$SSHHOME/.sshrc"
|
|
fi
|
|
if [ ! -z "$commandline" ]; then
|
|
export SSHHOMETMP
|
|
SSHHOMETMP=$(mktemp -d -t ".$(whoami).sshhome.XXXXXX")
|
|
trap 'rm -rf $SSHHOMETMP; exit' 0
|
|
cat "$SSHHOME/.sshrc" > "$SSHHOMETMP/.sshrc"
|
|
echo "$commandline && exit || exit" >> "$SSHHOMETMP/.sshrc"
|
|
ln -s "$SSHHOME/.sshrc.d" "$SSHHOMETMP/.sshrc.d"
|
|
export SSHHOME=$SSHHOMETMP
|
|
WELCOME_MESSAGE=""
|
|
SSHRC_QUIET="-o LogLevel=QUIET"
|
|
SSHRC_ACTIVATE_BIN=""
|
|
else
|
|
WELCOME_MESSAGE="
|
|
if [ -e /etc/motd ]; then cat /etc/motd; fi
|
|
if [ -e /etc/update-motd.d ]; then run-parts /etc/update-motd.d/ 2>/dev/null; fi
|
|
"
|
|
SSHRC_QUIET=""
|
|
SSHRC_ACTIVATE_BIN="chmod +x \$SSHHOME/sshrc"
|
|
fi
|
|
local files=.sshrc
|
|
if [ -d "$SSHHOME/.sshrc.d" ]; then
|
|
files="$files .sshrc.d"
|
|
fi
|
|
# shellcheck disable=SC2086
|
|
SIZE=$(tar cz -h -C "$SSHHOME" $files | wc -c)
|
|
if [ "$SIZE" -gt 65536 ]; then
|
|
echo >&2 $'.sshrc.d and .sshrc files must be less than 64kb\ncurrent size: '"$SIZE"' bytes'
|
|
exit 1
|
|
fi
|
|
# shellcheck disable=SC2029,SC2002,SC2086
|
|
ssh $SSHRC_QUIET -t "$@" "
|
|
command -v openssl >/dev/null 2>&1 || { echo >&2 \"sshrc requires openssl to be installed on the server, but it's not. Aborting.\"; exit 1; }
|
|
$WELCOME_MESSAGE
|
|
export SSHHOME=\$(mktemp -d -t .$(whoami).sshrc.XXXXXX)
|
|
export SSHRCCLEANUP=\$SSHHOME
|
|
trap \"rm -rf \$SSHRCCLEANUP; exit\" 0
|
|
echo \"$(cat "$0" | openssl enc -base64)\" | openssl enc -base64 -d > \$SSHHOME/sshrc
|
|
$SSHRC_ACTIVATE_BIN
|
|
echo \"$( cat <<- 'EOF' | openssl enc -base64
|
|
if [ -r /etc/profile ]; then source /etc/profile; fi
|
|
if [ -r ~/.bash_profile ]; then source ~/.bash_profile
|
|
elif [ -r ~/.bash_login ]; then source ~/.bash_login
|
|
elif [ -r ~/.profile ]; then source ~/.profile
|
|
fi
|
|
export PATH=$PATH:${SSHHOME}:${SSHHOME}/.sshrc.d
|
|
source $SSHHOME/.sshrc;
|
|
EOF
|
|
)\" | openssl enc -base64 -d > \$SSHHOME/sshrc.bashrc
|
|
echo \"$(tar cz -h -C $SSHHOME $files | openssl enc -base64)\" | tr -s ' ' $'\n' | openssl enc -base64 -d | tar mxz -C \$SSHHOME
|
|
export SSHHOME=\$SSHHOME
|
|
bash --rcfile \$SSHHOME/sshrc.bashrc
|
|
"
|
|
}
|
|
|
|
usage() {
|
|
echo -e "Usage: $PROGNAME [-h|--help] [-v] [-F file] [-id] [-n] [-s socks_port] sever1 server2 [server3] [server4 ....etc] [-c command]"
|
|
}
|
|
|
|
|
|
# Trap signals
|
|
trap "signal_exit TERM" TERM HUP
|
|
trap "signal_exit INT" INT
|
|
|
|
|
|
# Parse command-line
|
|
while [[ -n $1 ]]; do
|
|
case $1 in
|
|
-h | --help)
|
|
help_message; graceful_exit ;;
|
|
-v)
|
|
opt="-v" ;;
|
|
-c | --command)
|
|
shift;commandline=$1 ;;
|
|
-d | --debug)
|
|
_USE_DEBUG=1 ;;
|
|
-F | --file)
|
|
shift;SSH_CF=$1 ;;
|
|
-id | --ignore-default)
|
|
ignore_default_route=1 ;;
|
|
-ip | --ignore-proxies)
|
|
ignore_proxies=1 ;;
|
|
-n | --no-rc)
|
|
use_ssh=1 ;;
|
|
-s | --socks)
|
|
shift;socks=$1 ;;
|
|
-* | --*)
|
|
usage
|
|
error_exit "Unknown option $1" ;;
|
|
*)
|
|
host_list="$host_list $1" ;;
|
|
esac
|
|
shift
|
|
done
|
|
|
|
if [ -z "$host_list" ]; then
|
|
help_message
|
|
graceful_exit
|
|
fi
|
|
|
|
# Main logic
|
|
|
|
if [ ! -z "$DEFAULT_SSH_ROUTE" ] && [ "$ignore_default_route" -eq "0" ]; then
|
|
first_default=$(echo "${DEFAULT_SSH_ROUTE}" | awk '{print $1}')
|
|
first_hop=$(echo "${host_list}" | awk '{print $1}')
|
|
ignore_dr=$(sed -n "/[Hh]ost.* ${first_hop}\( \|$\)/,/^[ ]*$/p" "${Full_SSH_CF}" | grep -c "Ignore_DEFAULT_SSH_ROUTE")
|
|
if [[ "$first_default" == "$first_hop" ]]; then
|
|
debug "ignoring default route as first hop is default route"
|
|
elif [[ "$ignore_dr" -gt 0 ]]; then
|
|
debug "ignoring $ignore_dr default route as first hop as comment Ignore_DEFAULT_SSH_ROUTE"
|
|
else
|
|
debug "unsing default route - $DEFAULT_SSH_ROUTE"
|
|
# loop in case there is more than one hop in the default route
|
|
for h in ${DEFAULT_SSH_ROUTE}; do
|
|
debug "adding default hop $h"
|
|
add_hop "$h"
|
|
done
|
|
fi
|
|
fi
|
|
|
|
# if proxies are not ignored, check all hosts and add the proxy hops.
|
|
for h in ${host_list}; do
|
|
if [[ "$ignore_proxies" -eq 0 ]]; then
|
|
hdata=$(sed -n "/[Hh]ost.* ${h}\( \|$\)/,/^[ ]*$/p" "${Full_SSH_CF}")
|
|
h_proxy=$(echo "${hdata}" | grep -i "^[ ]*ProxyCommand" | awk '{print $3}')
|
|
if [[ "$h_proxy" == "-q" ]]; then
|
|
h_proxy=$(echo "${hdata}" | grep -i "^[ ]*ProxyCommand" | awk '{print $4}')
|
|
fi
|
|
if [[ ! -z "$h_proxy" ]]; then
|
|
debug "adding proxy - $h_proxy"
|
|
add_hop "$h_proxy"
|
|
fi
|
|
fi
|
|
debug "adding hop $h"
|
|
add_hop "$h"
|
|
done
|
|
|
|
{
|
|
i=${hops}
|
|
while [ $i -gt 1 ]; do
|
|
echo "Host ${host[${i}]}"
|
|
echo " Hostname ${hostname[${i}]}"
|
|
echo " Port ${port[${i}]}"
|
|
if [ ! -z "${user[$i]}" ] ; then
|
|
echo " User ${user[$i]}"
|
|
fi
|
|
if [ ! -z "${options[$i]}" ] ; then
|
|
echo "${options[${i}]}"
|
|
fi
|
|
if [ ! -z "$socks" ] && [ $i -eq ${hops} ] ; then
|
|
echo " DynamicForward localhost:${socks}"
|
|
fi
|
|
echo " ProxyCommand ssh -q -F ${conffile} ${opt} ${host[${i}-1]} -W ${hostname[${i}]}:${port[${i}]}"
|
|
echo " "
|
|
let i=i-1
|
|
done
|
|
echo "Host ${host[${i}]}"
|
|
echo " Hostname ${hostname[${i}]}"
|
|
echo " Port ${port[${i}]}"
|
|
if [ ! -z "${user[$i]}" ] ; then
|
|
echo " User ${user[$i]}"
|
|
fi
|
|
echo "${options[${i}]}"
|
|
echo " "
|
|
sed -n "/^Host \*\( \|$\)/,/^$/p" "${Full_SSH_CF}" | grep -v ProxyCommand
|
|
} >> "$conffile"
|
|
|
|
if [ ${_USE_DEBUG} -eq 1 ]; then
|
|
debug ""
|
|
debug "config file which will be used is at ${conffile} with contents:"
|
|
debug ""
|
|
cat "$conffile"
|
|
debug ""
|
|
for (( i=1; i<=hops; i++ )); do
|
|
debug "hop $i is ${host[${i}]}"
|
|
done
|
|
fi
|
|
|
|
debug "command: sshrc ${opt} -F $conffile ${host[${hops}]}"
|
|
|
|
if [ "$use_ssh" -eq 1 ]; then
|
|
# shellcheck disable=SC2029
|
|
ssh ${opt} -F "$conffile" "${host[${hops}]}"
|
|
else
|
|
sshrc ${opt} -F "$conffile" "${host[${hops}]}"
|
|
fi
|
|
|
|
graceful_exit
|