SSH wrapper that tries several connection parameters

I'm looking for a SSH wrapper (or SSH option, if there was one), that can try connecting to several IP addresses sequentially until one of them succeeds. For example 10.0.0.1, then my_machine.example.com and finally my_machine.example.com -J me@other_machine.example.com.

Is there any tool that does this?


Solution 1:

This is my general purpose ssh wrapper. No option nor address is hardcoded. The only thing you may need to adjust is the path to your ssh executable in the line 3 (you can use executable=ssh, I chose the full path). You will find my full code down below.

Let's say you saved it as sshmt ("ssh, multi target") where your $PATH points to, made executable with chmod. Then get familiar with the syntax:

sshmt -h

Excerpt:

USAGE

sshmt [-v] ARGS [+[N] ARGS]... [-- COMMON]
sshmt -h

SYNOPSIS

Invokes ssh command with the first set of arguments ARGS and common arguments COMMON. If this command returns exit status of 255 and the second set of arguments ARGS exists, then the second ssh will be invoked with these new ARGS and COMMON; then the third and so on.

In your example case you want to invoke it like this:

sshmt 10.0.0.1 + my_machine.example.com + my_machine.example.com -J me@other_machine.example.com

or better with some convenient timeouts:

sshmt 10.0.0.1 +2 my_machine.example.com +3 my_machine.example.com -J me@other_machine.example.com +5

To remotely execute df -h in a straightforward way, invoke:

sshmt 10.0.0.1 df -h +2 my_machine.example.com df -h +3 my_machine.example.com -J me@other_machine.example.com df -h +5

but you don't want to repeat yourself, so use this instead:

sshmt 10.0.0.1 +2 my_machine.example.com +3 my_machine.example.com -J me@other_machine.example.com +5 -- df -h

Pipes should work as well:

echo 123 | sshmt 10.0.0.1 +2 my_machine.example.com +3 my_machine.example.com -J me@other_machine.example.com +5 -- sh -c "cat > /tmp/foo"

In practice you may want to define an alias:

alias myssh='sshmt 10.0.0.1 +2 my_machine.example.com +3 my_machine.example.com -J me@other_machine.example.com +5 --'

then log in with

myssh

or execute a command like

myssh uptime

This is the code. All of its logic just parses the command line really.

#!/usr/bin/env bash

executable=/usr/bin/ssh
exename="${executable##*/}"
myname="${0##*/}"
declare -a args
declare -a seq_opts
declare -a common_opts

main () {
  split_opts "$@"
  process_seq "${seq_opts[@]}" "+"
  exit 255
}

split_opts () {
  while [ $# -ne 0 ]; do
    if [ "$1" = "--" ]; then
      shift
      common_opts=("$@")
      break
    else
      seq_opts=("${seq_opts[@]}" "$1")
      shift
    fi
  done
}

process_seq() {
  if [ "$*" = "+" ] || [ "$1" = "-h" ]; then
    print_help; exit 0
  fi

  while [ $# -ne 0 ]; do
    if [ "${1:0:1}" != "+" ]; then
      args=("${args[@]}" "$1")
    else
      timeout="${1:1}"
      [[ "$timeout" =~ ^[0-9]*$ ]] || print_error
      if [ "${#args[*]}" -ne 0 ]; then
        printf '%s\n' "${myname}: trying ${args[*]}" >&2
        "$executable" ${timeout:+-o ConnectTimeout=$timeout} "${args[@]}" "${common_opts[@]}"
        status=$?
        [ $status -ne 255 ] && exit $status
        args=()
      fi
    fi
    shift
  done
}

print_error() {
  cat >&2 << EOF
${myname}: error parsing command line
Try '$myname -h' for more information.
EOF
  exit 254
}

print_help() {
  cat << EOF
USAGE

    $myname [-v] ARGS [+[N] ARGS]... [-- COMMON]
    $myname -h

SYNOPSIS

Invokes \`${exename}' command with the first set of arguments ARGS
and common arguments COMMON. If this command returns
exit status of 255 and the second set of arguments ARGS
exists, then the second \`ssh' will be invoked with these
new ARGS and COMMON; then the third and so on.

Empty set of arguments is discarded without invoking \`ssh'.
Successful invocation of \`ssh' stops parsing the command
line and makes the script exit.

OPTIONS

    -h     print this help and exit (must be the first option)
    +, +N  execute \`ssh' with preceding ARGS and COMMON

N, if given, specifies timeout for \`ssh' invoked with
immediately preceding ARGS. This is just a convenient
alternative for \`-o ConnectTimeout=N'.

The final set of arguments may or may not have a terminating \`+'.

EXIT STATUS

The exit status is 254 in case of an error while parsing
the command line; 255, if none of \`${exename}' managed
to connect; or an exit status of successfully connected
\`${exename}' otherwise.

EXAMPLES

To try 10.0.0.1 and, if failed, the alternative address:
    $myname 10.0.0.1 + my_machine.example.com

To execute \`df -h' with timeouts:
    $myname 10.0.0.1 +3 my_machine.example.com +5 -- df -h

LICENCE
        Creative Commons CC0.
EOF
}

main "$@"

Solution 2:

As far as I known there is no such built-in feature. However this can be easily scripted:

#!/bin/bash

usage ()
{
    echo "usage:"
    echo "  $0 MYHOST"
    echo "or"
    echo "  $0 IP DNS PROXYJUMP"
}

if [[ $# -eq 1 ]]; then
    host="$1"

    ssh ${host}_ip && exit 0
    ssh ${host}_dns && exit 0
    ssh ${host}_proxyjump && exit 0
    exit 1
else if [[ $# -eq 3 ]]; then
    ip="$1"
    dns="$2"
    proxy="$3"

    ssh "$ip" && exit 0
    ssh "$dns" && exit 0
    ssh "$dns" -J "$proxy" && exit 0
    exit 1
else
    echo "Illegal number of argument"
    usage
    exit 1
fi

With the following .ssh/config file:

Host MYHOST_ip
  Hostname 10.0.0.1

Host MYHOST_dns
  Hostname my_machine.example.com

Host MYHOST_proxyjump
  Hostname my_machine.example.com
  ProxyJump me@other_machine.example.com

Note that connection may take a long time for example in case of the use of the proxyjump configuration. In fact, connection may occur after 2 timeouts.

Solution 3:

This sound pretty similar to a This Other Post

Shameless repost


This is a GREAT question, I always wondered too.

The short answer is No. You cannot. So I decided to do it myself.

You can use a simple script to get the desired functionality.

I wrote a Github Pages post for this


TLDR

On UNIX, Try this:
(On Windows, read my Github Pages Post)

~/.ssh/scripts/check-host-fingerprint.sh

#!/bin/bash

fingerprints=$(ssh-keygen -lf <(ssh-keyscan $1 2>/dev/null))

for fingerprint in $fingerprints
do
        if [ "$fingerprint" == "$2" ]
        then
                exit 0
        fi
done

exit 1

~/.ssh/config


# Host with Global Fallback
Match host "my_auto_host" exec "/bin/bash %d/.ssh/scripts/check-host-fingerprint.sh 192.168.0.100 SHA256:12345678901234567890123456789012345678901234567"
    Hostname 192.168.0.100
    Port 22
Host my_auto_host
    User username
    Hostname server.domain.org
    Port 1022

# Secondary Host using Primary as Proxy
Host secondary
    User u5ernam3
    Hostname 192.168.0.101
    Port 22
    ProxyJump my_auto_host

Please read my Github Pages post if this doesn't work for you.