[Read This Fine Material] from Joshua Hoblitt

Recursively chaining SSH ProxyCommand

| 1 Comment

I just came across this blog post, SSH through jump hosts, on setting up ssh proxing with netcat. The new, highly cleaver, twist here is instead of having to manually add each proxy/jump you want into your ssh config, using a wildcard host statement that can recursively match itself. If your confused as to what ssh proxy/jump/proxycommand is all about, I found a diagram & explanation on the SSH Menu Transparent Multi-hop SSH page.

Verbatim from the post:

Host */*
ProxyCommand ssh $(dirname %h) nc -w1 $(basename %h) %p

This is pretty slick in that you can bounce through an arbitrary number of hosts. eg.

ssh 1sthost/2ndhost/3rdhost

This method isn’t perfect, and while there are some good comments at the bottom of the blog post, I believe there are a few problems that aren’t fully fleshed out in the discussion. If you have different usernames on the systems you want to proxy through, this approach is going to run into trouble. The first ssh hop can get the correct username from the command line as in ssh @... but after that ssh defaults to using $USERNAME. Ideally, one could somehow specify the username for each subsequent hop. Perhaps something like:

ssh foo.example.com/a@bar.example.com/b@baz.example.com/...

However, ssh splits [user@]hostname input into a remote login username part (%r) and a target host name part (%h). The splitting is done on the right most @ and this can lead to the %h no longer being matched by Host */*. eg.

ssh foo@bar.example.com/baz@quix.example.com

Would get parsed as:

%r = foo@bar.example.com/baz
%h = quix.example.com

Since %h does not contain a / at this point, it will not match Host */*. One ugly thing that could be done is change the match to Host * and to make sure this directive stays at the very bottom of the configuration file. Then the original ssh argument could be recreated with %r@%h. At which point you would need to do your own host separator parsing. This approach is highly undesirable because the Host * rule will match any hostname not matched in the configuration file. The more obvious thing to do here is to replace the remote login username separator with another meta-character. This is what the first comment on the post tries to do:

Vincent Bernat Says:
2009-04-10 00:51:16+0200

Here is an “enhanced” version:

Host */*
ProxyCommand ssh ${$(dirname %h)/\%%/@} nc -w5 ${$(basename %h)#*%%} %p

You can specify the login for each hop with “%” instead of “@”.

Which is probably the correct general idea but this syntax won’t work under bash parameter expansion. Comment #6 suggests that this does work under zsh but I’m not generally a zsh user, nor is zsh as commonly installed as bash/ssh/nc. Even if this did work under bash, it would be more convenient to not have to specify data that’s already contained in the ssh client config file. As far as I can tell, there is no way to accomplish this with a recursively matching Host directive as %r will always be the string that was matched when ssh was first invoked. It looks like the only solution would be to write some code that can parse the the ssh config and pull out the appropriate username for each hop in the proxy chain.

The second issue is with the -w1 argument to netcat. Which tells it to disconnect if the connection is idle for one second. Unless your only executing remote commands, this value is probably too short. It needs to be at least >= ServerAliveInterval and probably more like >= ServerAliveInterval * ServerAliveCountMax. A wait timeout shouldn’t be needed at all as a broken ssh connection will kill off the associated shell, which should terminate the netcat invocation. The -w[n] param can just be removed.

If anyone knows a working solution to the the username issue, please let me know.

One Comment

  1. Pingback: One-liner SSH via jump box using ProxyCommand » lankycoder

Leave a Reply