プロキシを使用する際、開発者はしばしば従来のHTTPプロキシ設定がWebSocket接続に対して機能しないことや、gRPCがTLSハンドシェイクエラーで失敗することに直面します。問題は、WebSocket、HTTP/2、gRPCが単なる「HTTPのバリエーション」ではなく、トンネリング、マルチプレクシング、ヘッダー処理に対する独自の要件を持つ異なるトランスポートモデルであることです。この記事では、これらのプロトコルをどのようにプロキシングするか、実際に遭遇する落とし穴、特定のタスクに対してプロトコルとプロキシのタイプをどのように選択するか(WebSocketを介したパースから高負荷のgRPCマイクロサービスまで)を解説します。
プロトコルの違いとそれがプロキシにとって重要な理由
HTTP/1.1は「リクエスト-レスポンス」モデルで動作します:クライアントは接続を開き、リクエストを送信し、レスポンスを受け取り、接続を閉じるか、次のリクエストのために接続を維持します(keep-alive)。プロキシサーバーは数十年にわたりこのモデルに最適化されており、ヘッダーのパース、リクエストボディのバッファリング、Hostヘッダーによる簡単なルーティングが行われています。
WebSocketはこのモデルを破ります:初期のHTTPハンドシェイク(Upgrade: websocket)の後、接続は永続的な双方向チャネルに変わり、データはいつでも両方向に流れることができます。プロキシはこの接続を「解放」し、単にバイトを両方向に転送する必要があり、HTTPリクエストとして解釈しようとしないでください。
HTTP/2はマルチプレクシングを追加します — 複数の論理リクエストが1つのTCP接続を介して並行して行われ、テキストヘッダーの代わりにバイナリフレームを使用します。プロキシにとっては、単にテキストを行ごとに読むことはできず、プロキシレベルでのHTTP/2バイナリプロトコルのサポートが必要です。または、TLS上の透過的なTCPトンネリングが必要です。
gRPCはさらに進んでいます:HTTP/2に厳密に依存し、シリアル化にはProtocol Buffersを使用し、クライアントからサーバーまでのすべての経路でHTTP/2互換のトランスポートを要求します。プロキシがどこかの段階で接続をHTTP/1.1に「ダウングレード」すると、gRPCリクエストは単に通過しません。
これらの違いを理解することは重要です。なぜなら、間違ったタイプのプロキシを選択したり、トンネリングの設定を誤ったりすると、接続の切断、タイムアウト、診断が難しい説明できないエラーが発生するからです。これらはトランスポートレベルを理解しないと解決が難しいです。
プロキシ経由のWebSocket:設定とコード
プロキシ経由のWebSocketには、主に2つのシナリオがあります:WSS(TLSによるWebSocket)のためのCONNECTメソッドを使用するHTTPプロキシと、プロトコルを問わずTCP接続をトンネリングするSOCKS5プロキシです。SOCKS5は通常、最初から「透明なパイプ」として設計されているため、問題が少ないです。
PythonでのSOCKS5プロキシを介したWebSocket接続の例は、websocketsライブラリとpython-socksを使用しています:
import asyncio
from python_socks.sync import Proxy
import websockets
import websockets.sync.client as ws_client
def connect_ws_via_proxy():
proxy = Proxy.from_url("socks5://user:pass@proxy_host:1080")
sock = proxy.connect(dest_host="echo.websocket.events", dest_port=443)
ws = ws_client.connect(
"wss://echo.websocket.events",
sock=sock,
server_hostname="echo.websocket.events"
)
ws.send("Hello via proxy")
print(ws.recv())
ws.close()
connect_ws_via_proxy()
Node.jsでは、同様のタスクがwsパッケージとsocks-proxy-agentを使用して解決されます:
const WebSocket = require('ws');
const { SocksProxyAgent } = require('socks-proxy-agent');
const agent = new SocksProxyAgent('socks5://user:pass@proxy_host:1080');
const socket = new WebSocket('wss://echo.websocket.events', { agent });
socket.on('open', () => socket.send('Hello via proxy'));
socket.on('message', (data) => console.log(data.toString()));
プロキシがHTTP(CONNECTメソッド)でのみ利用可能な場合、スキームは類似しています — ほとんどの現代のWebSocketクライアントはHTTPトンネルを介して動作できるため、プロキシが長寿命の接続をタイムアウトで切断しないことを確認することが重要です。これは、非アクティブな接続に対して30〜60秒の厳しい制限が設定されている安価なデータセンタープロキシでよく見られる問題です。ストリーミングWebSocketタスクにとっては重要であり、プロバイダーにkeep-aliveのパラメータを確認することをお勧めします。
プロキシ経由のHTTP/2:マルチプレクシングと落とし穴
プロキシ経由のHTTP/2の主な難しさは、多くのプロキシサーバー(特に古いSquidや単純なフォワードプロキシ)がHTTP/1.1でのみ動作し、接続を自動的に「ダウングレード」することです。クライアントにはこれがしばしば目立たない(リクエストは依然として実行される)が、マルチプレクシングの利点が失われます — 特に同じホストへの並行リクエストが多い場合、遅延が増加します。
プロキシ経由でHTTP/2の接続をサポートしているかどうかを確認するには、cURLを使用して--http2フラグと詳細な出力を使用します:
curl -v --http2 -x http://user:pass@proxy_host:8080 https://example.com
# 出力で次の行を探します:
# * Using HTTP2, server supports multiplexing
# それがない場合、接続はHTTP/1.1にダウングレードされました。
Pythonでプロキシ経由のHTTP/2を使用するには、httpxライブラリが必要で、HTTP/2サポートが有効になっている必要があります:
import httpx
proxies = {
"http://": "http://user:pass@proxy_host:8080",
"https://": "http://user:pass@proxy_host:8080",
}
with httpx.Client(proxies=proxies, http2=True) as client:
response = client.get("https://example.com")
print(response.http_version) # "HTTP/2"を期待
print(response.status_code)
重要な点:HTTP/2はALPN拡張を使用してプロトコルを交渉するため、プロキシはCONNECTを介してTLSトラフィックを透過的にトンネリングする必要があります。そうでなければ、TLSを自分で終了させてはいけません(専門のHTTP/2互換プロキシでない限り)。そのため、HTTP/2のタスクには、レジデンシャルプロキシとデータセンタープロキシが異なる動作をすることがあります — プロバイダーにALPN交渉の中断なしに透過的なTLSトンネリングがサポートされているかどうかを確認することが重要です。
プロキシ経由のgRPC:TLS、ALPN、特別な要件
gRPCはこの組み合わせの中で最も要求が厳しいプロトコルです。これはHTTP/2に厳密に依存し、認証のために双方向TLS(mTLS)を頻繁に使用します。HTTP/2 CONNECTトンネリングをサポートしていない通常のフォワードプロキシは、gRPCトラフィックを通過させず、接続はUNAVAILABLE: upstream connect errorのようなエラーで失敗します。
PythonでgRPCをプロキシするには(grpcライブラリを使用)、プロキシは環境変数を介して設定されます。なぜなら、channel optionsに組み込みのプロキシサポートが歴史的に存在しなかったからです:
import os
import grpc
os.environ["grpc_proxy"] = "http://user:pass@proxy_host:8080"
os.environ["https_proxy"] = "http://user:pass@proxy_host:8080"
channel = grpc.secure_channel(
"grpc.example.com:443",
grpc.ssl_channel_credentials()
)
# 生成されたスタブを介した呼び出しの例
# stub = YourServiceStub(channel)
# response = stub.YourMethod(request)
Node.jsでは、gRPCのプロキシはgrpc-jsのチャネルオプションを介して設定され、HTTP/2互換のプロキシエージェントと組み合わせて使用されます:
const grpc = require('@grpc/grpc-js');
const { HttpsProxyAgent } = require('https-proxy-agent');
process.env.grpc_proxy = 'http://user:pass@proxy_host:8080';
process.env.https_proxy = 'http://user:pass@proxy_host:8080';
const client = new YourServiceClient(
'grpc.example.com:443',
grpc.credentials.createSsl()
);
プロキシ経由のgRPCの安定した動作には、低遅延と安定したTCP接続が重要です。マルチプレクシングされたgRPCストリームは、個別のHTTP/1.1リクエストよりもネットワーク障害に対して敏感です。このようなシナリオでは、レジデンシャルプロキシが安価なデータセンタープールよりも安定性が高いことがよくあります。なぜなら、ターゲットサーバーへのルートがより予測可能なネットワークインフラストラクチャを通過するからです。
プロトコルの比較表
| プロトコル | 接続タイプ | プロキシの要件 | 推奨プロキシタイプ |
|---|---|---|---|
| HTTP/1.1 | リクエスト-レスポンス、keep-alive | 最小限、任意のHTTPプロキシ | データセンタープロキシ |
| WebSocket | 永続的な双方向チャネル | 長期接続のサポート、タイムアウトなし | レジデンシャルプロキシ |
| HTTP/2 | マルチプレクシングされたバイナリストリーム | 透過的なTLSトンネリング、ALPN | レジデンシャルまたはデータセンタープロキシ |
| gRPC | HTTP/2 + Protobuf、しばしばmTLS | 低遅延、安定性、HTTP/2 CONNECT | 厳しいフィルタを回避するためのモバイルプロキシ |
自分のスタックに合ったプロトコルとプロキシの選び方
プロトコルの選択は、自由な決定であることは稀で、ターゲットAPIによって決定されます。しかし、このプロトコルに対するプロキシのタイプの選択はあなたの責任であり、具体的なシナリオに基づいて判断する必要があります:
WebSocketを介したデータのパース(例えば、マーケットプレイスのサイトからのリアルタイムデータやストリーミングの価格情報)は、タイムアウトによる切断なしで安定した長期接続を必要とします。ここでは、レジデンシャルプロキシがより効果的です — 彼らはターゲットサービスのアンチフロードアルゴリズムに引っかかりにくく、接続をより長く維持します。
外部プロキシを介したgRPCマイクロサービスとの統合(例えば、異なる地理的場所からAPIをテストする場合)は、低遅延とHTTP/2のサポートがすべての経路で必要です。これには、良好なルーティングを持つレジデンシャルIPが適しており、時間に敏感なタスクには、ターゲットサービスがデータセンターの範囲を積極的にフィルタリングする場合にモバイルプロキシが適しています。
REST/GraphQL APIへの大量のHTTP/2リクエスト(マルチプレクシングを含む)は、ターゲットサービスがデータセンターのIP範囲をデフォルトでブロックしない限り、速くて安価なデータセンタープロキシで十分です。
一般的なルール:プロトコルが接続の安定性に「気難しい」ほど(WebSocket、gRPC)、レジデンシャルまたはモバイルIPの意味が増します。リクエストが単純で短いほど(通常のHTTP/1.1または長期セッションのないHTTP/2)、データセンタープロキシで作業する方が快適です — 彼らはより速く、より高いスループットを提供します。
一般的なエラーとその解決策
エラー1:WebSocketが30〜60秒ごとに切断される。 原因は、プロキシまたは中間のロードバランサーが「非アクティブ」な接続を閉じることです。解決策:アプリケーションレベルでping/pongフレームを有効にし、プロキシのタイムアウトよりも短い間隔で送信します(通常20〜25秒で十分です)。
エラー2:gRPCがプロキシ経由でUNAVAILABLEで失敗し、直接は動作する。 プロキシがHTTP/2 CONNECTトンネリングをサポートしていません。解決策:プロバイダーのドキュメントを確認してHTTP/2のサポートを確認するか、プロトコルを解釈せずにTCPをトンネリングするSOCKS5プロキシを使用します。
エラー3:HTTP/2が目に見えずHTTP/1.1にダウングレードされる。 これはしばしばプロキシでALPNサポートが欠如しているために発生します。コード内でプロトコルのバージョンを明示的に確認してください(上記のhttpxの例のように) — レスポンスのhttp_versionを確認しなければ「すべてが動作している」とは信じないでください。
エラー4:プロキシ経由でgRPCのマルチプレクシング時に高遅延。 これはしばしばプロキシプロバイダーの中間ノードの数が多いことが原因です。解決策:単純なRTTリクエストを介してレイテンシを事前にテストし、特に時間に敏感なgRPC呼び出しのために直接ルーティングを持つプロバイダーを選択します。
結論
WebSocket、HTTP/2、gRPCは、異なるトランスポートモデルで動作するため、プロキシの設定に対して異なるアプローチを必要とします:WebSocketのための単純な「TCP接続を解放する」から、gRPCのためのHTTP/2 CONNECTとALPNの厳密なサポートまで。コード内でプロトコルのバージョンを明示的に確認し、長期接続の安定性を事前にテストし、最大のトンネリングの透明性が必要な場合はSOCKS5を選択してください。
あなたのスタックに長寿命のWebSocket接続や遅延に敏感なgRPC呼び出しが含まれている場合は、レジデンシャルプロキシを試すことをお勧めします — 彼らは典型的なデータセンタープールと比較して、より安定した予測可能な接続を提供します。単純な高スループットのHTTP/2リクエストには、データセンタープロキシが適しており、ターゲットサービスの厳しいアンチフロードフィルタリングに対しては、モバイルプロキシが適しています。