ちょっと話題の記事

SOCKSプロキシとHTTPプロキシの違いについて勉強してみた

パケットを眺めながらSOCKSプロキシとHTTPプロキシの違いについて勉強してみました。
2018.08.17

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

はじめに

サーバーレス開発部@大阪の岩田です。 先日接続元IPアドレスをクラスメソッドのGIPに制限した検証環境を利用してAPIのテストを行なっていたところ、リモートワーク中のメンバーが検証環境に接続できないという状況が発生しました。

下記の記事で紹介されているように、クラスメソッドではSOCKSサーバーが構築されているので、VPN経由で社内NWに接続し、SOCKSサーバーをプロキシとして利用すれば本来リモート環境からでも検証環境が利用できるはずです。

VPN利用者のためにdelegateでSOCKSサーバーを立ててみました

最初はcurlコマンドのオプションに--proxyを付けてプロキシサーバーを指定するようお願いしたのですが、--proxy http://proxy.example.com:xxxxのような指定を行なっていたようで、問題が解決しませんでした。 結局--proxy socks5://proxy.example.com:xxxxのように指定してもらうことで解決したのですが、よく考えたらSOCKSプロキシについて何も知らなかったので、勉強のためにHTTPプロキシとの違いについて調査してみました。

HTTPプロキシとは

まずHTTPプロキシについてのおさらいです。 プロキシとはHTTPなどの通信を中継するための仕組みで、クライアントとサーバーの通信経路の間で

  • 通信内容を転送する
  • 通信内容を改変して転送する
  • サーバーの代わりにクライアントに応答する

といった処理を行います。

HTTP/1.1ではプロキシを下記のように定義しており、リクエスト・レスポンスの改変を行わないプロキシは透過プロキシ、リクエスト・レスポンスの改変を行うプロキシは非透過プロキシと呼ばれます。

An intermediary program which acts as both a server and a client for the purpose of making requests on behalf of other clients. Requests are serviced internally or by passing them on, with possible translation, to other servers. A proxy MUST implement both the client and server requirements of this specification. A "transparent proxy" is a proxy that does not modify the request or response beyond what is required for proxy authentication and identification. A "non-transparent proxy" is a proxy that modifies the request or response in order to provide some added service to the user agent, such as group annotation services, media type transformation, protocol reduction, or anonymity filtering. Except where either transparent or non-transparent behavior is explicitly stated, the HTTP proxy requirements apply to both types of proxies.

HTTPへのアクセスをプロキシする場合

HTTPプロキシ経由でHTTPのリクエストを行う場合、リクエスト・レスポンスの改変を伴わないシンプルなパターンは下記のようなシーケンスになります。

  1. クライアントが3way hand shakeでHTTPプロキシとTCPのコネクションを確立
  2. クライアントがHTTPプロキシに対してHTTPリクエストを送信。この際パスを/index.htmlといった形式ではなくhttp://example.com/index.htmlという形式でリクエストする
  3. HTTPプロキシが/index.htmlというパスでサーバーにリクエストを送信
  4. HTTPプロキシがサーバーから受け取ったレスポンスをクライアントに返却

HTTPSのアクセスをプロキシする場合

次にHTTPプロキシ経由でHTTPSのリクエストを行う場合ですが、HTTPの場合と異なりメッセージが暗号化されているため、プロキシがメッセージの中身を覗き見て適切な処理を行うことができません。 そこで、HTTPS通信をプロキシするにHTTP/1.1で追加されたCONNECTメソッドを利用します。 CONNECTメソッドはHTTPのプロトコルの上に他のプロトコルのメッセージを流せるようにするメソッドです。

通信のシーケンスとしては

  1. クライアントが3way hand shakeでHTTPプロキシとTCPのコネクションを確立
  2. クライアントがHTTPプロキシに対してCONNECTのリクエストを送信
  3. HTTPプロキシが3way hand shakeでサーバーとTCPのコネクションを確立
  4. HTTPプロキシがクライアントに準備完了のレスポンスを返却
  5. クライアントがTLSやSSLのメッセージをHTTPでカプセル化してHTTPプロキシに送信
  6. HTTPプロキシがHTTPのカプセル化を解除してTLSやSSLのメッセージをサーバーに送信
  7. HTTPプロキシがサーバーから受け取ったレスポンスをHTTPでカプセル化してクライアントに返却

といった流れになります。

SOCKSプロキシとは

続いてSOCKSプロキシです。

SOCKSプロキシとはL4の通信を中継・代理する透過的なプロキシプロトコルSOCKSを利用するプロキシのことです。 SOCKSには基本となるSOCKS4と、SOCKS4の拡張であるSOCKS4a、SOCKS5というプロトコルが存在します。 SOCKS4については下記のサイトに分かりやすくまとまっていました。 http://hp.vector.co.jp/authors/VA017085/text/socks4.html

かいつまんで説明するとクライアントがSOCKSサーバーに対してCONNECTもしくはBINDというコマンドを発行し、サーバーへのメッセージ転送を要求。リクエスト成功後、SOCKSサーバーが透過的にクライアント⇄サーバーの通信をプロキシする。

といった処理を行います。

なお、個人的に重要だと思ったことですがHTTPプロキシを利用する場合、サーバーの名前解決はプロキシが行いますが、SOCKSプロキシを利用する場合、サーバーの名前解決はクライアントが行います。 クライアントがHTTP等のメッセージをSOCKSでカプセル化する際に、サーバーのIPアドレスと・ポート番号まで含めてカプセル化するような挙動となります。

※2018/8/25追記 Twitterで指摘を頂いたので追記します。 後述するSOCKS5からはプロキシ側に名前解決させることも可能になっています。 クライアントがプロキシに対してリクエストを行う際に、ATYP(Address Type)にDOMAINNAMEを指定することでプロキシ側で名前解決を行うことが可能です。 一例ですが、curlコマンドであれば--proxyオプションの指定をsocks5h://xxxxとすることで、プロキシ側で名前解決させることが可能です。 実際にパケットキャプチャで確認すると、以下のような結果になりました。

クライアント側で名前解決する場合

プロキシに名前解決させる場合

SOCKS5

SOCKS5はSOCKS4を拡張したプロトコルで、SOCKS4と比較すると

  • TCPだけでなくUDPにも対応
  • クライアントの認証機能が充実している

といった特徴を持ちます。

SOCKS5についてはRFC化されており、下記のリンクから詳細を確認することができます。 https://www.ietf.org/rfc/rfc1928.txt

SOCKS5を用いた通信シーケンスは下記のようになります。

  1. クライアントが3way hand shakeでSOCKSサーバーとTCPのコネクションを確立
  2. クライアントがSOCKSサーバーに対して認証のリクエストを発行
  3. SOCKSサーバーがクライアントに対して認証のレスポンスを返却
  4. クライアントがSOCKSサーバーに対してSOCKS5のコマンド(CONNECTもしくはBINDもしくはUDP ASSOCIAT)を発行
  5. SOCKSサーバーがクライアントに準備完了のレスポンスを返却
  6. SOCKSサーバーが3way hand shakeでサーバーとTCPのコネクションを確立
  7. SOCKSサーバーががクライアントに準備完了のレスポンスを返却
  8. クライアントがHTTP等のメッセージをSOCKS5でカプセル化してSOCKSサーバーに送信
  9. SOCKSサーバーがSOCKS5のカプセル化を解除してHTTP等のメッセージをサーバーに送信
  10. SOCKSサーバーがサーバーから受け取ったレスポンスをSOCKS5でカプセル化してクライアントに返却

この記事ではSOCKS5を用いて動作を確認しました。

検証

実際に下記のような環境を構築し、tcpdumpでパケットキャプチャを行ないながらcurlコマンドでリクエストを発行し、パケットの中身を分析してみました。

  • 同一のVPC内に3台のEC2を構築、それぞれクライアント、プロキシ、Webサーバーとして利用
  • プロキシサーバーにはHTTPプロキシ用にSquidをインストール、SOCKSプロキシ用にdelegateをインストール
  • Squid、delegateの設定はインストール直後から原則変更無し クライアントからのアクセス許可のみ追加
  • hostsファイルを使ってclient、proxy、webという名前でお互いに名前解決できるように設定

キャプチャ結果

パケットキャプチャの結果は次のようになりました。

HTTPプロキシでhttp://....にアクセス

curl http://web --proxy http://proxy:8080

クライアント上でのキャプチャ結果

クライアント上でのキャプチャ結果がこちらです。

上記画像のNo4でプロキシに対してHTTPのリクエストを行なっている箇所の詳細です。

プロキシに対してリクエストしているURIがhttp://web/となっており、ホストヘッダはWebサーバーの名前であるwebとなっています。

プロキシ上でのキャプチャ結果

次に、プロキシ上でのキャプチャ結果です。

クライアントからGETリクエストを受け付けた後、サーバーに対してGETリクエストを送信しています。 その際リクエスト先のURIがhttp://web/から/に変わっています。 その後サーバーから200のレスポンスを受け、クライアントに200のレスポンスを返却しています。

上記画像のNo6でクライアントからHTTPのリクエストを受信している箇所の詳細です。

No13でサーバーに対してHTTPのリクエストを行なっている箇所の詳細です。

GETするパスが変わっていたり、X-Forwarded-ForViaといったHTTPヘッダが増えたりしているのが分かります。

サーバー上でのキャプチャ結果

最後にサーバー上でのキャプチャ結果です。

上記画像のNo4でプロキシからHTTPのリクエストを受信している箇所の詳細です。

サーバーから見るとリクエスト元はプロキシであり、TCPレベルではクライアントの存在が隠蔽されています。

HTTPプロキシでhttps://....にアクセス

次にHTTPプロキシを介してHTTPSのサイトにアクセスしてみます。

curl https://web --proxy http://proxy:8080 -k

クライアント上でのキャプチャ結果

クライアント上でのキャプチャ結果はこちらです。

アクセス先のURIがhttpからhttpsに変わったことにより、プロキシに対して発行するHTTPのメソッドがGETからCONNECTに変わっています。

上記画像のNo4でCONNECTメソッドを呼んでいる箇所の詳細です。

上記画像のNo8でプロキシサーバーとTLSのHandshakeを行なっている箇所の詳細です。

TLS関連のメッセージがHTTPでカプセル化されていることが分かります。

プロキシ上でのキャプチャ結果

次に、プロキシ上でのキャプチャ結果です。

クライアントClient Helloのリクエストを受けた直後、そのままサーバーにClient Helloのリクエストを投げています。 その後サーバーからServer Helloの応答を受けた後、クライアントに対してServer Helloの応答を返しています。

上記画像のNo12でWebサーバーとTLSのHandshakeを行なっている箇所の詳細です。

先ほどのクライアント→プロキシの通信と見比べると、HTTPのカプセル化が解除されているのが分かります。

サーバー上でのキャプチャ結果

最後にサーバー上でのキャプチャ結果です。

完全にクライアントの存在が隠蔽されています。

SOCKSプロキシでhttp://....にアクセス

ここからSOCKSプロキシを使うパターンを見ていきます。

curl http://web --proxy socks5://proxy:1080

クライアント上でのキャプチャ結果

上記画像のNo4でSOCKSサーバーにConnectコマンドを発行している箇所の詳細です。

接続確立後にSOCKSサーバー経由でHTTPのリクエストを行なっている箇所の詳細です。

HTTPのメッセージがSOCKSでカプセル化されていることが分かります。

プロキシ上でのキャプチャ結果

次に、プロキシ上でのキャプチャ結果です。

上記画像のNo4でWebサーバーにHTTPのリクエストを行なっている箇所の詳細です。

SOCKSのカプセル化が外れていることが分かります。 この辺りの挙動はHTTPプロキシ経由でHTTPSのスキームにアクセスする時と良く似ていますね。

サーバー上でのキャプチャ結果

最後にサーバー上でのキャプチャ結果です。

上記画像のNo4でプロキシからHTTPのリクエストを受けている箇所の詳細です。

HTTPプロキシを経由する場合と比べると、HTTPヘッダーが少ないことが分かります。

SOCKSプロキシでhttps://....にアクセス

curl https://web --proxy socks5://proxy:1080 -k

クライアント上でのキャプチャ結果

上記画像のNo10でプロキシにTLSのHandshakeメッセージを送信している箇所の詳細です。

プロキシ上でのキャプチャ結果

次に、プロキシ上でのキャプチャ結果です。

上記画像のNo14でプロキシからサーバーにTLSのHandshakeメッセージを送信している箇所の詳細です。

SOCKSでのカプセル化が外れていることが分かります。

サーバー上でのキャプチャ結果

最後にサーバー上でのキャプチャ結果です。

上記画像のNo4でプロキシからTLSのHandshakeメッセージを受信している箇所の詳細です。

サーバーから見る限りは、HTTPプロキシを介してhttpsのスキームでアクセスされているのと同じような見え方です。

まとめ

ちょっとしたきっかけからSOCKSプロキシとHTTPプロキシの違いについて調査してみました。 SOCKSもHTTPも真新しい技術ではありませんが、こういった基本的なプロトコルに対する理解を深めるのは非常に有用だと考えています。 真新しい技術を追いかけるのも楽しいですが、たまには立ち止まって普段利用している技術を深掘りしていくような時間も作っていけたらと思います。

参考