Transparent SSH host-jumping (Expert)

A while ago in the Transparent SSH host-jumping (Advanced) post I described a technique on how one could jump quite effortlessly through a chain of intermediate hosts. However, there was a catch: the user names and ports across the whole chain should be the same and there was no easy way to change that.

Given that I recently paid quite a lot of attention to the ProxyCommand directive I decided to look into the implementation of the helper script that will allow one to tweak parameters for the hosts in the chain.

You can read the original post for the details of how this host-jumping technique works, here I am only going to provide the proxy script and the corresponding ssh config parameter block to use the script.

The goal was to support the following syntax:

[user@localhost ~]$ ssh default_user@userA^hostA/userB^hostB:port/hostC

It was a little challenge to come up with the character for identifying the user part for intermediate hosts:

So I decided on the ^ character as a delimiter.

In the proposed command above the “default_user” is the user name we ultimately want to use for logging into the last host in the chain (it happens that this user name will be used for any host in the chain where no alternative name is provided).

Each host in the chain could also be provided with the relevant port or, if the port is omitted, it will use the global port configuration (usually 22/tcp but can be changed with the -p argument to ssh). The script is a bit not optimised (bash is really slow on string processing, but I decided to stick with pure bash where it was possible):

#!/bin/bash
set -eu -o pipefail
exec 10<&0 11>&1 0<&2 1>&2
DEFAULT_USER="$1"
DEFAULT_PORT="$3"
HOST_CHAIN="$2"
HOST_NEXT="${HOST_CHAIN%%/*}"
HOST_USER="${HOST_NEXT%%^*}"
[ "$HOST_USER" == "$HOST_NEXT" ] && HOST_USER="$DEFAULT_USER" ||:
HOST_PORT="${HOST_NEXT##*:}"
[ "$HOST_PORT" == "$HOST_NEXT" ] && HOST_PORT="$DEFAULT_PORT" ||:
TARGET_HOST="${HOST_CHAIN##*/}"
TARGET_PORT="${TARGET_HOST##*:}"
TARGET_HOST="${TARGET_HOST%:*}"
[ "$TARGET_PORT" == "$TARGET_HOST" ] && TARGET_PORT="$DEFAULT_PORT" ||:
TARGET_HOST="${TARGET_HOST#*^}"
HOST_NEXT="${HOST_NEXT#*^}"
HOST_NEXT="${HOST_NEXT%:*}"
HOST_CHAIN="${HOST_CHAIN%/*}"
[ "$HOST_CHAIN" == "${HOST_CHAIN#*/}" ] && HOST_CHAIN= || HOST_CHAIN="${HOST_CHAIN#*/}"
HOST_CHAIN="$HOST_NEXT${HOST_CHAIN:+/$HOST_CHAIN}"
if [ ! -d "$HOME/.ssh/.sessions" ]; then
echo "Creating the sessions directory" >&2
mkdir -m700 "$HOME/.ssh/.sessions"
fi
CONTROL_SOCKET=$(printf "$HOST_USER@HOST_CHAIN:$HOST_PORT\n" | shasum | cut -f1 -d' ')
exec 0<&10 1>&11
exec ssh \
-o "ControlMaster auto" \
-o "ControlPath ~/.ssh/.sessions/$CONTROL_SOCKET" \
-o "ControlPersist 120s" \
-l "$HOST_USER" \
-p "$HOST_PORT" \
"$HOST_CHAIN" \
-W "$TARGET_HOST":"$TARGET_PORT"

The above is a proof of the concept and it “works for me” :). I am using it every day when I need to access boxes behind a bastion host. Your mileage may vary and you are free to create your own version of the script that would perform better (I would be really glad if a version of such a script could be shared with me).

The corresponding configuration block in the ssh config file looks as follows:

Host */*
# if you uncomment ControlPath you also need to uncomment ControlMaster
#ControlMaster auto
# For OpenSSH < 6.7 you may uncomment the following, but long chains will fail:
#ControlPath ~/.ssh/.sessions/%r@%h:%p
# For OpenSSH >= 6.7 you should uncomment the following:
#ControlPath ~/.ssh/.sessions/%C
ProxyCommand ~/bin/ssh-helper.sh %r %h %p

Well, this is a bit messy since OpenSSH introduced the %C macro in version 6.7 and without %C the ControlPath string gets too long for OpenSSH to create a socket on the filesystem for long chains of hosts.