I got interesting comments on my previous post about SSH through jump hosts, that made me think a bit.
The first one of them suggested to use a syntax that unfortunately doesn't work on anything other than zsh, but had the nice idea to add a login for each jump server. Sadly, this only works when there is only one hop, for reasons similar to those we will explore below.
The second one suggested "/" would not be so good a separator, because it would fail in svn+ssh urls and such, which is a good point that I didn't think of only by lack of such use case. The simplicity of the resulting configuration was nice enough.
This decided me to take a shot at implementing something that would allow adding both login and port number for each jump server.
The first implementation detail to set was which characters to use for login, port number and hop separators. I chose respectively "%" (@ is unfortunately impossible to use because ssh would take it, leaving out any hop before the character), ":" (pretty standard) and "+" (not allowed in DNS, and not too illogical as such a separator, I thought).
The result can look like dark magic when you are not savvy with sed
and regular expressions:
Host *+*
ProxyCommand ssh $(echo %h | sed 's/+[^+]*$//;/+/!{s/%%/@/;s/:/ -p /};s/\([^+%%]*\)%%\([^+]*\)$/\2 -l \1/') PATH=.:\$PATH nc -w1 $(echo %h | sed 's/^.*+//;/:/!s/$/ %p/;s/:/ /')
The syntax you can use to connect through jump hosts is the following:
ssh login1%host1:port1+login2%host2:port2+host3:port3 -l login3
We'll see further down why we can't put a login for the final destination host. Or if you want to skip the boring implementation details, you can also skip to the end, where a slightly more compact version lies: while writing this post, I got some optimization ideas.
Let me try to split the sed
syntax to make it a little more understandable:
s/+[^+]*$//;
# Remove all characters starting from the last "+" in the string (i.e. keep the n - 1 first hops)
/+/!{
# If the string doesn't contain a "+" after the previous command (i.e. there is only one hop remaining), do the following, otherwise, skip until the closing curly brace
s/%%/@/;
# Replace "%" with "@" (% is doubled because of the ProxyCommand
)
s/:/ -p /
# Replace ":" with " -p ". Combined with the previous command, we rewrite "login%host:port" as "login@host -p port"
};
s/\([^+%%]*\)%%\([^+]*\)$/\2 -l \1/
# Rewrite "hop1+hop2+login%lasthop" as "hop1+hop2+lasthop -l login"
The second sed
goes like this:
s/^.*+//;
# Remove everything up to the last occurrence of "+" in the string (i.e. only keep the last hop)
/:/!
# If there is no ":" in the string, do the following, otherwise, skip the next statement
s/$/ %p/;
# Add " %p" at the end of the line
s/:/ /
# Replace ":" with " ". These last three instructions prepare a "host port" combination for use with nc
.
Let's see what the ProxyCommand
looks like for some examples, skipping PATH
setting and nc
option for better readability:
host1+host2
ssh host1 nc host2 %p
login1%host1+host2:port2
ssh login1@host1 nc host2 port2
login1%host1:port1+host2:port2
ssh login1@host1 -p port1 nc host2 port2
Now, maybe you start to see why we can't put a login on the last hop: not only does it make no sense for nc, but also the main ssh
process that runs the ProxyCommand
and will actually talk to the remote host won't have any knowledge of it.
From the above examples, it also appears obvious why we replace "login%host:port" with "login@host -p port" : so that the ssh
command in the ProxyCommand
gets the proper arguments for login and port. Note we could also replace with "host -p port -l login" for the same effect.
With even more hops, this is what happens:
host1+host2+host3
-
ssh host1+host2 nc host3 %p
login1%host1:port1+host2:port2+host3
ssh login1%host1:port1+host2:port2 nc host3 %p
Each of these ProxyCommand
s will trigger another ProxyCommand
quite looking like our first few examples.
In the first hops, if we'd just replace all "%" with "@", in cases where logins are given everywhere, we'd end up with a ProxyCommand
like the following:
ssh login%host1+login%host2 nc host3 %p
As we saw above, we can't use a login on the last hop, which means this ProxyCommand
wouldn't work as expected and is why we have to rewrite "login1%host1+login2%hop2" as "login%host1+host2 -l login2" with the last instruction in the first sed
.
In the end, this is what happens:
- login1%host1+login2%host2+host3
- ssh login1%host1+host2 -l login2 nc host3 %p
- login1%host1:port1+login2%host2:port2+host3:port3
- ssh login1%host1:port1+host2:port2 -l login2 nc host3 %p
In this last example, you see the ":port2" part is not changed into "-p port2". Actually, it would still work with the latter form : the ProxyCommand
ssh login1%host1:port1+host2 -p port2 -l login2
would itself have ssh login1@host1 -p port1 nc host2 %p
as ProxyCommand
, in which %p
would be replaced by port2, given as argument to -p
.
Based on this and with further small optimizations, we can slightly shorten our ProxyCommand
to the following :
Host *+*
ProxyCommand ssh $(echo %h | sed 's/+[^+]*$//;s/\([^+%%]*\)%%\([^+]*\)$/\2 -l \1/;s/:/ -p /') PATH=.:\$PATH nc -w1 $(echo %h | sed 's/^.*+//;/:/!s/$/ %p/;s/:/ /')
sed
instructions "decryption" is left as an exercise to the reader ;).
Update : modified the shortened version to fix the issue spotted in the comments.