Dynamically routing ssh to a local or remote address depending on your current Wi-Fi network
I have an Intel NUC at home that I use as an HTPC and a sort-of single-machine homelab (which seems to be the in vogue term computer nerds are using for “the machine(s) I run at home for fun”). As an HTPC, it’s connected to my TV and is how I do a lot of film and TV watching. As a homelab, it’s useful for projects I’d prefer not to run on my laptop - because, say, it may involve long-running tasks, and if I have to ensure my laptop is constantly on & connected, it defeats the purpose of having a laptop.
My development environment is pretty much Vim and Tmux regardless of where
I’m working, so I can ssh to the server from my local network and work the same
way I do on my laptop. To make this a bit easier, I added a block to the
~/.ssh/config
on my laptop like:
Host htpc
User myusername
ForwardAgent yes
Hostname htpc.local
This allows me to just ssh htpc rather than needing to type ssh myusername@htpc.local.
So having a computer that isn’t a laptop and just sits, at home, always running is useful, but the flip side, of course, is that if I’m away from home and want to check in on one of the projects I’m running on that machine, that’s a hassle, since the machine isn’t with me.
Step 1 was doing the obvious thing and opened up SSH connections from outside my
local network. I’m not going to go through all the details of doing this safely
here, as there’s lots of resources already online, but it is important to
quickly mention that this is dangerous if you aren’t careful with your
configuration. If you aren’t careful, you can easily make it possible for a bad
actor to SSH into your home network from anywhere. Even if you don’t intend to
open up your machine to SSH from the Internet, a lot of the same
security-concious settings are good to apply. The key things I would recommend
anyone configuring sshd
pay attention to:
- Restrict which users can log in via
ssh
at all, either via an explicit list of users (AllowUsers
) or a group (AllowGroups
). - Regardless of how you restrict which users can login, ensure
root
isn’t one of them. That meansPermitRootLogin no
in yoursshd_config
. - Only allow authentication via keys - never passwords. Configuration options
for this include
PasswordAuthentication no
andChallengeReponseAuthentication no
(the second one is easy to forget!). - Use a non-standard
sshd
port (Port <number>
). This one falls under “security by obscurity”, so don’t count on it as a real layer of defense: if you don’t secure everything else correctly, this probably won’t save you. But it’s still worth doing – if nothing else, it will dramatically reduce the number of bots that try to break in via port-scanning, which isn’t harmful if everything else is configured correctly, but can be annoying.
After doing all that, I need to make sure I can actually reach my home server
from the Internet, so I added a port routing rule to my Wi-Fi router so that
incoming connections on the custom port I chose for sshd
are routed to my home
server’s IP. (I also have a static IP allocation for the home server so its
local IP address will never change.) Lastly, I set up a DNS record for a domain
name I own to point at my external IP. You could also use a service like
Dyn to register a domain and keep it up to date
automatically: I didn’t bother since my external IP almost never changes so I
don’t feel the need to automate this, and since being acquired by Oracle Dyn
doesn’t seem like as good a service as it used to be.
With all that done, I added a new block to ~/.ssh/config
so that when I’m away
from home I can access my home server with ssh htpc-remote:
Host htpc
User myusername
ForwardAgent yes
Port 12345
Hostname htpc.local
Host htpc-remote
User myusername
ForwardAgent yes
Port 12345
Hostname myhtpc.example.com
That’s pretty convenient, but it does mean my muscle memory gets tripped up: I’m
usually at home, so I’m used to just running ssh htpc, but now I have to
remember to type ssh htpc-remote sometimes. I could just change the Host
htpc
config to Hostname myhptc.example.com
, but I see no reason to deal with
additional latency when the target machine is a few feet away. It’d be nicer if
I could run ssh htpc and have it dynamically choose the best hostname.
Everything up until now was prelude to the “one weird trick” that makes this
possible: SSH’s Match
directive. The Match
directive allows elementary “if”
logic in your ssh_config
- you can match on things like the hostname being
connected to, the local user running ssh
, or the result of an arbitrary
command with exec
to alter the options applied to the connection. We’re going
to use exec
to make this work.
The first thing we need is a shell script which can detect if we’re on our home Wi-Fi. Here’s mine:
#!/bin/sh
#
# Exit 0 if I'm on my home network, 1 otherwise
set -e
network="MyHomeNetwork"
iwctl station wlan0 show | grep --quiet "Connected network.*$network"
I’ve saved this to ~/bin/is-home-wifi
and chmod +x
-ed it. The details of
how you’d implement this depend on your operating system and how your Wi-Fi
connection is managed. On recent versions of MacOS, networksetup
-getairportnetwork en0 should include your current network name. On Linux, I
leave this as an exercise to the reader:
iwd is what I use to manage Wi-Fi,
you’d need to adjust the commands if you use
NetworkManager, or
netctl, or something else. (If you
use Linux and aren’t sure what network manager you use, it’s probably
NetworkManager, I think it’s currently the most popular default choice for
distributions like Ubuntu).
Now that we’ve got this script ready, we can make our final change to
~/.ssh/config
:
Match host htpc !exec is-home-wifi
Hostname myhtpc.example.com
Host htpc
User myusername
ForwardAgent yes
Port 12345
Hostname htpc.local
The Match line
is operating this way:
host htpc
matches the host when we runssh htpc
.!exec is-home-wifi
runs theis-home-wifi
script we wrote: if it exits with status0
(success), then the condition matches.!
negates the condition.
So in pseudo-code, this can line be read as if host == "htpc" and not home wifi
.
When the condition matches, the next Hostname
line indicates which hostname to
actually connect to, overriding the default value configured under the Host
htpc
block.
Intuitively you might think the Match
block should go after the Host
block, but this doesn’t work. I don’t fully understand how the SSH config is
parsed and at what stage the Match
is evaluated and applied, but if the
block is put at the bottom, the host htpc
condition no longer matches -
host htpc.local
does match, but then the Hostname
change is no longer
applied, although ssh -vvv
output says the match was applied. If anyone can
explain the vagaries of SSH config evaluation to me, I’d be interested to learn
more.
With all of that, I can now run ssh htpc from anywhere, and be routed to the most appropriate hostname for my current network.