【実務・中級編】HTTP/2におけるALPN (Application-Layer Protocol Negotiation) の役割 – HTTPプロトコル・通信規格実践ガイド

HTTP/2時代の黒衣:ALPN(Application-Layer Protocol Negotiation)がTLSハンドシェイクの裏側でやっていること

こんにちは。インフラエンジニアとして幾多のWebサービスのローンチを見守り、時には深夜の障害対応で冷や汗を流してきたシニアネットワークアーキテクトです。

さて、皆さんは日々の開発で「HTTP/2」や、さらにその先の「HTTP/3」の恩恵を当たり前のように受けていることでしょう。マルチプレクシングによる頭出しブロック(Head-of-Line Blocking)の解消、HPACKによるヘッダー圧縮など、ブラウザを開けばサクサクとリソースが降ってくる。本当に素晴らしい時代になりました。

しかし、ふと立ち止まって考えてみてください。
ブラウザ(クライアント)とWebサーバーが初めて通信を開始するその瞬間、「お互いにHTTP/2で喋りましょう」という合意は、一体いつ、どうやって行われているのでしょうか?

「えっ、HTTPのヘッダーに何か書くんじゃないの?」と思った方、惜しい。実は、HTTPの通信が始まるはるか手前、暗号化のハンドシェイクの最中に、その勝負はすでに決まっているのです。

今回は、その立役者である ALPN(Application-Layer Protocol Negotiation) について、パケットの動きや実務での設定、そして現場で役立つデバッグ手法まで、徹底的に深掘りしていきましょう。

—

1. なぜALPNが必要なのか?(歴史的背景と仕組み)

かつてのHTTP/1.1の時代はシンプルでした。TCPの3wayハンドシェイクが終わったら、そのままプレーンテキスト(あるいはTLSを挟んで暗号化)で `GET / HTTP/1.1` と投げればよかったのです。

しかし、HTTP/2が登場したとき、大きな制約が立ちはだかりました。
「HTTP/2は、原則として暗号化(TLS 1.2以降)が必須(実質的な要件)」 であり、かつTCPの上で独自フレーミングを行うため、従来のHTTP/1.1とはプロトコルのバイナリ構造が全く異なります。

ここで問題が発生します。
サーバー側は「HTTP/1.1もHTTP/2も両方喋れますよ」と言っている。クライアント側も「両方いける口です」というとき、最初のパケットを投げる前に「今回はどのプロトコルを使おうか?」とネゴシエーション(交渉)しなければ、サーバー側がパケットを正しく解釈できません。

従来の「NPN」から「ALPN」への進化

実は、HTTP/2策定の初期には NPN(Next Protocol Negotiation) という仕組みが使われていました。これはサーバー側が「私はこれに対応しています」とクライアントに提示し、クライアントが決める方式でした。しかし、これには「TLSの拡張としてセキュリティ上・アーキテクチャ上のエレガントさに欠ける」という問題がありました。

そこで標準化されたのが、RFC 7301で定義された ALPN(Application-Layer Protocol Negotiation) です。

  • NPN: サーバーがクライアントに候補を教える(Server-driven)
  • ALPN: クライアントがサーバーに「これを話したいのですが」と候補のリストを提示し、サーバーがその中から一つを選ぶ(Client-driven)

この主客逆転が非常に重要です。ALPNは、TLSハンドシェイクの「Client Hello」と「Server Hello」という拡張領域(Extension)の中で行われます。 つまり、TCPの確立から暗号化の確立(TLSハンドシェイク)という一連のプロセスの中で、アプリケーション層のプロトコルまで同時に決めてしまうわけです。無駄な往復ラウンドトリップ(RTT)を増やさない、非常に洗練された仕組みと言えます。

—

2. パケットで見るALPNのネゴシエーション実態

百聞は一見にしかず。WiresharkなどでTLSハンドシェイクのパケットをキャプチャすると、ALPNがどのようにやり取りされているか一目瞭然です。

① Client Hello(クライアントの主張)

クライアントはTLS接続を開始する際、`extension_application_layer_protocol_negotiation (16)` という拡張フィールドに、自分が対応しているプロトコルIDのリストを詰め込んで送信します。

HTTP/2に対応しているモダンなクライアント(ブラウザや最新のHTTPクライアント)の場合、このリストには以下のようなIDが含まれます。

  • `h2` (HTTP/2 over TLS)
  • `http/1.1` (フォールバック用のHTTP/1.1)

② Server Hello(サーバーの選択)

サーバーは、クライアントから送られてきたリストを受け取り、自身がサポートし、かつ優先したいプロトコルを1つだけ選んで `Server Hello` で返します。

もしサーバーがHTTP/2に対応しており、設定も有効であれば、サーバーは以下のように返答します。

  • 選択されたプロトコルID: `h2`

この瞬間、暗号化のトンネルが掘られると同時に、「このTLSセッションの上では、HTTP/2のフレームを流すぞ」という合意が完了します。もしサーバーがHTTP/2を知らない、あるいは無効化されている場合は `http/1.1` が選ばれ、古典的なHTTP/1.1のやり取りにフォールバックします。

—

3. 実務で直面する「ALPNの必須要件」とインフラ設定

現場のインフラエンジニアとして最も注意しなければならないのは、「HTTP/2を使いたいなら、暗号化(TLS)の要件を厳格に満たさなければならない」という点です。単に「NginxやApacheでHTTP/2を有効にした」だけでは動きません。ブラウザやクライアントライブラリは、ALPNで `h2` の合意が取れないと、問答無用でHTTP/1.1にフォールバックします。

ここからは、実務でよく使われる代表的なWebサーバー・リバースプロキシの設定例を見ていきましょう。

A. Nginx の設定例

NginxでHTTP/2とALPNを有効にするのは非常にシンプルですが、TLSの暗号スイート(Cipher Suites)の要件に注意が必要です。HTTP/2のスペック(RFC 7540)では、ブラックリストに指定されている脆弱な暗号スイートを使用すると、クライアント側からプロトコルエラー(INADEQUATE_SECURITY)として切断される規定があります。

server {
listen 443 ssl http2; # ※Nginx 1.25.1以降では “listen 443 ssl;” とし、別途 “http2 on;” を書く仕様に変わっています
server_name api.example.com;

# SSL証明書の指定
ssl_certificate /path/to/fullchain.pem;
ssl_certificate_key /path/to/privkey.pem;

# 最新かつHTTP/2で許可されている強固なTLSプロトコルと暗号スイートを指定
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers ‘ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384’;
ssl_prefer_server_ciphers off;

# ※ Nginxは設定が適切であれば、自動的にALPNで “h2” と “http/1.1” をネゴシエーションします

location / {
proxy_pass http://127.0.0.1:8080;
proxy_http_version 1.1; # バックエンドとの通信はHTTP/1.1を使うことが多い
proxy_set_header Connection “”;
}
}

B. Go言語 (net/http) によるサーバー実装例

マイクロサービスやAPIサーバーをGo言語で自製する場合、標準の `net/http` パッケージは、TLS設定を適切に行うだけで自動的にALPN(`h2`)をサポートしてくれます。

package main

import (
“fmt”
“net/http”
)

func handler(w http.ResponseWriter, r http.Request) {
// リクエストがどのプロトコル(HTTP/1.1 or HTTP/2)で来ているかを確認
fmt.Fprintf(w, “Hello, HTTP/2! Your protocol was: %s\n”, r.Proto)
}

func main() {
mux := http.NewServeMux()
mux.HandleFunc(“/”, handler)

// Goのhttp.Serverは、TLSConfigを設定すれば自動的にALPN (h2) が有効になります
// ※実際の運用では適切なcrypto/tlsの設定を行ってください
server := &http.Server{
Addr: “:443”,
Handler: mux,
}

fmt.Println(“Starting HTTPS server with HTTP/2 support on :443…”)
// 自己証明書等を用いたテスト用 (実際にはValidな証明書パスを指定)
err := server.ListenAndServeTLS(“server.crt”, “server.key”)
if err != nil {
panic(err)
}
}

—

4. 現場のトラブルシューティング:ALPNが失敗する原因とデバッグ手法

「サーバー側でHTTP/2を有効にしたはずなのに、なぜかクライアントから見るとHTTP/1.1で通信している……」
これは、インフラ構築の現場で本当によくあるトラブルです。大抵の場合、原因はALPNのネゴシエーション失敗にあります。

ここからは、私が現場で使っている具体的なデバッグ手順を伝授しましょう。

手順1: `curl` を使ってALPNのネゴシエーション結果を暴く

一番手っ取り早いのは、デバッグフラグを立てた `curl` コマンドを実行することです。`-v`(verbose)オプションに加え、どのプロトコルで接続できたかを確実に見ます。

curl -iv –http2 https://api.example.com/healthz

【チェックすべきポイント】
出力されたログの中に、以下のような記述があるか確認します。

  • ALPN: offers h2, http/1.1
  • ALPN: server accepted h2

もしここが `server accepted http/1.1` になっている場合、サーバー側が `h2` を拒否している、あるいはALPNのリストに `h2` を含めていない証拠です。

手順2: OpenSSLコマンドで直接サーバーのALPN対応状況を叩く

「Nginxやロードバランサーの前にいるファイアウォールやWAFが邪魔をしているのでは?」と疑うときは、`openssl s_client` を使って直接TLSレイヤーのALPNネゴシエーションをテストできます。

openssl s_client -connect api.example.com:443 -alpn h2

接続が成功した後、コンソールに出力される情報の中に、以下の一行があるか確認してください。

ALPNNegotiated: h2

もしここが `none` になっていたり、エラーで弾かれる場合は、サーバーのTLS設定、あるいは暗号スイートのミスマッチが疑われます。

よくあるハマりポイント

1. 古いOpenSSL / 移設前のライブラリを使っている
OSやミドルウェアが古く、ALPN自体をサポートしていないケース(特に古いRHEL/CentOS 7系などで無理やり新しいソフトを動かそうとした時など)。
2. 証明書の不備や不適切な暗号スイート
前述の通り、HTTP/2仕様で禁止されている暗号スイート(RC4など)を有効にしていると、厳格なクライアントはALPNで `h2` を合意してくれません。
3. プロキシ・WAF・CDNによる終端
CloudflareやAWS CloudFrontなどのCDNを挟んでいる場合、オリジンサーバー(EC2など)とCDN間の通信、あるいはクライアントとCDN間の通信のどちらでHTTP/2を期待しているのかを整理する必要があります(通常、CDNとクライアント間はALPN `h2` が使われますが、オリジン側はHTTP/1.1であることが多々あります)。

—

5. まとめ

HTTP/2のマルチプレクシングや高速な通信の裏側には、TLSハンドシェイクという極めて限られた瞬間に行われる ALPN(h2)の静かなる合意 が存在します。

普段私たちが何気なく叩いているAPIや、ブラウザで表示するリッチなWebアプリケーションは、このTCP/TLS/ALPNという幾重ものプロトコルの積み重ねの上手 위에成り立っています。

もし今後、「なぜかHTTP/2にならない」「パフォーマンスが出ない」という壁にぶぶかったときは、慌ててアプリケーションコードを見る前に、まずは 「ALPNでちゃんと `h2` が握れているか」 を `curl` や `OpenSSL` で疑ってみてください。ネットワークの視点を持っていれば、トラブルシューティングの時間は驚くほど短縮されるはずです。

皆さんのインフラライフが、快適な `h2` のストリームで満たされることを祈っています!

コメント

タイトルとURLをコピーしました