🍻

golangでHTTP3を試してみる

2022/06/14に公開
2

はじめに

つい先日、HTTP3がRFC9114として正式に発表されました。

https://blog.cloudflare.com/cloudflare-view-http3-usage/

RFC読むよりとりあえずパケット見る派なので、とりあえずコード書いて動かしてキャプチャしたいところです。

quic-gohttp3 ディレクトリがあり、対応してそうなのでサンプルコードを書いてみました。
数日前にcommitが入っていて開発も活発そうですね。

サンプルのサーバ側コードを試す時はお手数ですが、opensslやmkcertコマンドなどでご自分で公開鍵&秘密鍵を生成してください。

https://github.com/sat0ken/go-example-http3

クライアント

まずはクライアントのコードを書いてみます。
go.docを見ると、RoundTrip という関数に *http.Request を渡すとHTTP3のクライアントコードになりそうです。
こんなコードになりました。

package main

import (
	"crypto/tls"
	"fmt"
	"github.com/lucas-clemente/quic-go/http3"
	"io/ioutil"
	"log"
	"net/http"
)

func main() {

	r := http3.RoundTripper{
		TLSClientConfig: &tls.Config{
			MinVersion: tls.VersionTLS13,
			MaxVersion: tls.VersionTLS13,
		},
	}
	req, _ := http.NewRequest("GET", "https://google.com", nil)

	resp, err := r.RoundTrip(req)
	if err != nil {
		log.Fatal(err)
	}

	body, _ := ioutil.ReadAll(resp.Body)
	fmt.Print(string(body))

}

これを実行してみます。
ちゃんとレスポンスが返ってきました。

$ go run http3-client.go 
<HTML><HEAD><meta http-equiv="content-type" content="text/html;charset=utf-8">
<TITLE>301 Moved</TITLE></HEAD><BODY>
<H1>301 Moved</H1>
The document has moved
<A HREF="https://www.google.com/">here</A>.
</BODY></HTML>

サーバ

クライアントはよさげなので、次にサーバのコードを書いてみます。
go.docを見ると、ListenAndServeTLS というよく見た関数があるのでこれを使えばいいのかなとごにょごにょして以下のようなコードとなりました。

package main

import (
	"crypto/tls"
	"fmt"
	"github.com/lucas-clemente/quic-go/http3"
	"log"
	"net/http"
	"os"
)

func HelloHTTP3Server(w http.ResponseWriter, req *http.Request) {
	fmt.Printf("client from : %s\n", req.RemoteAddr)
	fmt.Fprintf(w, "hello\n")
}

func main() {

	mux := http.NewServeMux()
	mux.Handle("/", http.HandlerFunc(HelloHTTP3Server))

	w := os.Stdout
	
	server := http3.Server{
		Addr: "127.0.0.1:18443",
		TLSConfig: &tls.Config{
			MinVersion:   tls.VersionTLS13,
			MaxVersion:   tls.VersionTLS13,
			KeyLogWriter: w,
		},
		Handler: mux,
	}

	err := server.ListenAndServeTLS("./my-tls.pem", "./my-tls-key.pem")
	if err != nil {
		log.Fatal(err)
	}
}

サーバを起動させておいて、先に作成したクライアントのURLをLocalhostにしてリクエストを送ります。

$ go run http3-client.go
hello

ちゃんとレスポンスが返ってきました。

QUICとHTTP3のパケットを見てみよう

これでパケットキャプチャできる環境が整いましたが、パケットはTLSで暗号化されています。
WiresharkeではTLSプロトコルの設定にSSLKeyログファイルを指定することで、パケットを復号して見ることができます。

golangでは tls.Config の設定でSSLKeyログを吐くことができるのでその細工をします。
クライアントコードの tls.ConfigKeyLogWriter で標準出力の w をセットして実行します。

	w := os.Stdout // ←追加
	r := http3.RoundTripper{
		TLSClientConfig: &tls.Config{
			MinVersion:   tls.VersionTLS13,
			MaxVersion:   tls.VersionTLS13,
			KeyLogWriter: w, // ←追加
		},
	}

そうして実行すると ~_TRAFFIC_SECRET というのが先頭に4行追加で出力されます。
この追加された出力がクライアント、サーバ側の暗号化で使われる鍵の情報になるので、これをテキストファイルに貼り付けて保存します。

$ go run http3-client.go 
CLIENT_HANDSHAKE_TRAFFIC_SECRET b60958cb9caddc6c2ae054db08ab55ce5798dafa4f011cfbaf9b34b6799910af f03ea9ff4331d67e91736dd55f18e089ffb150ef25accbb6b9b7a2fa07784f68
SERVER_HANDSHAKE_TRAFFIC_SECRET b60958cb9caddc6c2ae054db08ab55ce5798dafa4f011cfbaf9b34b6799910af 001394c1ca7cda2322438a09d734ddf0e6f728d41a93c2873ca08033e7c8e9ca
CLIENT_TRAFFIC_SECRET_0 b60958cb9caddc6c2ae054db08ab55ce5798dafa4f011cfbaf9b34b6799910af 19f0b87b4277acd1128c51e19ddf956ff869beebe1bc598295d3d3668cd859b2
SERVER_TRAFFIC_SECRET_0 b60958cb9caddc6c2ae054db08ab55ce5798dafa4f011cfbaf9b34b6799910af eea78c0453f940374d4e477b1734f53ecf67d669929c2d7cbb27a8f636641cce
<HTML><HEAD><meta http-equiv="content-type" content="text/html;charset=utf-8">
<TITLE>301 Moved</TITLE></HEAD><BODY>
<H1>301 Moved</H1>
The document has moved
<A HREF="https://www.google.com/">here</A>.
</BODY></HTML>

保存したテキストファイルをWiresharkeで読み込むようにTLSプロトコルの設定でセットします。

鍵はRTTごとに生成されるので、以下のような手順になります。

  1. Wiresharkeでパケットキャプチャして保存
  2. SSLKeyログを貼り付けてファイルに保存
  3. 保存したpcapファイルを読み込む
  4. 復号された状態でパケットが見れる

それではパケットを見てみますが、ちゃんとRFC読んでないですし詳しい解説は他の人の解説をご覧ください。
以下は素人の野球解説ですw

まずクライアントからInitial PacketのCryptoフレームにClientHelloを入れて送ります。

サーバからCryptoフレームでServerHelloが返ってきています。

クライアントはServerHelloを受信したことを示すためにACKフレームを送ります。
この時、ServerHelloで送られてきたSource Connection IDをDestination Connection IDにセットして送っています。

このConnection IDという仕組みによって、例えばWifiが切り替わってIPアドレスやポートが変わってもそのままサーバと通信が続けられるんですよね。

ACKフレームがサーバに送られたら、サーバからTLSのEncrypted Extendsionsが、

次にサーバのCertificateが送られてきて、

クライアントはServerCertificateを受信したことをACKフレームを送って知らせています。

サーバからCertificate, CertificateVerify, Finishedが送られてきます。

Certificate〜Finishedメッセージの後ろに、Streamフレームのデータとして、HTTP3のSettingsパラメータが送られます。

クライアントはACKを送ってから、Finishedメッセージを送ります。

クライアントはStreamフレームでHTTPヘッダとSettingsを送ります。
ヘッダのTypeが01って静的テーブルのIndex=1番って意味でしたっけ?
HTTP3のQPACKもHPACKと基本同じなはずですが、もう忘れちゃいましたw。

サーバからACKの次にHANDSHAKE_DONEが送られてきました。
これでハンドシェイク完了ってことですね。

ACKフレームを挟んだ後に、サーバから301のDocumentが返ってきました。

ふーむ、なるほど??🤔🤔🤔

おわりに

というわけでgolangでQUICとHTTP3をやってみたけどよくわかりませんでした、というクソみたいなBlogと同じ結末を迎えてしまい、ここまで読んだ頂いた方に申し訳ないです🙇🏻‍♂️🙇🏻‍♂️🙇🏻‍♂️

quic-goを使えばgolangでHTTP3を喋れることはわかったので実装も見ながら、ちょうどboothで解説本が出ていたのでこれを見ながら勉強したいと思います。

https://quic.booth.pm/items/3848264

レポジトリにpcapとsslkeyログファイルも入れておいたので、パケット見たい方はご覧になってください(ぜひ教えてください🙏🙏🙏)

Discussion

ryo-yamaokaryo-yamaoka

FYI: 現在のバージョン(私の試した環境ではv0.28.0)だとhttp3.Serverの定義が変わっており以下のように直接HTTPHandlerを入れる形になっています。

	w := os.Stdout
	sv := http3.Server{
		Addr: "localhost:18443",
		TLSConfig: &tls.Config{
			MinVersion:   tls.VersionTLS13,
			MaxVersion:   tls.VersionTLS13,
			KeyLogWriter: w,
		},
		Handler: mux,
	}