1
1
#! /usr/bin/env bash
2
2
# Usage:
3
3
# ssh-et [ssh_options] <remote>
4
- #
5
- # See also https://github.com/infokiller/ssh-et
6
4
7
5
# See https://vaneyckt.io/posts/safer_bash_scripts_with_set_euxo_pipefail/
8
6
set -o errexit -o errtrace -o nounset -o pipefail
9
7
8
+ readonly MAX_RETRIES=10
9
+
10
10
_command_exists () {
11
11
command -v -- " $1 " & > /dev/null
12
12
}
@@ -57,13 +57,18 @@ _check_args() {
57
57
fi
58
58
}
59
59
60
+ # https://stackoverflow.com/a/6943581
61
+ _is_port_open () {
62
+ local port=" $1 "
63
+ ! { printf ' ' > /dev/tcp/127.0.0.1/" ${port} " ; } & > /dev/null
64
+ }
65
+
60
66
# The range 49152–65535 contains ephemeral/dynamic ports. We scan 200 ports
61
67
# that start with the prefixes "522" or "622" (the 22 part of the prefix is
62
68
# useful to remember it's used for SSH).
63
69
_find_ephemeral_port () {
64
70
for port in {52200..52299} {62200..62299}; do
65
- # Check if port is open. See: https://stackoverflow.com/a/6943581
66
- if ! { printf ' ' > /dev/tcp/127.0.0.1/" ${port} " ; } & > /dev/null; then
71
+ if _is_port_open " ${port} " ; then
67
72
echo " ${port} "
68
73
return 0
69
74
fi
@@ -82,34 +87,58 @@ main() {
82
87
# defined when the trap is ran.
83
88
# shellcheck disable=SC2064
84
89
trap " rm -rf -- '${tmpdir} ' &> /dev/null || true" EXIT ERR INT HUP TERM
85
- local port
86
- if ! port=" $( _find_ephemeral_port) " ; then
87
- _log_error ' Could not find an ephemeral port'
88
- return 2
89
- fi
90
- _log_info " Found open port: ${port} "
91
90
local et_fifo=" ${tmpdir} /et_fifo"
92
91
mkfifo " ${et_fifo} " || return $?
93
- local et_cmd=(et -t " ${port} " :22 -N " ${remote} " )
94
- _log_info " Running: ${et_cmd[*]} "
95
- " ${et_cmd[@]} " > " ${et_fifo} " &
96
- et_pid=$!
97
- found=0
98
- while IFS=' ' read -r line; do
99
- printf ' et: %s\n' " ${line} "
100
- if [[ $line == * " feel free to background" * ]]; then
101
- found=1
102
- break
92
+ local port num_retries=0 success=0 has_stdout=0
93
+ while (( ! success && num_retries < MAX_RETRIES)) ; do
94
+ if ! port=" $( _find_ephemeral_port) " ; then
95
+ _log_error ' Could not find an ephemeral port'
96
+ return 1
103
97
fi
104
- done < " ${et_fifo} "
105
- (( found)) || return 3
106
- # We use the localhost loopback address for all remote hosts, so we don't want
107
- # to register it in the known hosts file or do any host auth against it (which
108
- # will lead to errors since SSH will think the host key changed). et does its
109
- # own host auth so this is hopefully safe.
98
+ _log_info " Found open port: ${port} "
99
+ local et_cmd=(et -t " ${port} " :22 -N " ${remote} " )
100
+ _log_info " Running: ${et_cmd[*]} "
101
+ " ${et_cmd[@]} " > " ${et_fifo} " &
102
+ et_pid=$!
103
+ success=0
104
+ has_stdout=0
105
+ while IFS=' ' read -r line; do
106
+ has_stdout=1
107
+ printf ' et: %s\n' " ${line} "
108
+ if [[ $line == * " Address already in use" * ]]; then
109
+ wait " ${et_pid} " || true
110
+ if (( num_retries < MAX_RETRIES - 1 )) ; then
111
+ _log_info ' Port became used, finding another one...'
112
+ sleep 0.$(( RANDOM))
113
+ fi
114
+ (( num_retries += 1 ))
115
+ break
116
+ fi
117
+ if [[ $line == * " feel free to background" * ]]; then
118
+ success=1
119
+ break
120
+ fi
121
+ return 1
122
+ done < " ${et_fifo} "
123
+ (( has_stdout)) || return 1
124
+ done
125
+ (( success)) || return 1
126
+ # We use the localhost loopback address for all remote hosts, but we still
127
+ # want to use the SSH options set for the remote in the client
128
+ # config. Therefore, we use the original remote hostname, but override two SSH
129
+ # options:
130
+ # - HostName: to connect to the loopback address
131
+ # - HostKeyAlias: to avoid warnings about the host key file
132
+ # NOTE: We must put the user provided SSH args first, since SSH gives priority
133
+ # to the *first* args it encounters.
134
+ # TODO: This doesn't work correctly when using the "%h" token in the ssh
135
+ # config, since it will be set to the loopback address instead of the remote
136
+ # name given on the command line. One possible workaround to explore is to
137
+ # create a temporary SSH config that includes the real config, and makes sure
138
+ # to only set HostName after all other config is passed.
110
139
local ssh_cmd=(
111
- ssh -o ' UserKnownHostsFile=/dev/null ' -o ' StrictHostKeyChecking=no '
112
- ' 127.0.0.1 ' -p " ${port } " " ${ssh_args[@] } "
140
+ ssh -p " ${port} " " ${ssh_args[@]} " -oHostName= ' 127.0.0.1 '
141
+ -oHostKeyAlias= " ${remote } " " ${remote } "
113
142
)
114
143
_log_info " Running: ${ssh_cmd[*]} "
115
144
" ${ssh_cmd[@]} "
0 commit comments