🔌

[ポート番号].localhostでSSH内のlocalhostにアクセスする

2024/10/25に公開

やること

ssh user@ssh.server -L 9876:localhost:9876に接続した上で、
クライアント側のブラウザで http://8000.127.0.0.1:9876/resource をリクエストすると、
接続したSSHサーバー内のhttp://127.0.0.1:8000/resourceを返すようなプロキシサーバーを構築します。

モチベーション

SSHのローカル環境と、個人端末内でのローカル環境を同居させることが目的です。
SSHのダイナミックポートフォワードと組み合わせたSOCKSプロキシを用いる場合、ループバックはおろか全ネットワークがSSHプロセスによってプロキシされてしまい、その間は端末内のローカル環境は封鎖されてしまいます。
かと言って使用するであろうポートを事前にひとつひとつ-Lオプションで追加しているとキリがないわけで。
そこでChromeがlocalhostのサブドメインをDNS設定なしで巻き取ってくれる事を利用した、簡易的なトンネリングを行うプロキシサーバーをSSH側に構築します。

開発環境

Deno v2.0.2

注意点

このプロキシはあくまでもブラウザによるlocalhostのサブドメインの巻き取りを利用したものであり、追加のfetchやiframe、WebSocketを含むあらゆる接続要素はプロキシされることはないためご注意ください。(そのため VSCode-Server はフォルダが正常に表示されませんでした)

実装

ここでは事前にローカルポートフォワーディングを指定して接続します。

ローカル
ssh user@ssh.server -L 9876:localhost:9876

サーバーでの実装に移ります。

サーバー
const
    urlPortCache = {},
    urlObjCache = {}
;

Deno.serve({
    port: Number(Deno.args[0])
}, req => {
    const reqURL = urlObjCache[req.url] ||= new URL(req.url);
    
    return fetch(Object.assign(reqURL, {
        port: reqURL.hostname.split(".")[0],
        host: "127.0.0.1"
    }).href)
})
サーバー
deno --allow-net ./main.js 9876

テスト

適宣ファイルサーバーを使用して適当なディレクトリをホストします。
ここではDeno 標準モジュールの file_server を使用します。

サーバー
file_server --port=8000

Chromeから8000.localhost:9876にアクセスして、従来のlocalhostへのリクエストと同様の結果が得られれば成功です。

いかがでしたか?

この実装・およびこれを改変したものを実行して起こる不具合の責任は負いかねます。

追記・訂正(2024/10/27)

先述の例ではVSCodeのserve-webをプロキシ経由で表示しましたが、結果としてVSCodeは正常に動作できませんでした。
よってこのプロキシはプレビューのlocalhostにのみ適用することを推奨します。

原因はVSCode内部の仕様です。
WebSocket接続の解決がSSH側で8000番ポートからホストされた場合、クライアントではwss://localhost:8000に接続するようになっており、結果として通常のHTTPリクエストは8000.localhost:9876でプロキシできてもWebSocketはプロキシできないためです。

よってこの場合、VSCodeのserve-webコマンドは別途固定のローカルポートで直接クライアントに表示し、プレビュー用のlocalhostはVSCode用のものとは別の固定ポートからこのプロキシを経由することを推奨します。

以下の訂正例では、VSCode専用に8000番を、プレビュー用に9876番を開放しています。

クライアント
ssh user@ssh.server -L 8000:localhost:8000 -L 9876:localhost:9876

Discussion