SOCKS しか話さないアプリには、トンネルがパケットを運びアプリがプロキシのエンドポイントを求めるため、普通の VPN 上で話し相手がない。アクセスの所有者が許せば、動的フォワーディングが一つの SSH セッションをローカルプロキシに変えるので、OpenSSH がそのエンドポイントを用意できる。次に、SSH ホストに届くトンネルと、そのホストが先に開く接続という二つのホップを、はっきり分けておく必要がある。両者を分けることで、この仕組みがなぜ動くのか、そして決して何をしないのかが説明できる。

SSH リスナーが何になり、何にならないか

動的フォワーディングは、SSH クライアントをローカルで待ち受けさせ、接続してくる相手と SOCKS4 または SOCKS5 で話させる。各リクエストは宛先を名指しし、SSH サーバーが先の接続を開く。だから宛先の名前解決は向こう側で行える。これはアプリケーション層の TCP フォワーディングであり、端末全体の VPN ではない。UDP アソシエーションも ICMP も運ばず、プロキシ設定を完全に無視するアプリには何も提供しない。SSH サーバーからアプリへのホップもトンネルの内側ではない。宛先が既知のネットワークからの通信しか受け付けない場合、これは効いてくる。

前提は現実的だ。SSH ホストへの経路、許可されたアカウント、検証済みのサーバーホスト鍵、そしてフォワードの許可である。どれもプロキシが作るものではなく、最後の一つが最初に欠けるのが普通だ。他の何かを調べる前に四つすべてを確認してほしい。どれが欠けていても、クライアント側から見た失敗は同じに見えるからだ。

ループバック上で明示的なポートを指定して起動する

未使用のローカルポートを選び、アドレスを省くのではなくループバックに明示的にバインドする。以下の三つのオプションは、それぞれ一つの理由で存在する。

ssh -D 127.0.0.1:1080 -NT -o ControlPath=none -o ExitOnForwardFailure=yes \
  [email protected]

-N はリモートコマンドを飛ばし、-T は疑似端末を飛ばすので、セッションはフォワードのためだけに存在する。ExitOnForwardFailure はリスナーを作れないときにクライアントを終了させる。頼んだフォワードなしで動かし続けることはない。ControlPath=none はセッションを既存の多重化接続の外に保つ。その接続が独自のフォワードを足す可能性があるからだ。ループバックはリスナーを LAN の外に置くが、共有マシンでは認可の境界にはならない。他のローカルプロセスも到達できるからだ。OpenSSH クライアントのリファレンスはこれらのフラグをすべて文書化しており、ループバックの警告は待ち受けるソケットとは何かということから導かれる。

名前はプロキシを迂回させず、プロキシ経由で送る

SOCKS5 クライアントは宛先を自分で解決することも、プロキシに頼むこともでき、その選択がどのリゾルバーが答えるかを決める。socks5h では curl がホスト名をプロキシ経由で送るので、名前解決は SSH サーバー上で行われる。素の socks5 ではノート PC が先に解決し、旅するのは接続だけになる。名前が内部向けなら、これは静かに目的を無効化する。下のリクエストはプロキシ除外リストも空にするので、継承された NO_PROXY の値がプロキシを迂回させることはできない。

curl -sS --noproxy '' --proxy socks5h://127.0.0.1:1080 https://app.corp.example/health

証明書の検証とアプリ自身のログインは引き続き適用される。プロキシはどちらも置き換えない。残る名前解決は二つだ。ノート PC は許可された VPN と DNS の経路で SSH ホストに届く必要があり、SSH サーバーはアプリの名前を自分で解決する必要がある。二つ目は、トラフィックがプロキシを通る前に単独で試せる。

ssh [email protected] getent hosts app.corp.example

許可された宛先を一つ証明し、それからセッションを閉じる

ポリシーが許可する宛先と、拒否すべき宛先の両方を試す。動くフォワードが証明するのはリスナーが動くことだけで、それ以上ではない。アプリが見るのは SSH サーバーが選ぶ送信元アドレスであり、それはノート PC のトンネルアドレスではない。遠い側が接続を送信元で記録している場合、リクエストが拒否されたときに最初に見るべきはこの違いだ。

セッションは、無関係なシェルを殺すのではなく、それを所有するプロセスを通じて止める。そしてリスナーが消えたことを確認する。まだ応答するポートは、実際には閉じていないセッションだ。この演習で加えたプロキシ設定だけを外し、アプリの他の設定はそのままにする。転送ルールを抱えた放置された端末はサービスではない。常設のプロキシには承認されたライフサイクルが必要で、忘れられたウィンドウではない。

ss -ltn | grep 1080

この仕組みでは直せない障害

いくつかの症状は上の限界をまっすぐ指している。早く見抜けば、プロキシのデバッグに半日を使わずに済む。表は、目にする現象とそれが通常意味するものを対応づける。最後の列にあるものはすべて設計の問いであり、設定の誤りではない。

What you seeWhat it usually meansWhere to look
The app connects and nothing followsIt ignores proxy settings entirelyIts own network settings, not the SSH session
A name resolution error for the destinationThe SSH server cannot resolve itServer resolver and search domains
A UDP-only application failsDynamic forwarding carries TCP onlyThis arrangement cannot serve it
curl works, the browser does notThe browser is not using the socks5h endpointBrowser proxy configuration
The listener answers after the session endedAnother process owns port 1080The ss output, then the owning process

最初の三行は、どれもプロキシ自体についてのエラーではなく接続の試行を生むため、人が誤診しやすい。ノート PC で解決できる名前はサーバーのリゾルバーについて何も語らず、失敗は代わりに証明書やタイムアウトの問題として読まれる。UDP だけのアプリは設計の内側で壊れているのではなく設計の外にあり、追加の ssh オプションでもそれは変わらない。ブラウザーの行は、プロキシ設定がアプリごとだという当たり前の注意だ。最後の行は終了確認が存在する理由であり、まだ待ち受けているポートは今は別の何かが所有しているということだ。

関連記事