14
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

h2c (HTTP/2 平文) で通信してみた 【その1 〜 バックエンド(Go/Node.js/Python)サービス編】

Last updated at Posted at 2025-09-11

はじめに

先日(2025-08)、HTTP/1.1の電文が混ざってしまうdesyncやsmugglingと呼ばれる脆弱性を告発(?)するサイトが話題になっていました。

データフレームを用いずテキストフォーマットで電文の切れ目を示す HTTP/1.1が根本的に脆弱性があるので、CDNなどのフロントに限らず、オリジン(バックエンド)においても安全なバイナリフォーマットベースのHTTP/2以上をリバースプロキシは使うべきという趣旨のようです。

ただし、HTTP/2は通常TLSを前提としているのでバックエンドサービス間の通信にTLSを導入するのは管理が大変です。

そこで本記事では、手軽にHTTP/1.1から脱却するため VPC内などバックエンド間でh2c (HTTP/2 Cleartext = HTTP/2 平文) で通信できるかを調べてみました。

まずはバックエンドサービス編として以下の言語で検証を行ってみます。

  • Go言語: golang.org/x/net/http2/h2c
  • Node.js: node:http2
  • Python: hypercorn

前提知識: h2c (HTTP/2 Cleartext) の接続方法とクライアントの対応状況について

h2cの接続方法には以下の2種類があります。

  • Prior Knowledge 方式: HTTP/2接続を直接開始する方式。クライアントは接続開始時にPRI * HTTP/2.0\r\n\r\nSM\r\n\r\nという24バイトのマジック文字列(Connection Preface)を送信してHTTP/2通信を開始する
  • Upgrade (ネゴシエーション) 方式: HTTP/1.1からh2cへアップグレードする方式。クライアントはHTTP/1.1リクエストにUpgrade: h2cヘッダーとHTTP2-Settingsヘッダーを付けて送信し、サーバーが101 Switching Protocolsで応答すればh2cに切り替わる

h2cに対応していないクライアントやプロキシの互換性を考えるとUpgrade方式が望ましいですが、バックエンドサーバーが本当にこれに対応するべきかは場合によるでしょう。

クライアントの対応状況は以下の通りです。

  • curl: h2cおよび両方式に対応
    • --http2-prior-knowledge: Prior Knowledge方式を使用
    • --http2: Upgrade方式を使用
  • メジャーブラウザ(Chromeなど): そもそもh2cは未対応
    • TLS接続時のALPN(Application Layer Protocol Negotiation)中にh2の利用可否をネゴシエートし、TLS確立後にいきなりHTTP/2通信(Prior Knowledge方式)を行います

基本的にh2cの検証にメジャーブラウザは使えないので、curlで検証していきます。

Go言語での検証

特に苦労なく golang.org/x/net/http2/h2c パッケージを使うことでh2cサーバを立ち上げることができます。

main.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) {
		fmt.Fprintln(w, "Hello!")
	})

	// h2c サーバの立ち上げ
	h2s := &http2.Server{}

	if err := http.ListenAndServe(":8080", h2c.NewHandler(mux, h2s)); err != nil {
		log.Fatal(err)
	}
}

以下のいずれでも応答は返ってきます。

$ go version
go version go1.24.0 darwin/arm64

$ curl --http1.1 http://localhost:8080/
Hello

$ curl -v --http2 http://localhost:8080/
< HTTP/2 200 
Hello

$ curl --http2-prior-knowledge http://localhost:8080/
Hello

もちろんh2c.NewHandler 無しのHTTPサーバだと上記のようにつなぐことはできません。Upgrade方式の場合もつなげることはできますが、最終的にHTTP/1.1で応答されています。

main.go
-	if err := http.ListenAndServe(":8080", h2c.NewHandler(mux, h2s)); err != nil {
+   if err := http.ListenAndServe(":8080", mux); err != nil {
$ curl --http1.1 http://localhost:8080/
Hello
$ curl -v --http2 http://localhost:8080/
< HTTP/1.1 200 OK
Hello
$ curl --http2-prior-knowledge http://localhost:8080/
curl: (16) Remote peer returned unexpected data while we expected SETTINGS frame.  Perhaps, peer does not support HTTP/2 properly.

Upgradeも特に問題なく動くので、今後Goでバックエンドサービスを立ち上げる際はh2cをデフォルトで使ってもいいかもしれません。

Node.js での検証

Node.jsでは、標準の node:http2モジュール を使えば簡単にHTTP/2サーバを立ち上げることができますが、h2cはPrior Knowledge方式でしか接続できません。

main.js
import http2 from 'node:http2';

const server = http2.createServer();
// http2.createSecureServer({ allowHTTP1: true }); であれば HTTP/1.1 も受け入れ可能だがもはやこれはh2cではない

server.on('request', (req, res) => {
  res.writeHead(200, { 'Content-Type': 'text/plain; charset=utf-8' });
  res.end('Hello!');
});

server.listen(8080, () => {
  console.log('Server listening on :8080');
});
$ node --version
v22.19.0

$ curl --http1.1 http://localhost:8080/
curl: (1) Received HTTP/0.9 when not allowed

$ curl --http2 http://localhost:8080/
curl: (1) Received HTTP/0.9 when not allowed

$ curl --http2-prior-knowledge http://localhost:8080/
Hello!

GoのようなUpgrade方式は現在(2025-09)サポートされていないので、Node.jsでh2cを使う場合はクライアント(リバースプロキシ)もPrior Knowledge方式で接続する必要があります。

Node.jsのWebフレームワークから応答する

Goも同様ですが、大抵のWebフレームワークについては標準のサーバを受け入れる実装があるので、それを用いればWebフレームワークとh2cを組み合わせることができます。以下はHonoと組み合わせた例です。

hono.js
import http2 from 'node:http2';
import { Hono } from 'hono';
import { serve } from '@hono/node-server';

const app = new Hono();

app.get('/', (c) => {
  return c.text('Hello!');
});

serve({
  fetch: app.fetch,
  port: 8080,
  createServer: http2.createServer,
});

結果はもちろん素の node:http2 と変わりません。

$ curl --http1.1 http://localhost:8080/
curl: (1) Received HTTP/0.9 when not allowed

$ curl --http2 http://localhost:8080/
curl: (1) Received HTTP/0.9 when not allowed

$ curl --http2-prior-knowledge http://localhost:8080/
Hello!

Python での検証

前提知識: 今どきのPythonのWeb処理方式について

寡聞にして知らなかったのですが、PythonはWeb処理仕様とそれを動かすWebサーバの実装が分かれており、代表的なPythonのWeb処理仕様(Java Servlet仕様のようなもの)は以下があるようです。

今どきはASGI対応のフレームワーク (例: FastAPI)で処理を作成しておき、それをASGI実装のサーバ (例: uvicorn, Hypercorn) に載せてWebアプリケーションサーバを構築するのが主流のようです。

今回は FastAPI + Hypercorn でh2c対応のWebアプリケーションサーバを構築してみます。

FastAPI + Hypercorn での実装

上記で説明した通り、FastAPI(ASGIアプリ)とASGIサーバ実装は本来分離可能ですが、説明に都合がいいので分離せずシングルスクリプトで立ち上げます。

main.py
# /// script
# requires-python = ">=3.13"
# dependencies = [
#     "fastapi==0.116.1",
#     "hypercorn==0.17.3",
# ]
# ///

import asyncio
from fastapi import FastAPI, Request
from hypercorn.asyncio import serve
from hypercorn.config import Config

app = FastAPI()

@app.get("/")
async def root(request: Request):
    return "Hello!"

async def main():
    config = Config()
    config.bind = ["0.0.0.0:8080"]
    await serve(app, config)

if __name__ == "__main__":
    asyncio.run(main())

uv コマンドで実行します。1

$ uv run main.py

curl で接続してみます。

$ curl --http1.1 http://localhost:8080/
Hello!

$ curl -v --http2 http://localhost:8080/
> GET / HTTP/1.1
> Host: localhost:8080
> User-Agent: curl/8.7.1
> Accept: */*
> Connection: Upgrade, HTTP2-Settings
> Upgrade: h2c
> HTTP2-Settings: AAMAAABkAAQAoAAAAAIAAAAA
> 
* Request completely sent off
< HTTP/1.1 101 
< date: Tue, 02 Sep 2025 12:57:53 GMT
< server: hypercorn-h11
< connection: upgrade
< upgrade: h2c
< 
* Received 101, Switching to HTTP/2
* Closing connection
curl: (16) Error in the HTTP2 framing layer

$ curl -v --http2-prior-knowledge http://localhost:8080/
Hello!

Prior Knowledge方式はうまくいったようですが、Upgrade方式は最後にエラーになってしまいました。Hypercornの参照している h2 の仕様が遠因なのかもしれません。

まとめ

以下の言語バックエンドでのh2cのサポート状況を確認してきました。

  • Go言語: golang.org/x/net/http2/h2c
  • Node.js: node:http2
  • Python: hypercorn

いずれも、Prior Knowledge方式によるh2c接続には対応していますが、HTTP/1.1との共存やUpgrade(ネゴシエーション)に思ったように対応しているかはまちまちのようです。

バックエンドの場合は、リバースプロキシがh2cやアップグレードを希望してきた場合のみ接続をHTTP/2に柔軟に切り替えるという戦略が成り立ちづらく、 構成上では最初からh2cを採用するか決定し、その上でPrior Knowledge方式を用いる必要があるように感じました。

HTTP/1.1の話題抜きでもgRPCの通信基盤にもなるので、今後利用する機会があるかもしれません。

(続編に続く)

  1. uvについて詳しく知りたい方は弊記事 https://qiita.com/ssc-ksaitou/items/9da75058489ebe8c2009 を参考にしてください

14
4
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
14
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?