自宅LANにあるホストに対して、自宅からは直接、外出先からはVPS経由で接続したいという要件がある。Tailscaleを入れられない/入れたくない端末からも使いたいので、SSHとncだけで完結する方法を採っている。
前提となるネットワーク構成
自宅ホストとVPSはTailscaleで繋がっている。自宅LAN内にいれば直接、外出先ではVPS経由で接続する。この2つの経路をProxyCommandで自動的に切り替えるのがこの仕組みの要点。

以降の設定例では、自宅ホストをmyhost、VPSをjumpboxとして記載する。
会社の端末などTailscaleをインストールできない/したくない環境でも、SSHとncが使えればVPS経由で自宅ホストに到達できるのがメリットになっている。
ssh_configの全体像
~/.ssh/configにはこういう設定を書いている。
Host myhost
HostName 192.168.10.10
Port 22
ControlMaster no
ProxyCommand sh -c 'timeout 2 nc -z %h %p 2>/dev/null && exec nc %h %p || exec ssh -W myhost:%p jumpbox'
ssh myhostと打つだけで、自宅でも外出先でも同じコマンドで接続できる。rsyncやscpのようなスクリプトもネットワーク環境を気にせず書ける。ポイントはProxyCommandとControlMaster noの2つ。
ProxyCommandによる経路の自動切り替え
ProxyCommandの中身はこうなっている。
ProxyCommand sh -c 'timeout 2 nc -z %h %p 2>/dev/null && exec nc %h %p || exec ssh -W myhost:%p jumpbox'
まずnc -zで対象ホストにTCP接続できるか2秒以内に確認する。成功すれば自宅LAN内にいるということなのでncで直接接続。失敗すれば外出先なので、VPSを踏み台にしてssh -Wで接続する。
ssh myhost
└─ ProxyCommand 実行
├─ nc -z 192.168.10.10 22 が2秒以内に成功?
│ ├─ YES → nc で直接接続(LAN内)
│ └─ NO → ssh -W で VPS 経由(外出先)
ControlMaster noにしている理由
ControlMaster(接続多重化)が有効だと、最初の接続で確立した経路がソケットに残ってProxyCommandが再評価されない。自宅で直接接続したあと外出先に移動すると、死んだソケットを掴んでタイムアウトまでハングする。自分の場合はクライアントのネットワークが頻繁に切り替わるので、多重化は諦めた。
ただ、ControlPersistを短く(例えば30秒に)設定すれば、全セッションを閉じたあとソケットが自動削除されるので、作業を終えてから移動するパターンであれば多重化の恩恵も得られる。
Host myhost
HostName 192.168.10.10
Port 22
ControlMaster auto
ControlPath ~/.ssh/sockets/%r@%h-%p
ControlPersist 30
ProxyCommand sh -c 'timeout 2 nc -z %h %p 2>/dev/null && exec nc %h %p || exec ssh -W myhost:%p jumpbox'
セッションを開いたままノートPCを閉じて移動するパターンが多い人にはこの折衷案は向かない。自分もそのパターンがあるので、今のところControlMaster noのままにしている。
Tailscaleフォールバック
各ホストには*-tsサフィックスのエントリも用意している(myhost-ts等)。TailscaleのIPを直指定していて、ProxyCommandは使わない。Tailscaleが使える端末で、LANにもVPSにも繋がらない場合の手動フォールバック用。
トレードオフ
この仕組みはシンプルだが、いくつか割り切りがある。
外出先では毎回nc -zのタイムアウト(2秒)を待ってからVPS経由に切り替わるので、初回接続に少し遅延がある。ControlMasterを無効にしているので同一ホストへの複数セッションで毎回認証が走る。
VPSが単一障害点になっている点も気になる。Tailscaleが使える端末なら*-tsエントリでフォールバックできるが、そもそもTailscaleを入れられない端末のためにこの仕組みを使っているので、その場合はVPSが落ちたら打つ手がない。
経路判定もnc -zが2秒以内に通るかだけで決まるので、ホストがスリープ中だったりネットワークが不安定だったりすると誤判定する。接続に失敗したときに原因の切り分けがやや面倒で、ssh -vで追うことになる。
Tailscaleを入れられる端末だけで運用するなら、素直にTailscaleを主経路にするほうがシンプルだ。