I use ssh a lot, especially when I am on my laptop. Sometimes I even move my laptop. Then the directives in my ssh config file can refer to unresolvable hostnames or use incorrect proxy commands.
I have “fixed” this problem by using different ssh Host
names to switch between different connection options. Today I learned about Match
which allows me to automate this.
Match Documentation
The man ssh_config
page has the following details about Match
:
Match
Restricts the following declarations (up to the next Host or Match keyword) to be used only when the conditions following the Match keyword are satisfied. Match conditions are specified using one or more criteria or the single token
all
which always matches.
There is a lot more detail. The important thing to realise here is that the Match
block is conditional and lasts until the next Host
block (so don’t put a Host
block inside it!).
Original SSH Configuration
With this we should come up with an example split configuration and go through fixing it. I have a machine I refer to as dl
(deep learning). If I am on my local network I can ssh to dl.local
directly as I have an entry for that in my DNS server. The problem arises when I am at work, where I have to proxy my ssh connection through a gateway box on my network.
This gives me two configurations, dl-home
and dl-office
for the two locations:
Host dl-home
HostName dl.local
Host jump
HostName i.live.here
Host dl-office
ProxyCommand ssh jump nc %h %p
I use the jump
configuration just to name the jump box.
New SSH Configuration
The difference between the two configurations is the use of the ProxyCommand
. We need to apply this change only when the ssh target is the deep learning machine, and only when I am at the office. If the ProxyCommand
was unconditionally applied then when I ssh to the jump
box directly that would get proxied … through the jump
box.
The Match
directive documentation does not state this explicitly, but the conditions of the directive are all AND
ed and boolean short circuiting is applied. This means that we can efficiently apply the ProxyCommand
as we can first test that the ssh target is the deep learning box, and then run some command to determine if I am on my home network.
This is done with the two conditionals host
and exec
. The host
conditional makes the Match
directive only match if the current ssh target is the named host, so these two following directives are equivalent:
Host dl-home
HostName dl.local
Match host dl-home
HostName dl.local
The exec
conditional runs a command and tests the exit status. If the command completes successfully then the condition passes. It can be negated with !
(so !exec
).
To determine if I am on my home network I can test if the dl.local
hostname resolves. This can be done with nslookup
(dig
can also do this but does not fail when the hostname is unknown):
nslookup dl.local
Server: 127.0.0.53
Address: 127.0.0.53#53
Non-authoritative answer:
Name: dl.local
Address: ... ip v4 ...
Name: dl.local
Address: ... ip v6 ...
When it fails:
nslookup dl.local
Server: 127.0.0.53
Address: 127.0.0.53#53
** server can't find dl.local: NXDOMAIN
With this I can craft my exec conditional:
!exec "nslookup dl.local"
The output of the command is not printed to the terminal when the test is run.
Putting it all together I get:
Host dl
HostName dl.local
Host jump
HostName i.live.here
Match host dl !exec "nslookup dl.local"
ProxyCommand ssh jump nc %h %p
Now I can consistently use ssh dl
no matter where I am!
It’s nice to make sure that the host
conditional is first as that avoids running the nslookup
command when sshing to an unrelated machine.
Testing It
It would be nice to be able to prove that it is running the exec
command and correctly including or excluding the directives within. We can see this if we run ssh dl -vvv
. There is a lot of output to this command, I am going to show the relevant output from a successful Match
:
...
debug2: checking match for 'host dl !exec "nslookup dl.local"' host dl originally dl
debug3: /home/matthew/.ssh/config line 4: matched 'host "dl"'
debug1: Executing command: 'nslookup dl.local'
debug3: command returned status 1
debug3: /home/matthew/.ssh/config line 4: matched '!exec "nslookup dl.local"'
debug2: match found
...
debug1: Executing proxy command: exec ssh jump nc dl.local 22
...
Here you can see that:
- the
host dl
conditional passes - the
nslookup dl.local
command returned exit status 1 (0 is success, everything else is failure). - so the
!exec
conditional passes - and later we see that the proxy command is executed
With this we can be sure that the conditional proxy command worked. (The verbose output when the match is skipped is similar, I’ve omitted it for brevity).