153

On Cygwin, I want a Bash script to:

  1. Create an SSH tunnel to a remote server.
  2. Do some work locally that uses the tunnel.
  3. Then shut down the tunnel.

The shutdown part has me perplexed.

Currently, I have a lame solution. In one shell I run the following to create a tunnel:

# Create the tunnel - this works! It runs forever, until the shell is quit.
ssh -nNT -L 50000:localhost:3306 jm@sampledomain.com

Then, in another shell window, I do my work:

# Do some MySQL stuff over local port 50000 (which goes to remote port 3306)

Finally, when I am done, I close the first shell window to kill the tunnel.

I'd like to do this all in one script like:

# Create tunnel
# Do work
# Kill tunnel

How do I keep track of the tunnel process, so I know which one to kill?

Boann
  • 48,794
  • 16
  • 117
  • 146
jm.
  • 23,422
  • 22
  • 79
  • 93
  • I wrote a script that would help to do ssh tunneling, you can check it out at: https://github.com/gdbtek/ssh-tunneling.git – Nam Nguyen Apr 27 '14 at 09:49

7 Answers7

380

You can do this cleanly with an ssh 'control socket'. To talk to an already-running SSH process and get it's pid, kill it etc. Use the 'control socket' (-M for master and -S for socket) as follows:

$ ssh -M -S my-ctrl-socket -fNT -L 50000:localhost:3306 jm@sampledomain.com
$ ssh -S my-ctrl-socket -O check jm@sampledomain.com
Master running (pid=3517) 
$ ssh -S my-ctrl-socket -O exit jm@sampledomain.com
Exit request sent. 

Note that my-ctrl-socket will be an actual file that is created.

I got this info from a very RTFM reply on the OpenSSH mailing list.

joelostblom
  • 43,590
  • 17
  • 150
  • 159
Chris McCormick
  • 4,356
  • 1
  • 21
  • 19
  • 9
    This is the best answer I have seen on the topic so far. Thank you a lot, it should be the accepted one. I use this to connect to my Vagrant VM and run a FlywayDB update script. – Christian Jan 07 '14 at 16:46
  • 2
    Apparently control sockets do not work everywhere. For example, I get `Operation not permitted` on drone.io continuous integration environment: `muxserver_listen: link mux listener ssh-ctrl-socket.wsASkszgSBlK7kqD => ssh-ctrl-socket: Operation not permitted` – Mikko Ohtamaa Jan 02 '15 at 02:49
  • So what happens to the `my-ctrl-socket` file after this is run? When I do `ls -la` in the current folder I can't see the file anymore. – sachinruk Aug 23 '17 at 05:28
  • 4
    If you use it in a script you need to wait for the control socket for a few seconds to become available. My solution: `while [ ! -e $ctrl_socket ]; do sleep 0.1; done` – Adam Wallner Feb 11 '18 at 22:19
  • when I do this I'm getting `open failed: administratively prohibited: open failed` and I don't think it's opening the tunnel – Andy Ray Apr 03 '19 at 02:02
  • @AndyRay this sounds like a misconfiguration on the server side. Does this answer help? https://unix.stackexchange.com/questions/14160/ssh-tunneling-error-channel-1-open-failed-administratively-prohibited-open – Chris McCormick Apr 03 '19 at 07:49
  • 1
    Brilliant stuff, way more elegant then everything else I found online. Thanks – Aleksandar Grbic Oct 16 '20 at 13:11
23

You can tell SSH to background itself with the -f option but you won't get the PID with $!. Also instead of having your script sleep an arbitrary amount of time before you use the tunnel, you can use -o ExitOnForwardFailure=yes with -f and SSH will wait for all remote port forwards to be successfully established before placing itself in the background. You can grep the output of ps to get the PID. For example you can use

...
ssh -Cfo ExitOnForwardFailure=yes -N -L 9999:localhost:5900 $REMOTE_HOST
PID=$(pgrep -f 'N -L 9999:')
[ "$PID" ] || exit 1
...

and be pretty sure you're getting the desired PID

vfclists
  • 19,193
  • 21
  • 73
  • 92
Maine Guy
  • 231
  • 2
  • 2
  • You may not realise it, but this is sort of genius. I was searching for a way to track the SSH tunnel PIDs and almost ended up using the systemd service scripts. Not anymore: I can grep the SSH process I need using the tunnel name. This idea has somehow completely skipped me. Thanks a lot! – ᴍᴇʜᴏᴠ Aug 29 '18 at 19:01
19
  • You can tell ssh to go into background with & and not create a shell on the other side (just open the tunnel) with a command line flag (I see you already did this with -N).
  • Save the PID with PID=$!
  • Do your stuff
  • kill $PID

EDIT: Fixed $? to $! and added the &

jm.
  • 23,422
  • 22
  • 79
  • 93
ZeissS
  • 11,867
  • 4
  • 35
  • 50
  • 3
    If my script dies somewhere before it gets to the KILL, I have to be careful to handle that. – jm. Feb 10 '10 at 23:23
  • 3
    @jm: `trap 'kill $PID' 1 2 15` will cover many cases of script failure. – Norman Ramsey Feb 10 '10 at 23:27
  • 3
    For this to work reliably, i had to "sleep" a little AFTER creating the tunnel, but before using it. – jm. Feb 11 '10 at 00:01
  • @NormanRamsey I think you mean `trap "kill $PID"`, because bash will only interpolate variables inside double quoted strings – JuanCaicedo Jan 05 '17 at 20:11
  • 1
    @JuanCaicedo The distinction would only be important if the `PID` variable were redefined later on. The variable is either expanded when the `trap` built-in is called (the OP's approach) or when a signal has been caught (your approach); both approaches produce the same result here. – Witiko Mar 16 '17 at 11:01
  • sleep is NOT reliable ;) – Jonathan Aug 02 '17 at 17:59
4

I prefer to launch a new shell for separate tasks and I often use the following command combination:

  $ sudo bash; exit

or sometimes:

  $ : > sensitive-temporary-data.txt; bash; rm -f sensitive-temporary-data.txt; exit

These commands create a nested shell where I can do all my work; when I'm finished I hit CTRL-D and the parent shell cleans up and exits as well. You could easily throw bash; into your ssh tunnel script just before the kill part so that when you log out of the nested shell your tunnel will be closed:

#!/bin/bash
ssh -nNT ... &
PID=$!
bash
kill $PID
too much php
  • 88,666
  • 34
  • 128
  • 138
  • Very interesting. This may handle the "trap" problem better. Will have to try it. – jm. Feb 14 '10 at 06:06
2

You could launch the ssh with a & a the end, to put it in the background and grab its id when doing. Then you just have to do a kill of that id when you're done.

Valentin Rocher
  • 11,667
  • 45
  • 59
  • Be aware if using the ampersand ("&"). It is an ugly approach since you will have to determine the actual connection established for yourself. It can cause to have further code being executed which is not waiting for the actual connection to be fully established. Furthermore the connection won't be killed automatically if the script breaks. – Jonathan Aug 02 '17 at 17:58
0

A simple bash script to solve your problem.

# Download then put in $PATH
wget https://raw.githubusercontent.com/ijortengab/bash/master/commands/command-keep-alive.sh
mv command-keep-alive.sh -t /usr/local/bin

# open tunnel, put script in background
command-keep-alive.sh "ssh -fN -o ServerAliveInterval=10 -o ServerAliveCountMax=2 -L 33306:localhost:3306 myserver" /tmp/my.pid &
# do something
mysql --port 33306
# close tunnel
kill $(cat /tmp/my.pid)
-1

https://github.com/aronpc/remina-ssh-tunnel

#!/usr/bin/env sh

scriptname="$(basename $0)"
actionname="$1"
tunnelname=$(echo "$2" | iconv -t ascii//TRANSLIT | sed -E 's/[^a-zA-Z0-9-]+/-/g' | sed -E 's/^-+|-+$//g' | tr A-Z a-z)
remotedata="$3"
tunnelssh="$4"

if [ $# -lt 4 ] 
 then
    echo "Usage: $scriptname start | stop LOCAL_PORT:RDP_IP:RDP_PORT SSH_NODE_IP"
    exit
fi

case "$actionname" in

start)

  echo "Starting tunnel to $tunnelssh"
  ssh -M -S ~/.ssh/sockets/$tunnelname.control -fnNT -L $remotedata $tunnelssh
  ssh -S ~/.ssh/sockets/$tunnelname.control -O check $tunnelssh
  ;;

stop)
  echo "Stopping tunnel to $tunnelssh"
  ssh -S ~/.ssh/sockets/$tunnelname.control -O exit $tunnelssh 
 ;;

*)
  echo "Did not understand your argument, please use start|stop"
  ;;

esac

usage example

Edit or create new remmina server connection

schema

~/.ssh/rdp-tunnel.sh ACTION TUNNELNAME LOCAL_PORT:REMOTE_SERVER:REMOTE_PORT TUNNEL_PROXY

name description
ACTION start|stop
TUNNELNAME "string identify socket" slugify to create socket file into ~/.ssh/sockets/string-identify-socket.control
LOCAL_PORT the door that will be exposed locally if we use the same port for two connections it will crash
REMOTE_SERVER the ip of the server that you would access if you had it on the proxy server that will be used
REMOTE_PORT the service port that runs on the server
TUNNEL_PROXY the connection you are going to use as a proxy, it needs to be in your ~/.ssh/config preferably using the access keys

I use the combination (% g-% p) of the remmina group name and connection name to be my TUNNELNAME (this needs to be unique, it will see the socket name)

pre-command

~/.ssh/rdp-tunnel.sh start "%g-%p" 63394:192.168.8.176:3389 tunnel-name-1

post-command

~/.ssh/rdp-tunnel.sh stop "%g-%p" 63394:192.168.8.176:3389 tunnel-name-1

image

you can and should use this script to access anything, I use it constantly to access systems and services that do not have a public ip going through 1,2,3,4,5 or more ssh proxies

see more into :

  1. ssh config
  2. ssh mach
  3. ssh jump hosts
  4. sshuttle python ssh

Refs:

  1. https://remmina.org/remmina-rdp-ssh-tunnel/
  2. https://kgibran.wordpress.com/2019/03/13/remmina-rdp-ssh-tunnel-with-pre-and-post-scripts/
  3. Bash script to set up a temporary SSH tunnel
  4. https://gist.github.com/oneohthree/f528c7ae1e701ad990e6
desertnaut
  • 57,590
  • 26
  • 140
  • 166
Aron
  • 39
  • 4
  • 2
    While this link may answer the question, it is better to include the essential parts of the answer here and provide the link for reference. Link-only answers can become invalid if the linked page changes. - [From Review](/review/low-quality-posts/28511851) – desertnaut Mar 11 '21 at 17:33
  • thanks, I managed to put readme.md, I didn't know I had – Aron Mar 11 '21 at 18:03