Saturday, February 9, 2013

Reverse SSH tunnel manager (remote ssh forwardings)

Port forwarding over ssh is a great feature of ssh (a.k.a ssh tunnels). It provide you extra layer of security and accessibility. I happen to use this feature a lot especially the remote ssh port forwarding (a.k.a reverse ssh tunnels). I administer many Linux based systems that don't have any kind of direct access from internet and reverse ssh tunnels come in real handy for accessing these systems remotely. The idea is that I forward the ssh port from these systems to a random remote port on my Linux server (that has a live static IP). So whenever I need to access these systems, I login to my Linux server and ssh to these forwarded ssh ports. (If you don't know how ssh tunnels or port forwarding work have a look at this great article explaining the topic in great detail)

I have written a bash script to manage these remote tunnels. Using this script you can easily forward as many ports as you want to a remote server. It will handle the ssh based reverse tunnel sessions and try its best to retain them. It is easily deployable and configurable. Here is how to use this script:

1- Download/copy/paste the script on the source system (i.e the system from which you want to create the tunnel to your server) and make it executable.
wget -O /opt/rssht_1 http://kxr.me/scripts/rssht
chmod +x /opt/rssht_1
Note: You can rename this script to whatever you want

2- Edit the script and set the host/port configuration variables:
REMOTE_HOST=live.host.com   # The remote host to which you want to forward the ports
REMOTE_SSH_PORT=22          # SSH port of the remote host
REMOTE_USER=root            # SSH user on the remote host
Note: There are other advance configuration options as well. Check the script for more details.

3- Set the ports to forward via the tunnel. This is the list of ports forwarding, and the format is exactly what you would use in ssh command after the -R switch. This variable is an array and you can have as many values (forwardings) as you want. Suppose you want port 22 (ssh) and port 80 (http) on this system to be forwarded to port 10122 and 10180 respectively on the remote server, then this variable will look something like this:
REMOTE_FWDS=(   
10122:localhost:22
10180:localhost:80
)

4- Setup password less login from this machine to the remote server. Yes, this script expects a password-less ssh connection to the remote host (if you want to enter password every time, you don't need this script, run the ssh command manually).

Note: Running the script with --help or install as first argument will give you quick instructions (based on the configuration variables you set) on how to setup the password-less ssh login to the remote server and add the script to cron job when required.
[root@host ~]# /opt/rssht_1 install
INSTALLATION INSTRUCTIONS:

# Set the configuration variables and forwardings

# Make sure you have ssh keys generated
ssh-keygen

# Setup password-less login to the remote host
ssh-copy-id 'live.host.com -l root -p 22'

# Add the cron job
echo '*/5 * * * * root /opt/rssht_1' /etc/cron.d/rssht_live.host.com

5- Run the script. Once the password less login is setup, simply run the script. It should give you a success message with the pid of the ssh session:
[root@host ~]# /opt/rssht_1
RSSHT to host root@live.host.com:22  started successfully ssh pid: 12779
Once the reverse tunnel is started, running the script again should check the status of the running session:
[root@host ~]# /opt/rssht_1
ssh connection is fine, exiting
[root@host ~]# /opt/rssht_1
ssh connection is fine, exiting
You can pass the stop argument if you want to kill the session and remove the tunnel:
[root@hplt ~]# /opt/rssht_1 stop
Stop argument passed, killing ..
Alternatively, if you want the tunnel to be permanent, you can add the cron entry for this script. Running the script with the "install" argument will give you a quick command to enable cron job to run this script every 5 minutes:
echo '*/5 * * * * root /opt/rssht_1' > /etc/cron.d/rssht_live.host.com

QUICK TIP: When using the script in persistent/cron mode, there is a configuration variable in the script called REFRESH_SOCKET. You can set it to a non-zero positive number and the script will reset (exit and reconnect) the ssh socket connection if its that many minutes old. For example, if this variable is set to 60, the script will reset the connection ever hour. This could be very useful if the connection is unstable and gets stuck a lot. To disable this feature set the variable to 0.

Note: One script file has the ability to create tunnel to only one remote server. If you want to create tunnels to more than one server, you can have multiple copies of this script each with its own configuration ( and with different file names e.g: /opt/rssht_2).

Here is the complete script code:
#!/bin/bash
# Script: rssht (Reverse SSH Tunnel)
# Author: Khizer Naeem (khizernaeem(x)gmail.com)
# Created : 09 Sep 2012
# Version: 1.03
# Latest Revision: 08 Feb 2013
# Tested on: Centos/RHEL 6, Centos/RHEL 5,  
# Description: A bash script to maintain reverse ssh tunnel with remote port forwardings.
# URL: http://kxr.me/scripts/rssht_v1.03
#
# Copyright (C) 2013 Khizer Naeem All rights reserved.
# This copyrighted material is made available to anyone wishing to use,
# modify, copy, or redistribute it subject to the terms and conditions
# of the GNU General Public License v.2.
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software Foundation,
# Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA

#############################
## CONFIGURATION VARIABLES ##
#############################

REMOTE_HOST=live.host.com    # The remote host to which you want to forward the ports
REMOTE_SSH_PORT=22    # SSH port of the remote host
REMOTE_USER=root    # SSH user for the remote host (Should have password less login from this machine with this user)
SSH_RUN_DIR=/var/run/rssht/  # Directory to keep ssh socket connection and pid file (Should exist)
PORTS_REMOTELY_ACCESSIBLE=yes  # If set to yes, -g switch will be used while making the ssh connection. Read ssh manual for detail of -g
SSH_BIN=/usr/bin/ssh    # Location of the ssh client on this host
REFRESH_SOCKET=0     # If set to a non-zero integer, this script will restart the ssh session if its that many minutes old.

###########################
## PORT FORWARDING TABLE ##
###########################
REMOTE_FWDS=( 
 10122:localhost:22
 10180:localhost:80
)

#######################
## SSH OPTIONS TABLE ##
#######################
# These options will be passed to ssh with the -o switch e.g -o "ControlMaster yes"
# You can comment this out if you want the script to obey the ssh config
SSH_OPTS=(  
 'ControlMaster yes'
 'PreferredAuthentications publickey'
 'Ciphers arcfour256'
 'Compression yes'
 'TCPKeepAlive yes'
 'ServerAliveCountMax 3'
 'ServerAliveInterval 10'
)

#   /////////////////////////
#   // Do not modify below //
#   /////////////////////////

###################
## SCRIPT CHECKS ##
###################

if [ "$1" == "install" -o "$1" == "--help" -o "$1" == "-h" ]
then
 echo "INSTALLATION INSTRUCTIONS:"
 echo
 echo "# Set the configuration variables and forwardings"
 echo
 echo "# Make sure you have ssh keys generated"
 echo "ssh-keygen"
 echo
 echo "# Setup password-less login to the remote host"
 echo "ssh-copy-id '$REMOTE_HOST -l $REMOTE_USER -p $REMOTE_SSH_PORT'"
 echo
 echo "# Add the cron job"
 echo "echo '*/5 * * * * root $( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )/$(echo ${BASH_SOURCE[0]} | rev | cut -d'/' -f1 | rev)' > /etc/cron.d/rssht_$REMOTE_HOST"
 exit 0
fi


###############################
## SSH CONNECTION EVALUATION ##
###############################

# - If socket exists:
#   1-If the socket is old kill the connection (so that a new one is created)
#   2-Run the check command on the socket
#    2.1- if it passes, do nothing, exit
#    2.2- if it fails kill ssh and remove socket file
# - Run the ssh command

# Socket file
SOCK_FILE="$SSH_RUN_DIR/$REMOTE_HOST.sock"
# PID file
PID_FILE="$SSH_RUN_DIR/ssh_$REMOTE_HOST.pid"

if (mkdir -p $SSH_RUN_DIR)
then :
else
 echo "FATAL Error: Cannot create RUN directory $SSH_RUN_DIR/"
fi

if [ -S "$SOCK_FILE" -o "$1" == "stop" ]
then
 # If Socket is older than {REFRESH_SOCKET} minutes OR if stop argument is passed, stop the connection
 if [ "$REFRESH_SOCKET" -gt "0" -o "$1" == "stop" ]
 then
  if [ -n "$(find $SOCK_FILE -mmin +$REFRESH_SOCKET)" -o "$1" == "stop" ]
  then
   if [ "$1" == "stop" ]
   then
    echo "Stop argument passed, killing .."
   else
    echo "Existing SSH connection is old, killing .."
   fi
   # Send the exit command to the existing socket
   ssh -O exit -S $SOCK_FILE $REMOTE_HOST &> /dev/null
   # Kill the pid if the process some how still exists
   if (kill -0 $(cat $PID_FILE) &> /dev/null )
   then
    echo "killing ssh process $(cat $PID_FILE)"
    kill -9 $(cat $PID_FILE) &> /dev/null
   fi
   rm -rf $PID_FILE &> /dev/null
  fi
 fi
 #If the user passed stop, don't proceed further
 [ "$1" == "stop" ] && exit 0
 
 # Check the status of the SSH connection through the socket file
 if (ssh -O check -S $SOCK_FILE $REMOTE_HOST &> /dev/null)
 then
  # SSH connection is fine
  echo "ssh connection is fine, exiting"
  exit 0
 else
  # SSH socket check failed
  # Try killing the PID first
  if [ -e "$PID_FILE" ]
  then
   if (kill -0 $(cat $PID_FILE) &> /dev/null )
   then
    echo "killing ssh process $(cat $PID_FILE)"
    kill -9 $(cat $PID_FILE) &> /dev/null
    rm -rf $PID_FILE &> /dev/null
   fi
  fi
  # Remove the socket if it still exists
  if [ -S "$SOCK_FILE" ]
  then
   if (rm -rf "$SOCK_FILE" &> /dev/null)
   then
    echo "FATAL ERROR: Cannot remove stalled socket file $SOCK_FILE"
    echo "Exiting.."
    exit 9
   else
    echo "Stalled socket file removed"
   fi
  fi
 fi
fi

# The socket and process should be gone by now; If not this is an exception; exit!
# This should not happen
if [ -S "$SOCK_FILE" ]
then
 echo "Exception: Cannot remove socket file. SSH connection seems to be stuck"
 echo "Exiting"
 exit 11
fi

##########################
## SSH COMMAND CREATION ##
##########################

# Whether to use -g switch or not
P_R_A=""
[ "$PORTS_REMOTELY_ACCESSIBLE" == "yes" ] && P_R_A="-g"

# Remote forwardings
RFWDS=""
for i in "${!REMOTE_FWDS[@]}"
do
  RFWDS="$RFWDS-R ${REMOTE_FWDS[$i]} "
done

# SSH options
SOPTS=""
for i in "${!SSH_OPTS[@]}"
do
  SOPTS="$SOPTS-o '${SSH_OPTS[$i]}' "
done

# SSH final command
SSH_COMMAND="$SSH_BIN $SOPTS $P_R_A -f -q -N -S $SOCK_FILE $RFWDS -p $REMOTE_SSH_PORT -l $REMOTE_USER $REMOTE_HOST"

#####################
## RUN SSH COMMAND ##
#####################

eval "$SSH_COMMAND"
 if [ "$?" -ne "0" ]
 then
  echo "FATAL ERROR: SSH command failed"
  echo "SSH_COMMAND=$SSH_COMMAND"
  exit 10
 else
 #Save the PID
  SOCK_CHECK=$(ssh -O check -S $SOCK_FILE $REMOTE_HOST 2>&1)
  SPID=$(echo $SOCK_CHECK | cut -d'=' -f2 | sed 's/)//')
  echo "$SPID" > $PID_FILE
  echo "RSSHT to host $REMOTE_USER@$REMOTE_HOST:$REMOTE_SSH_PORT started successfully ssh pid: $SPID"
  exit 0
 fi

5 comments:

  1. Very good script and big help... Thank you very much..

    ReplyDelete
  2. Hey thanks for this cool script, I love it :)

    ReplyDelete
  3. So much easier than messing about with routers and dynamic DNS. I've been wanting this for years. Thank you.

    ReplyDelete
  4. Awesome. Just what I was looking for.

    ReplyDelete
  5. Wow, this is awesome.
    There may be a small issue that may not have come up in your testing.
    If you were to run the script and let it start a tunnel.
    Then kill the tunnel with kill -9, or some other bad event happens and the tunnel dies, your script doesn't cleanup the socket file correctly.
    The 'then' and 'else' procedures are backwards, it manages to delete the sock file but then exits thinking there was a problem, which there was not.

    # Remove the socket if it still exists
    if [ -S "$SOCK_FILE" ]
    then
    if (rm -rf "$SOCK_FILE" &> /dev/null)
    then
    echo "FATAL ERROR: Cannot remove stalled socket file $SOCK_FILE"
    echo "Exiting.."
    exit 9
    else
    echo "Stalled socket file removed"
    fi
    fi

    Works For Me better as

    # Remove the socket if it still exists
    if [ -S "$SOCK_FILE" ] #if FILE exists and is a socket.
    then
    if (rm -rf "$SOCK_FILE" &> /dev/null)
    then
    echo "Stalled socket file removed"
    else
    echo "Stalled socket file removed"
    echo "FATAL ERROR: Cannot remove stalled socket file $SOCK_FILE"
    echo "Exiting.."
    fi
    fi

    ReplyDelete