How to automatically switch ssh config based on local subnet?
Asked Answered
H

7

25

I need to jump through an intermediate host to reach my destination when I'm on a certain network (subnet is 10.10.11.x) because of a destination port I can't change and limited ports on which I can exit the restricted network. I use a SSH config like the following with success:

Host web-direct web
    HostName web.example.com
    Port 1111

Host web-via-jump jweb
    HostName web.example.com
    Port 1111
    ForwardAgent yes
    ProxyCommand ssh -p 110 -q relay.example.com nc %h %p

Going through the jumpbox is a significant performance hit, so I need to avoid it for the majority of times that it is not needed. Switching the ssh/scp/rsync host nickname is fine for interactive use, but there are some automated/scripted tasks which it is very painful.

My shell stays open across network transitions, so startup (.zshrc) mechanisms don't help.

I've thought of running a script to poll for the restricted subnet and automating the switch by modifying the .ssh/config file, but I'm not even sure there would be a caching issue. Before I implement that, I thought I would ask if there is a better approach.

What's the best approach for swapping out SSH config based on origin host subnet detection?

In pseudo-config, something like:

if <any-active-local-interface> is on 10.10.11.x:
    Host web
        HostName web.example.com
        Port 1111
        ForwardAgent yes
        ProxyCommand ssh -p 110 -q relay.example.com nc %h %p
else:    
    Host web
        HostName web.example.com
        Port 1111
endif
Hydrophilous answered 22/11, 2016 at 16:3 Comment(0)
D
8

Based on the answer by Fedor Dikarev, Mike created a bash script named onsubnet:

#!/usr/bin/env bash

if [[ "$1" == "--help" ]] || [[ "$1" == "-h" ]]  || [[ "$1" == "" ]] ; then
  printf "Usage:\n\tonsubnet [ --not ] partial-ip-address\n\n"
  printf "Example:\n\tonsubnet 10.10.\n\tonsubnet --not 192.168.0.\n\n"
  printf "Note:\n\tThe partial-ip-address must match starting at the first\n"
  printf "\tcharacter of the ip-address, therefore the first example\n"
  printf "\tabove will match 10.10.10.1 but not 110.10.10.1\n"
  exit 0
fi

on=0
off=1
if [[ "$1" == "--not" ]] ; then
  shift
  on=1
  off=0
fi

regexp="^$(sed 's/\./\\./g' <<<"$1")"

if [[ "$(uname)" == "Darwin" ]] ; then
  ifconfig | grep -F 'inet ' | grep -Fv 127.0.0. | cut -d ' ' -f 2 | grep -Eq "$regexp"
else
  hostname -I | tr -s " " "\012" | grep -Fv 127.0.0. | grep -Eq "$regexp"
fi

if [[ $? == 0 ]]; then 
  exit $on
else
  exit $off
fi

Then in his .ssh/config file, he uses Match exec like Jakuje's answer:

Match exec "onsubnet 10.10.1." host my-server
    HostName web.example.com
    Port 1111
    ForwardAgent yes
    ProxyCommand ssh -p 110 -q relay.example.com nc %h %p

Match exec "onsubnet --not 10.10.1." host my-server
    HostName web.example.com
    Port 1111
Dad answered 22/11, 2016 at 16:4 Comment(0)
R
27

You can use Match's exec option to execute shell commands, so you can write something like this:

Match host web exec "hostname -I | grep -qF 10.10.11."
    ForwardAgent yes
    ProxyCommand ssh -p 110 -q relay.example.com nc %h %p
Host web
    HostName web.example.com
    Port 1111

The Match option boolean logic can short-circuit, so put host first to skip the exec term for other hosts. Try ssh web -vvv to see the Match logic in action.

Really answered 22/11, 2016 at 16:43 Comment(2)
Instead of comparing the local IP address I used Match host ... exec "ip route | grep ^192.168.123.0/24" to check if there is access to the target networkVerleneverlie
I went with Match Host * exec "resolvectl query %h 2>&1 | grep 10.20.0." - this matches when I pass a 10.20.0.x IP address or when I pass a hostname which resolves to 10.20.0.xVacation
D
8

Based on the answer by Fedor Dikarev, Mike created a bash script named onsubnet:

#!/usr/bin/env bash

if [[ "$1" == "--help" ]] || [[ "$1" == "-h" ]]  || [[ "$1" == "" ]] ; then
  printf "Usage:\n\tonsubnet [ --not ] partial-ip-address\n\n"
  printf "Example:\n\tonsubnet 10.10.\n\tonsubnet --not 192.168.0.\n\n"
  printf "Note:\n\tThe partial-ip-address must match starting at the first\n"
  printf "\tcharacter of the ip-address, therefore the first example\n"
  printf "\tabove will match 10.10.10.1 but not 110.10.10.1\n"
  exit 0
fi

on=0
off=1
if [[ "$1" == "--not" ]] ; then
  shift
  on=1
  off=0
fi

regexp="^$(sed 's/\./\\./g' <<<"$1")"

if [[ "$(uname)" == "Darwin" ]] ; then
  ifconfig | grep -F 'inet ' | grep -Fv 127.0.0. | cut -d ' ' -f 2 | grep -Eq "$regexp"
else
  hostname -I | tr -s " " "\012" | grep -Fv 127.0.0. | grep -Eq "$regexp"
fi

if [[ $? == 0 ]]; then 
  exit $on
else
  exit $off
fi

Then in his .ssh/config file, he uses Match exec like Jakuje's answer:

Match exec "onsubnet 10.10.1." host my-server
    HostName web.example.com
    Port 1111
    ForwardAgent yes
    ProxyCommand ssh -p 110 -q relay.example.com nc %h %p

Match exec "onsubnet --not 10.10.1." host my-server
    HostName web.example.com
    Port 1111
Dad answered 22/11, 2016 at 16:4 Comment(0)
T
6

OpenSSH 9.4 added a new match predicate match localnetwork that can match a list of CIDR-format addresses. So to matching if the client is on the subnet 10.10.12.0/24 while defaulting to use a VPN IP address otherwise can be done with:

Match Host pi localnetwork 10.10.12.0/24
    HostName 10.10.12.4

Host pi
    HostName 10.0.0.2
    User turtvaiz
    Identityfile ~/.ssh/id_pi

You can also match multiple subnets with a list, e.g. localnetwork 10.10.12.0/24,10.0.0.0/24.

Tannie answered 19/11, 2023 at 12:37 Comment(2)
Thanks. However, it does not seem to be supported on OpenSSH_for_Windows_9.4p1, "match localnetwork: not supported on this platform", see source code github.com/PowerShell/openssh-portable/blob/…Rodriguez
I didn't notice that you had added this in between my visits to this question, and accidentally added a duplicate answer (now deleted).Research
S
4

The easiest way to match an IP is the use of a regex like this:

Match exec "[[ '%h' =~ ^10\.10\.11\. ]]"
   ... 

You can expand it to match additional IPs:

Match exec "[[ '%h' =~ ^10\.10\.1(1|2)\. ]]"
   ... 
Sharpshooter answered 24/5, 2021 at 21:11 Comment(2)
I see in the ssh config docs that %h The remote hostnamein the TOKENS section but the IP match is for the local IP address. How would this work? Is this a different %h?Hydrophilous
Hello Mike, Right %h is the remote host. Between the double quotes is shell syntax. In this case a regular expression matching. If i run ˋssh 10.10.11.3ˋ it matches and all directives behind like a proxycommand will be asigned. Any futher questions? Best OlliSharpshooter
L
2

My solution to this problem is the following:

Host myserver
    HostName [internal IP]
    ...
    
Match Host [internal IP] !Exec "nc -w1 -q0 %h %p < /dev/null"
    ProxyCommand ssh jumphost -W %h:%p

It's important to have the Host myserver lines first, so the SSH client will know the IP address.

In the Match expression,

  • The Host option matches on that IP. (It accepts *, so you can match to /8, /16 or /24 subnets too.)
  • The Exec option executes a netcat with a 1 second timeout to test if the SSH port is open. If not, the ProxyCommand is used.

This is the clearest way I found to actually test if you need a jumphost or not. If your network is lagging, you can set higher timeouts, of course. See man ssh_config for more details.

Lightyear answered 15/7, 2020 at 22:10 Comment(0)
C
1

I'm using the following function for that:

function ssh() {
  network=`networksetup -getairportnetwork en0 | cut -d: -f2 | tr -d [:space:]`
  if [ -n "$network" -a -f $HOME/.ssh/config.$network ]; then
    /usr/bin/ssh -F $HOME/.ssh/config.$network "$@"
  else
    /usr/bin/ssh "$@"
  fi
}
export -f ssh

So I need a separate configuration file for each WiFi network where I want a custom solution. It works for me right now, but it's ugly. I can recommend it only as an idea, not as the best solution.

I'd be glad to know any better solution.

Cowitch answered 22/11, 2016 at 16:36 Comment(3)
I'm guessing this doesn't address rsync, scp and/or git. Have you tried those?Hydrophilous
It's only alias, it doesn't impact other programs. For scp and rsync it's easy to make similar aliases and for git not sure it will be easy. @Really give solution using Match exec, I will rewrite my configuration as it looks like better solution.Cowitch
If you get the Match exec solution working please comment with your experience. I would change grep to fgrep unless you want to use a real regexp.Hydrophilous
R
1

Instead of checking the subnet CIDR, you can check what domain suffix DHCP has given you.

  • If you're trying to use a jump box when outside the intranet, this approach is more robust than checking for IP ranges in the reserved private allocation (e.g. your home network or remote-work location uses the same block as example.com's intranet).

  • This approach is not useful if your intranet uses the same DNS suffix everywhere and you're trying to traverse subnets within the intranet. If that's your situation, use Jakuje's solution.

Match Host web Exec "hostname -d | ! grep -q -E '^example\.com'"
    ForwardAgent yes
    ProxyCommand ssh -p 110 -q relay.example.com nc %h %p
Host web
    HostName web.example.com
    Port 1111

If you don't have a recent version of hostname that has the -d option (e.g. you're on MacOS), you can just query resolve.conf directly:

Match Host web Exec "! grep -q -E '^\s*search[ \t]+example\.com' /etc/resolv.conf"
    ...
    ...
Research answered 3/2, 2020 at 15:22 Comment(1)
An alternative to the hostname command on macOS is the scutil tool. E.g. scutil --dns | grep -qE '^\s+search domain...\s+:\s+example.com' (this looks for the search domain setting, which may or may not be helpful depending on your particular network config).Clay

© 2022 - 2024 — McMap. All rights reserved.