理系学生日記

おまえはいつまで学生気分なのか

忍者TOOLS

gRPC と HTTP/2 と ALPN

gRPC の TLS を NLBで終端しようと思っていたらずいぶんとトラブってしまいました。 結局のところその原因は ALPN にあったのですが、ぼく自身が ALPN に詳しくないため、なかなかよくわからなかった。 このため、きちんと ALPN を理解しようというのが本エントリの趣旨になります。

ALPN

ALPN は Application-Layer Protocol Negotiation の略で、TLS の拡張です。RFC は RFC 7301 - Transport Layer Security (TLS) Application-Layer Protocol Negotiation Extension。 ALPN が何をしてくれる拡張かというと、TLS の上のアプリケーションレイヤで使用するプロトコルを Client - Server 間でネゴシエーションするための拡張になります。

Client は TLS の ClientHello メッセージの中で、自分のサポートできるプロトコルをリスト形式で Server に送ります。このリストは、自分が利用を望むプロトコル順になっています。 Server は、その中で Server 自身が対応しているプロトコルを選び、ServerHello メッセージの中でその選択結果を返却します。

RFC の中の図で説明すると、以下のようになります。まだコネクション確立前の段階なので、これらのネゴシエーションは平文で行われます。

  Client                                              Server

   ClientHello                     -------->       ServerHello
     (ALPN extension &                               (ALPN extension &
      list of protocols)                              selected protocol)
                                                   Certificate*
                                                   ServerKeyExchange*
                                                   CertificateRequest*
                                   <--------       ServerHelloDone
   Certificate*
   ClientKeyExchange
   CertificateVerify*
   [ChangeCipherSpec]
   Finished                        -------->
                                                   [ChangeCipherSpec]
                                   <--------       Finished
   Application Data                <------->       Application Data

じゃぁどういうアプリケーションプロトコルがこれらの選択肢に入るかというと、HTTP/1.1SPDY/3HTTP/2 over TLSHTTP/2 over TCP などがあります。

ALPN と HTTP/2

HTTP/2 の RFC (RFC 7540) には以下の記述があり、TLS で HTTP/2 を使おうとすると、ALPN が前提になっていることがわかります。

A client that makes a request to an "https" URI uses TLS with the application-layer protocol negotiation (ALPN) extension.

なぜ NLB 上で gRPC が使用できなかったのか

ここからは想像を含みます。

  1. NLB で TLS を終端しようとしていた
  2. そして NLB ではおそらく ALPN をサポートしていない。

というわけで、gRPC クライアントが NLB との ALPN のネゴシエーションで失敗していたということが理由なんだと理解しています。 訳がわからなかったのは、Go の gRPC クライアントでは成功したにもかかわらず node.js の gRPC のクライアントで失敗し、言語間で挙動が異なっていたということ。 これはおそらく、Go の gRPC クライアントのバグ。