I assume you cannot do just ssh -L 10002:hostB:10001 user@hostA
because
- either whatever listens on B's
10001
binds to the local interface only,
- or the firewall on B disallows connection to
hostB:10001
from A,
- or whatever.
ssh … && ssh …
the second command executes only after I "exit" from the first connection.
Yes, this is normal. The part before &&
must terminate and return exit status before the next command is run (or not, depending on the status).
This will run the second ssh
almost immediately but it's flawed:
( ssh … & ) && ssh …
It's flawed because the first ssh
has no impact on the second one. The second one will probably run way before the first one establishes the tunnel. Asking for passwords may also get problematic.
For this usage case ssh
has -f
option. From man 1 ssh
:
-f
Requests ssh
to go to background just before command execution. […]
So the basic command may be:
ssh -fNL 22:hostB:22 user@hostA && ssh -NL 10002:localhost:10001 user@localhost
It's up to you whether you want to use -N
with the second ssh
or not. Now the trick is the first ssh
forks to the background after asking for password (if applicable) and after establishing the first tunnel, so temporarily there are two "first" ssh
processes. The one in the background starts handling the tunnel; the one in the foreground exits, this allows &&
to work.
You probably want -o ExitOnForwardFailure=yes
. If the first tunnel cannot be established, the first ssh
will exit unsuccessfully and the second one won't run. The improved command is:
ssh -fNL 22:hostB:22 \
-o ExitOnForwardFailure=yes \
user@hostA &&
ssh -NL 10002:localhost:10001 user@localhost
Note when the second ssh
exits, the first one (in the background) remains. The advantage is you can run the sole second ssh
later. The disadvantage is you'll need to kill the first one manually if you don't want it to remain. With this in mind consider the following approach:
ssh -fNL 22:hostB:22 \
-o ExitOnForwardFailure=yes \
user@hostA
ssh -NL 10002:localhost:10001 user@localhost
Note there is no &&
. Now if there is the old first ssh
still running then the new one will fail because it won't be able to bind to the port. The second ssh
will run regardless, it will use the first tunnel and it doesn't matter how old the tunnel is.
It may happen there is no old ssh
and the first ssh
fails for whatever reasons. If so, the second one will run and it will probably fail (connection refused) because there's nothing listening on the port 22
.
Still you may not want to leave an "idle" ssh
process, you may want to terminate it automatically after the second ssh
exits. Obviously killall ssh
may cause collateral damage. Let's find something more subtle:
-M
Places the ssh
client into “master” mode for connection sharing. […]
[…]
-O ctl_cmd
Control an active connection multiplexing master process. […]
[…]
-S ctl_path
Specifies the location of a control socket for connection sharing […]
#!/bin/sh
socket=/tmp/hostA.socket
ssh -fNL 22:hostB:22 \
-o ExitOnForwardFailure=yes \
-MS "$socket"
user@hostA &&
{
ssh -NL 10002:localhost:10001 user@localhost
ssh -S "$socket" -O exit dummy
}
After the second ssh
terminates, the third one will tell the first one to exit. In the third ssh
command it's the socket that matters, not the hostname; still you need to provide some hostname, hence dummy
. The first ssh
will remove the socket before exiting, there should be no garbage left.
There are still few concerns:
- If the script is interrupted, the first
ssh
will remain. Either drop &&
or send -O exit
at the very beginning just in case (it would be the zeroth ssh
).
If the first ssh
dies abruptly, the socket may remain. It will make any new first ssh
fail. A good idea may be to expand the zeroth ssh
like this:
ssh -S "$socket" -O exit dummy || rm "$socket"
/tmp/hostA.socket
may not be the best location for the socket. In ssh_config
the equivalent of -S
command line option is ControlPath
. See what man 5 ssh_config
says about it:
It is recommended that any ControlPath
used for opportunistic connection sharing include at least %h
, %p
, and %r
(or alternatively %C
) and be placed in a directory that is not writable by other users.
A "directory that is not writable by other users" may be /run/user/$UID
, if your OS supports it (I believe it's systemd
's thing).
ssh hostA -W hostB:22
which is nearly indistinguishable fromssh hostA -N -L 12345:hostB:22
... – grawity Jun 29 at 15:52