はじめに
その1 (前編) に続いて、HTTP/2 の平文通信(h2c)をメジャーな以下のWebサーバで検証していきます。
- Nginx
- Apache HTTP Server
- Caddy
具体的には、Webサーバをリバースプロキシとして使う場合に、下流および上流にh2cを使えるかを調査します。
準備: Webサーバから上流(リバースプロキシ先)としてつなげるテスト用Webサーバの準備
Webサーバからリバースプロキシ先としてつなげるために、HTTP/1.1 および h2c (Upgrade/Prior Knowledge) で接続でき、接続プロトコルを返す以下のGoプログラムを用意しました。
package main
import (
"fmt"
"log"
"net/http"
"golang.org/x/net/http2"
"golang.org/x/net/http2/h2c"
)
func main() {
mux := http.NewServeMux()
mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
slog.Info("Request received", "Protocol", r.Proto, "Upgrade", r.Header.Get("Upgrade"), "Connection", r.Header.Get("Connection"))
fmt.Fprintf(w, "Protocol: %s, Upgrade: %s, Connection: %s\n", r.Proto, r.Header.Get("Upgrade"), r.Header.Get("Connection"))
})
h2s := &http2.Server{}
if err := http.ListenAndServe(":8888", h2c.NewHandler(mux, h2s)); err != nil {
log.Fatal(err)
}
}
http://localhost:8081 にリクエストを投げると、接続プロトコル名を答えてくれます。この記事ではWebサーバがこのプログラムにどんなプロトコルで接続しようとしたか判別するのに使います。
$ curl http://localhost:8888
Protocol: HTTP/1.1, Upgrade: , Connection:
検証は簡易のためにDockerにて検証します。上記プログラムを起動した上流として http://host.docker.internal:8888 をリバースプロキシ先に指定して検証します。
Nginx (1.29.1)
今や一番シェアが多い Nginx を以下のファイルとともに docker compose watch を使って検証していきます。
# docker compose watch で実行
services:
nginx:
image: nginx:1.29.1
ports:
- "8080:8080"
volumes:
- ./nginx.conf:/etc/nginx/nginx.conf:ro
develop:
watch:
- path: ./nginx.conf
action: restart
events {}
http {
server {
listen 8080;
# 下流からの h2c (Prior Knowledge) に対応
http2 on;
location / {
# 上流に h2c (Prior Knowledge) で接続するには grpc_pass を指定
grpc_pass http://host.docker.internal:8888;
}
}
}
curl で接続してみます。
# HTTP/1.1で接続
$ curl -v --http1.1 http://localhost:8080
< HTTP/1.1 200 OK
Protocol: HTTP/2.0, Upgrade: , Connection:
# HTTP/2 Upgradeで接続
$ curl -v --http2 http://localhost:8080
< HTTP/1.1 200 OK
Protocol: HTTP/2.0, Upgrade: , Connection:
# HTTP/2 Prior Knowledgeで接続
$ curl -v --http2-prior-knowledge http://localhost:8080
< HTTP/2 200
Protocol: HTTP/2.0, Upgrade: , Connection:
接続結果は以下のようになりました。基本的には Prior Knowledge はうまくいっているようです。
Nginxでは http2 on を使うことで下流から h2c (Prior Knowledge) で接続できます。HTTP/1.1と共存はできますが、そこからのUpgradeには対応できません。
また、grpc_pass を使うことで上流にh2c (Prior Knowledge) で接続できます。この grpc_pass という名前をどう思うかですが、おそらく本来ふさわしいであろう proxy_http_version は Nginx も FreeNginx も 1.0 か 1.1 としてしか指定できません。(パッチをあてれば 2.0 も指定できるようですが……)
Apache HTTP Server (2.4.65)
「Apacheを最後に仕事で触ったのはいつだろう?」そんなことを思いつつ、以下のファイルとともに docker compose watch を使ってApacheを起動して検証していきます。
# docker compose watch で実行
services:
apache:
image: httpd:2.4.65
ports:
- "8080:8080"
volumes:
- ./httpd.conf:/usr/local/apache2/conf/httpd.conf:ro
develop:
watch:
- path: ./httpd.conf
action: restart
# (中略)
LoadModule proxy_module modules/mod_proxy.so
LoadModule proxy_http_module modules/mod_proxy_http.so
LoadModule proxy_http2_module modules/mod_proxy_http2.so
LoadModule http2_module modules/mod_http2.so
# (中略)
# h2c を有効にする
Protocols h2c http/1.1
# h2c の場合はデフォルトで on になる
# H2Direct on
# 上流に h2c (Prior Knowledge) で接続する
ProxyPass / h2c://host.docker.internal:8888/
ProxyPassReverse / http://host.docker.internal:8888/
curl で接続してみます。
# HTTP/1.1で接続
$ curl -v --http1.1 http://localhost:8080
< HTTP/1.1 200 OK
Protocol: HTTP/2.0, Upgrade: , Connection:
# HTTP/2 Upgradeで接続
$ curl -v --http2 http://localhost:8080
< HTTP/2 200
Protocol: HTTP/2.0, Upgrade: , Connection:
# HTTP/2 Prior Knowledgeで接続
$ curl -v --http2-prior-knowledge http://localhost:8080
< HTTP/2 200
Protocol: HTTP/2.0, Upgrade: , Connection:
結果は以下のようになりました。Nginx と違い、 curl 側からの Upgrade に対応できています。
Apache HTTP Serverは ProxyPass ディレクティブで h2c:// スキームを指定することで、上流へh2c (Prior Knowledge) で接続します。
ApacheはHTTP/1.1とHTTP/2の両方に対応し、Upgradeメカニズムも正しく実装しています。Protocols h2c http/1.1 により、下流からのh2c接続を受け入れ、ProxyPass で h2c:// スキームを使用することで上流へのh2c接続を行うことができます。
ちなみに H2Direct は Prior Knowledge の PRI~~~ からなるバイトについて検知するためのディレクティブで、 Protocols h2c を指定するとデフォルトで on になりますが、あえて off にすると、 curl --http2-prior-knowledge での接続が失敗します。わざわざ指定する必要はないでしょう。
$ curl -v --http2-prior-knowledge http://localhost:8080
> GET / HTTP/2
> Host: localhost:8080
> User-Agent: curl/8.7.1
> Accept: */*
>
* Request completely sent off
* Remote peer returned unexpected data while we expected SETTINGS frame. Perhaps, peer does not support HTTP/2 properly.
* Closing connection
curl: (16) Remote peer returned unexpected data while we expected SETTINGS frame. Perhaps, peer does not support HTTP/2 properly.
Caddy (2.10.0)
私の記事ではおなじみの Caddy を以下のファイルとともに検証していきます。
# docker compose watch で実行
services:
caddy:
image: caddy:2.10.0
ports:
- "8080:8080"
volumes:
- ./Caddyfile:/etc/caddy/Caddyfile:ro
develop:
watch:
- path: ./Caddyfile
action: restart
{
servers :8080 {
# 以下の指定がない場合はHTTP/1.1からのUpgradeには対応しない (Prior Knowledgeでの接続は可能)
protocols h1 h2c
}
}
http://localhost:8080 {
# 上流に h2c (Prior Knowledge) で接続するには h2c:// を指定
reverse_proxy h2c://host.docker.internal:8888
}
curl で接続してみます。
# HTTP/1.1で接続
$ curl -v --http1.1 http://localhost:8080
< HTTP/1.1 200 OK
Protocol: HTTP/2.0, Upgrade: , Connection:
# HTTP/2 Upgradeで接続
$ curl -v --http2 http://localhost:8080
< HTTP/1.1 101 Switching Protocols
< Connection: Upgrade
< Upgrade: h2c
< HTTP/2 502
(失敗)
# HTTP/2 Prior Knowledgeで接続
$ curl -v --http2-prior-knowledge http://localhost:8080
< HTTP/2 200
Protocol: HTTP/2.0, Upgrade: , Connection:
結果は以下のようになりました。
curl -v --http2 のUpgrade方式での接続が失敗しています。おそらくクライアントから送られたUpgrade関連のヘッダを上流にそのまま流すことで誤動作をしているのでしょう。Goに送るヘッダから -Upgrade および -Connection を削除してみると接続できます。
http://localhost:8080 {
+ reverse_proxy h2c://host.docker.internal:8888 {
+ header_up -Upgrade
+ header_up -Connection
+ }
}
# HTTP/2 Upgradeで接続 (ヘッダ修正後)
$ curl -v --http2 http://localhost:8080
< HTTP/2 200
Protocol: HTTP/2.0, Upgrade: , Connection:
しかしまあ普通に考えたら、いちいちこういった細工をするのは気持ち悪いので「下流からh2cで繋ぐならPrior Knowledgeで接続するべし」と結論したほうがいいと思われます。
おまけ: Caddy 2.10.2 が Upgrade してくれないバグ
以下のファイルで接続を試験していたところ、Caddyのバージョンを2.10.0から2.10.2に上げたタイミングでUpgradeしてくれなくなりました。
{
servers :8080 {
protocols h1 h2c
}
}
http://localhost:8080 {
respond "Hello from Caddy"
}
# 〜 Caddy 2.10.0
$ curl -v --http2 http://localhost:8080/
< HTTP/2 200
Hello from Caddy!
# Caddy 2.10.2 (たぶんバグ)
$ curl -v --http2 http://localhost:8080/
< HTTP/1.1 200 OK
Hello from Caddy!
おそらく Caddy 2.10.2 の(もしくは依存Goのバージョンによる) バグと思われます。
おまけ: Caddyの設定確認
以下の設定など、 servers :8080 の記述をちょっと間違えると protocols h1 h2c の記述が効かないことがあります。
{
servers :8080 {
protocols h1 h2c
}
}
何故か効いていないと思ったら、Caddyの厳密なJSONでの設定を caddy adapt コマンドで確認するとよいでしょう。
# Dockerコンテナ上のCaddyで実行する場合
$ docker compose exec caddy caddy adapt --config /etc/caddy/Caddyfile
{"apps":{"http":{"servers":{"srv0":{"listen":[":8080"],"routes":[{"match":[{"host":["localhost
"]}],"handle":[{"handler":"subroute","routes":[{"handle":[{"body":"Hello from Caddy!","handler
":"static_response"}]}]}],"terminal":true}],"automatic_https":{"skip":["localhost"]},"protocol
s":["h1","h2c"]}}}}}
これに限らず、Caddyの最終的に反映されている設定を詳細に確認する場合はJSON表現をたどるのが確実です。
まとめ
Nginx, Apache HTTP Server, Caddy の3つのWebサーバで下流〜上流にh2cを使う場合の検証を行いました。
その1(前編) と同じ結論になりますが、 結局のところバックエンド間のh2c接続は
- h2c だけを使うと決め (HTTP/1.1 を有効にせず)
- とりわけPrior Knowledge 方式で接続する
のが相互運用性上一番いいという結論に尽きる と考えました。
バックエンドサービスの相互接続では HTTP/1.1 から Upgrade する余地は忘れましょう。Prior Knowledge だけを使いましょう。