Istio サイドカープロキシが WebSocket トラフィックをインターセプトすると、Google Chrome は期待されるデフォルトのリターンコード(1005)やカスタムリターンコードではなく、リターンコード 1006 を受信する場合があります。Firefox および Safari には影響ありません。この問題を解決するには、EnvoyFilter で delayed_close_timeout: 0s を設定します。
原因
Envoy サイドカープロキシの HTTP 接続マネージャーは、接続を正常に終了(グレースフルシャットダウン)するために delayed_close_timeout を使用します。WebSocket のクローズハンドシェイク中にこのタイムアウトが発生すると、Chrome がクローズフレームを受信する前に Envoy が接続をリセットしてしまうため、"wasclean": false およびリターンコード 1006 が返されます。他のブラウザでは接続タイミングの処理方法が異なり、正しいリターンコードが受信されます。
前提条件
開始する前に、以下の条件を満たしていることを確認してください。
ACK マネージドクラスター。詳細については、「ACK マネージドクラスターの作成」をご参照ください。
Service Mesh (ASM) インスタンス(ACK クラスターが追加済み)。詳細については、「ASM インスタンスの作成」および「ASM インスタンスにクラスターを追加する」をご参照ください。
少なくとも 1 つのイングレスゲートウェイサービスがデプロイ済みであること。詳細については、「イングレスゲートウェイの作成」をご参照ください。
ASM インスタンスにデプロイされたアプリケーション。 詳細については、「ASM インスタンスにアプリケーションをデプロイする」をご参照ください。
kubectl が ACK クラスターおよび ASM インスタンスの両方に接続できるように設定済みであること。詳細については、「kubectl を使用した ACK クラスターへの接続」および「コントロールプレーン上で kubectl を使用して Istio リソースにアクセス」をご参照ください。
ステップ 1:サンプル WebSocket アプリケーションのデプロイ
問題を再現するために WebSocket サーバーをデプロイします。以下のいずれかの方法を選択してください。
オプション A:事前ビルド済みの Alibaba Cloud イメージを使用
以下の内容で
sample.yamlというファイルを作成します。apiVersion: apps/v1 kind: Deployment metadata: name: websocket-test namespace: default labels: app: websocket-test version: current spec: replicas: 1 selector: matchLabels: app: websocket-test version: current template: metadata: labels: app: websocket-test version: current spec: containers: - name: websocket-test image: registry.cn-hangzhou.aliyuncs.com/aliacs-app-catalog/asm-websocketsample:v1 imagePullPolicy: Always command: ["node", "ws.js"] --- apiVersion: v1 kind: Service metadata: labels: app: websocket-test name: websocket-test namespace: default spec: ports: - name: http port: 80 protocol: TCP targetPort: 9900 selector: app: websocket-test type: ClusterIPマニフェストを
default名前空間に適用します。default名前空間に対して自動サイドカープロキシ注入が有効化されている必要があります。詳細については、「自動サイドカープロキシ注入の有効化」をご参照ください。kubectl apply -f sample.yaml
オプション B:カスタム Docker イメージのビルド
Node.js アプリケーション用の
package.jsonファイルを作成します。{ "name": "wssample", "version": "0.0.1", "main": "ws.js", "license": "UNLICENSED", "scripts": { "start": "node --trace-warnings ./ws.js" }, "dependencies": { "ws": "^8.0.0" } }WebSocket サーバーのロジックを含む
ws.jsファイルを作成します。const WebSocket = require("ws"); const http = require("http"); const wss = new WebSocket.Server({ noServer: true }); const server = http.createServer() server.on("upgrade", async (request, socket, head) => { const handleAuth = (ws) => { wss.emit("connection", ws, request); }; wss.handleUpgrade(request, socket, head, handleAuth); }) wss.on("connection", (conn, req) => { // デフォルトのクローズではリターンコード 1005 が送信されます。 // サイドカープロキシ注入が有効な場合、Chrome では代わりに 1006 が報告されます。 // conn.close() // カスタムリターンコード 4321。 // サイドカープロキシ注入が有効な場合、Chrome では依然として 1006 が報告されます。 // サイドカープロキシ注入が無効な場合、Chrome では正しく 4321 が報告されます。 conn.close(4321, "test") }); server.listen({ host: '0.0.0.0', port: 9900 });Dockerfileを作成します。FROM node:16.7.0-alpine3.14 WORKDIR /root/app COPY . . RUN yarn installイメージをビルドし、オプション A と同様の Deployment および Service を使用してデプロイします。
imageフィールドをカスタムイメージのレジストリパスに置き換えます。
ステップ 2:ASM インスタンスの設定
イングレスゲートウェイを経由して WebSocket アプリケーションにトラフィックをルーティングするため、Gateway、DestinationRule、および VirtualService を作成します。
ASM コンソール にログインします。
左側ナビゲーションウィンドウで、Service Mesh > Mesh Management を選択します。
Mesh Management ページで、対象の ASM インスタンスの名前をクリックするか、Actions 列の Manage をクリックします。
Gateway の作成
左側ナビゲーションウィンドウで、ASM Gateways > Gateway を選択し、Create from YAML をクリックします。
Namespace を default に設定し、シナリオテンプレートを選択して、以下の YAML を貼り付け、Create をクリックします。
apiVersion: networking.istio.io/v1beta1 kind: Gateway metadata: name: websocket-test namespace: default spec: selector: istio: ingressgateway servers: - hosts: - '*' port: name: http number: 80 protocol: HTTP
DestinationRule の作成
左側ナビゲーションウィンドウで、Traffic Management Center > DestinationRule を選択し、Create from YAML をクリックします。
Namespace を default に設定し、シナリオテンプレートを選択して、以下の YAML を貼り付け、Create をクリックします。
apiVersion: networking.istio.io/v1alpha3 kind: DestinationRule metadata: name: websocket-test namespace: default spec: host: websocket-test subsets: - name: current labels: version: current
VirtualService の作成
左側ナビゲーションウィンドウで、Traffic Management Center > VirtualService を選択し、Create from YAML をクリックします。
Namespace を default に設定し、シナリオテンプレートを選択して、以下の YAML を貼り付け、Create をクリックします。
apiVersion: networking.istio.io/v1alpha3 kind: VirtualService metadata: name: websocket-test namespace: default spec: gateways: - websocket-test hosts: - '*' http: - name: default route: - destination: host: websocket-test subset: current
ステップ 3:問題の再現
シンプルな HTML クライアントを使用して、Chrome が期待されるリターンコードではなく 1006 を返すことを確認します。
以下の内容で
client.htmlファイルを作成します。<ingress-gateway-ip>をご利用のイングレスゲートウェイの IP アドレスに置き換えてください。<!DOCTYPE html> <html> <head> <title>WebSocket の例</title> </head> <body> <script> var ws = new WebSocket('ws://<ingress-gateway-ip>'); ws.onopen = function (ev) { console.log(ev) }; ws.onmessage = function (ev) { console.log("on msg", ev) }; ws.onclose = function (ev) { console.log("on close", ev) }; ws.onerror = function (ev) { console.log("on error", ev) }; </script> </body> </html>Google Chrome で
client.htmlを開き、F12 を押して開発者ツールを開きます。ページを更新し、Console タブを確認します。期待されるカスタムリターンコード
4321ではなく、リターンコード1006が表示されます。
ステップ 4:EnvoyFilter による修正の適用
Envoy HTTP 接続マネージャーの delayed_close_timeout を 0s に設定することで、サイドカープロキシが WebSocket クローズフレームに干渉するのを防ぎます。
以下の内容で EnvoyFilter を作成します。この EnvoyFilter は、すべての Envoy サイドカー(プロキシバージョン
^1.*.*)に対し、HTTP 接続マネージャーのネットワークフィルターにdelayed_close_timeout: 0s設定をマージするパッチを適用します。apiVersion: networking.istio.io/v1alpha3 kind: EnvoyFilter metadata: labels: asm-system: 'true' provider: asm name: hack-to-fix-delayedclosetimeout-istio-upper-version namespace: istio-system spec: configPatches: - applyTo: NETWORK_FILTER match: listener: filterChain: filter: name: envoy.filters.network.http_connection_manager proxy: proxyVersion: ^1.*.* patch: operation: MERGE value: typed_config: '@type': >- type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager delayed_close_timeout: 0sEnvoyFilter を対象のワークロードまたは名前空間にバインドします。詳細については、「ワークロードまたは名前空間への Envoy フィルターテンプレートのバインド」をご参照ください。
ステップ 5:修正の検証
Google Chrome で
client.htmlを開き、F12 キーを押してよく使うツールを開きます。ページを更新し、Console タブを確認します。リターンコードが WebSocket サーバーで設定したカスタムコード
4321と一致するようになりました。