あらゆる環境下で単一のTCP接続のポートフォワーディングを透明性高く実現するPiping Serverを使った手法
モチベーション
遠隔地のマシンのアプリケーションをあたかもローカルにあるかのように利用する機会はよくある。それを実現するために遠隔のポートをローカルにssh -Lでフォワーディングするのは一般的な手法。だが遠隔マシンにSSHサーバーを導入できない場合やSSHポートを解放できない場合もある。デフォルト設定では任意のコマンドが実行できるためSSHを避けたいケースもある。そこでSSHサーバーに依存せずに透明性の高い手法でポートフォワーディングを実現したい。
手法
利用したいアプリケーションがある側(多くの場合は遠隔地)をサーバーホスト、それ利用する側をクライアントホストと呼ぶことにする。以下のコマンドは"サーバーホスト"の22ポートを"クライアントホスト"の2222ポートに転送する例。どちらのコマンドを先に実行しても良い。
code:bash
# サーバーホスト
code:bash
# クライアントホスト
(クライアントホスト側のncのオプションはGNU版とOpenBSD版とで差異があることは後述する)
コマンドを実行するなので、あらかじめ両方のホストを制御できる必要がある。@Cryolite氏との手法の同じ意味の別の実現方法であるため完全な互換性がありどちらかを置き換えても通信ができる。 以下は上記のコマンドを実際に実行した様子。サーバーホストSSHのポートを転送してクライアントホスト側からsshしている。
https://gyazo.com/e47fbc7fa2327ac14fb67b0d50d379ff
curlコマンドの各オプションの意味は以下の通り。
-s: プログレスバーなどを非表示する。(サイレントのs)
-S: -sをつけていてもエラーメッセージは出力する。
-N: スムーズにストリーミングさせるためにバッファリングを無効にする。
コマンドの早見表
/aaaや/bbbのパスを自動生成したり後述するE2E暗号化のコマンドの自動生成をすぐにコピペできる早見表を用意している。
https://gyazo.com/b9125d0960fdd61fe06d49a7b710c8ca
GNU版とOpenBSD版のncの違い
ncでlistenするときのオプションの違いがある。以下は全て同じことをする。
GNU nc: nc -lp 2222
OpenBSD nc: nc -l 2222
socat: socat TCP-LISTEN:2222,reuseaddr -
どちらが入っていても動くようにするために( nc -lp 2222 2>/dev/null || nc -l 2222 )を使う手もある。
code:bash
# client host (GNU nc)
# client host (OpenBSD nc)
# client host (socat)
上記のようにsocatでもパイプと連携しやすい書き方で書ける。
データの流れを図に起こすと対称性があり、データストリームの輪ができる。
https://gyazo.com/5fa135f2cdaf860a8b01ffd4392714d7
外向きのHTTPS接続のみで構成される。そのためWebサイトを閲覧できる環境を必要とするだけ。
高い透明性の実現
ポートフォワーディングやトンネリングは数多くあるが、専用のCLIのインストールを必要とするものばかり。ここで示した手法はCLIは nc (netcat)とcurl だけで実現できる。これらのCLIは多くの環境でデフォルトでインストールされ、どういった挙動をするのか把握していて、すでに信頼しているソフトウェアだと思う。これらを組み合わせたコマンドは簡素で非常に透明性が高い。
Piping Server自体の透明性のはオープンソースであり簡単にセルフホストできるように設計しているところから更に高められる。そのほかに、後述するE2E暗号化を利用することで任意のTCP接続を暗号化できるため、仮にサーバーが信頼できない状態でも安全性を確保することが出来る。 エンドツーエンド暗号化 (E2E暗号化)
Piping Server自体が信頼できない場合にも安全性を確保するためにE2E暗号化する手法を提供する。すでに「HTTPSで暗号化されているではないか」と思われるかもしれないが、これはクライアントとサーバー間の暗号化。現存する多くのアプリケーションがこの水準のセキュリティではあるが、それよりも高度な水準を実現したい。またPiping Serverに流れるデータはメモリ上にありストレージには保存されずにストリーミングされる。だがソースコードのままデプロイされている保証をするのは困難。そこでE2E暗号化してこの問題を解決する。 アイデアは以下のようにシンプルである。外に出す通信は暗号化し、中に入ってくる通信は復号する。
code:bash
# サーバーホスト (アイデア)
code:bash
# クライアントホスト (アイデア)
実際に動作するコマンドは以下。opensslコマンドとstdbufコマンドを利用している。
code:bash
# サーバーホスト
curl -sSN https://ppng.io/aaa | stdbuf -i0 -o0 openssl aes-256-ctr -d -pass "pass:mypass" -bufsize 1 -pbkdf2 | nc localhost 22 | stdbuf -i0 -o0 openssl aes-256-ctr -pass "pass:mypass" -bufsize 1 -pbkdf2 | curl -sSNT - https://ppng.io/bbb code:bash
# クライアントホスト
curl -sSN https://ppng.io/bbb | stdbuf -i0 -o0 openssl aes-256-ctr -d -pass "pass:mypass" -bufsize 1 -pbkdf2 | nc -lp 2222 | stdbuf -i0 -o0 openssl aes-256-ctr -pass "pass:mypass" -bufsize 1 -pbkdf2 | curl -sSNT - https://ppng.io/aaa stdbuf -i0 -o0 <コマンド>: リアルタイムなストリーミングをするためにバッファを無効化する。
-pbkdf2: PBKDF2を使ってパスワードから鍵を導出する。 このE2E暗号化を実現するコマンドも先程の早見表に掲載されている。
このopensslコマンドを使った例はあくまでも一例であり今後新しい暗号化技術が生まれればそれに容易に置き換えられる可能性も強調したい。
クライアントとしてのWebブラウザ
https://gyazo.com/0654027d6e304892bd9bd20546d7dff2
以下はUbuntu (VM) のリモートデスクトップをE2E暗号化で実現しているデモ。
https://gyazo.com/927759e3039063acfbc4600bca139e47
TLSを使ったE2E暗号化
opensslコマンドを使った手法意外にもE2E暗号化を実現する方法を提示する。
サーバーホストで以下のコマンドを実行してserver.crtとserver.keyを作成する。
code:bash
openssl req -x509 -newkey rsa:4096 -keyout server.key -out server.crt -days 3653 -sha256 -nodes -subj '/CN=localhost/' && cat server.key server.crt > server.pem
クライアントホストで以下のコマンドを実行してclient.crtとclient.keyを作成する。
code:bash
openssl req -x509 -newkey rsa:4096 -keyout client.key -out client.crt -days 3653 -sha256 -nodes -subj '/CN=localhost/' && cat client.key client.crt > client.pem
サーバーホストに存在する8181ポートを暗号化して転送したいとする。
サーバーホストで以下の二つのコマンドを実行する。
code:bash
socat openssl-listen:4433,reuseaddr,cert=server.pem,cafile=client.crt tcp:localhost:8181
code:bash
クラインとホストで以下の二つのコマンドを実行する。これによりサーバーホストの8181ポートがクライアントホストの8282ポートに転送される。
code:bash
code:bash
socat tcp-listen:8282,reuseaddr openssl-connect:localhost:5433,openssl-commonname=localhost,cert=client.pem,cafile=server.crt
図にすると以下のようになる。
code:図
おまけ: 単一コマンドでSSHクライアント
いままでの方法だと一度クライアントホストで2222のような適当なポートでlistenさせてそこにssh -p 2222でアクセス方法だった。以下の方法はssh -o ProxyCommandを使ってクライアントホストのコマンドを一つにまとめて実行しやすくする。ProxyCommandを~/.ssh/configに記述して省略することも可能。
code:bash
# サーバーホスト
code:bash
# クライアントホスト
上記のusernameは適切に変更すると良い。
上記の@dummyはどんな値でも良い。ただし接続先サーバーごとに~/.ssh/know_hostsで被らないように文字を変えておくと「WARNING: REMOTE HOST IDENTIFICATION HAS CHANGED!」を回避できる。
おまけ: 複数のTCPコネクション
ここ提示した方法と@Cryolite氏の手法共に単一のTCPコネクションをポートフォワーディングする技術になっている。SSHやVNCは一つのTCPコネクションだけで十分に役割を果たせる。だが、例えばHTTP、特にHTTP 1.1は複数のTCPコネクションを必要とする場合が多い。その複数のTCPコネクションを実現するためにはパイプに流れるTCPコネクションを多重化して複数のTCPコネクションを一つのストリームに流す必要がある。 以下でサーバーホストの8080をクライアントホストの8181に転送する。
code:bash
# サーバーホスト
code:bash
# クライアントホスト
yamuxのCLIは@nwtgckが開発していることには注意。ncに影響を受けて作られている。 以下のデモでは何度もリロードできている様子とコマンドを終了した後は切断されることを示している。
https://gyazo.com/b9f8f83851e0ba3cc9b48b155726a98a
複数のTCPコネクションを実現する他の方法としてはSSHをポートフォワーディングした上でssh -Lを使ってTCPコネクションをSSH上で多重化する方法もある。ポータブルなPiping Serverと繋げやすいSSHサーバー実装としてがある。 おまけ: UDP
UDPを転送する方法をyamuxコマンドは実験的に用意している。
code:bash
# サーバーホスト (実験的)
code:bash
# クライアントホスト (実験的)
ダイアグラムをベースとしているため複数のダイアグラムの境界が必要になりシンプルではあるがTCPの時とは違って独自プロトコルになってしまう(単純にダイアグラムのサイズをヘッダにしている)。このUDP対応を使ってDNSやHTTP/3が利用可能なことをは実験済み。