2025-04-26

Detecting your home network with SSH

Suppose you have a server in your private home network. It's called myserver.home, and you SSH into it as user me. When you're at home, all you need to get in is:

ssh me@myserver.home

But now you want to SSH from outside. You've set up a dynamic DNS service under the name mydyndns.example.org, and configured forwarding in your router on external port 9000 to 22 on myserver.home. Your command becomes:

ssh me@mydyndns.example.org -p 9000

You can set up aliases for these command in ~/.ssh/config:

Host srv-ext srv-int
User me

Host srv-int
Hostname myserver.home

Host srv-ext
Port 9000
Hostname mydyndns.example.org

Cool, except you have to think about which one to use, depending on where you are. Well, it might not be that important, as the srv-ext alias will likely also work at home. Nevertheless, it would be cooler to use just one alias srv, and have it automatically detect whether a local connection is possible. It might also be noticeably faster in some cases.

Solution

SSH configuration provides a Match directive that can detect several conditions, including the result of a generic command. You can use that to override the general case of external access with optimizations for internal access.

Detecting the local network

First, you need a way to detect your local network. Perhaps the most robust way is to detect your router's MAC address xx:xx:xx:xx:xx:xx, which you can get with:

$ arp -a _gateway
_gateway (192.168.1.254) at xx:xx:xx:xx:xx:xx [ether] on wlp4s0

You could wrap up testing for it in a script:

#!/bin/bash
# -*- sh-basic-offset: 2; indent-tabs-mode: nil -*-

declare -A mac_arg=()

while [ $# -gt 0 ] ; do
  arg="$1" ; shift
  case "$arg" in
    (-h|--host)
      host_arg="$1" ; shift
      ;;

    (--host=*)
      host_arg="${arg#--host=}"
      ;;

    (-m|--mac)
      arg="$1" ; shift
      arg="${arg,,}"
      mac_arg["$arg"]=yes
      ;;

    (--mac=*)
      arg="${arg#--mac=}"
      arg="${arg,,}"
      mac_arg["$arg"]=yes
      ;;

    (-*|+*)
      printf >&2 '%s: unknown switch: %s\n' "$0" "$arg"
      exit 1
      ;;

    (*)
      printf >&2 '%s: unknown arg: %s\n' "$0" "$arg"
      exit 1
      ;;
  esac
done

if [ -z "$host_arg" -o "${#mac_arg[@]}" -eq 0 ] ; then
  printf >&2 'usage: %s -h host -m mac\n' "$0"
  exit 1
fi

read canon ipbr at got_mac rest < <(arp -a "$host_arg" 2> /dev/null)
test -n "$got_mac" && test -n "${mac_arg["$got_mac"]}"

(You can surely get away with something a lot simpler.) Drop the script in (say) /usr/local/bin/host-is-mac and make it executable (chmod 755 /usr/local/bin/host-is-mac). Now you can see whether you're in your own network:

$ host-is-mac -h _gateway -m xx:xx:xx:xx:xx && echo internal
internal
$ 

If you test a hostname which doesn't resolve, the command quietly fails:

$ host-is-mac -h made-up -m xx:xx:xx:xx:xx && echo internal
$ 

A special case for your home network

Now you can configure SSH to test whether the local network is your home network, to decide whether to connect using internal or external parameters. Conflicting options are resolved by choosing the first instance, so you should define the parameters for external access as the general case, and then precede them with the specific case of being in the same network:

## specific (internal) case
Match originalhost="srv" exec "host-is-mac -h _gateway -m xx:xx:xx:xx:xx"
Port 22
Hostname myserver.home

## general (external) case
Host srv
User me
HostName mydyndns.example.org
Port 9000

The Match directive enables its following directives only if srv is given as the SSH destination, and then only if the host-is-mac command succeeds. They are treated as a logical AND with short-circuiting, so the external command is only invoked when necessary.

Test the configuration by using ssh -v srv echo yes, and look for lines with Connecting to in them. If you're connecting locally, you'll see:

debug1: Connecting to myserver.home [192.168.1.100] port 22.

If you're connecting from outside:

debug1: Connecting to mydyndns.example.org [10.10.10.10] port 9000.

Proxying

Suppose you use myserver.home as a proxy to another server otherserver.home, and you want the alias alt-srv to conditionally go direct when local. Its specialization must disable proxying:

Match originalhost="alt-srv" exec "host-is-mac -h _gateway -m xx:xx:xx:xx:xx"
ProxyJump none

Host alt-srv
User me
Hostname otherserver.home
ProxyJump srv

Handling multiple aliases

If you have several aliases for a single server, you can list them in the Host clause, separating them with spaces. However, to list them in the originalhost condition, separate them with commas:

Match originalhost="srv1,srv2" exec "host-is-mac -h _gateway -m xx:xx:xx:xx:xx"
Port 22
Hostname myserver.home

Host srv1 srv2
User me
HostName mydyndns.example.org
Port 9000

You could, of course, just use Match for both clauses.

Things that didn't work

Trying to make transclusion of configuration files conditional doesn't work:

Match originalhost="srv,alt-srv" exec "host-is-mac -h _gateway -m xx:xx:xx:xx:xx"
Include site1-specializations.conf

The Include directive applies unconditionally, and the Match directive applies only to the initial directives of the transcluded file, up until the next Match or Host.

No comments:

Post a Comment