net/http のTransportとServerが、ConnectionState() tls.ConnectionStateを実装するnet.Conn上でもALPNによるHTTP/2ネゴシエーションを扱えるようになった
net/http
概要
net/http の Transport と Server が、ユーザー提供の net.Conn を *tls.Conn と同等に扱えるようになった。対象は ConnectionState() tls.ConnectionState メソッドを実装している接続で、これまでの「*tls.Conn である場合のみ」という制限がなくなり、ALPNネゴシエーション結果に基づくHTTP/2の自動有効化や Request.TLS/Response.TLS の設定が、crypto/tls 以外のTLSスタック(あるいはTLS類似の接続実装)でも機能するようになる。
導入経緯
net/http は crypto/tls の具象型 *tls.Conn に強く結合しており、外部のTLSスタック(tls-tris によるTLS 1.3先行実装や、BoringCrypto、openssl bindings など)を差し込むことができない、という課題が issue #21753 で報告されていた。当時は Server.TLSNextProto / Transport.TLSNextProto が具象の *tls.Conn を要求する型であることが最大の障壁とされ、あるコントリビューターからは「net/http単体の複雑化は避けたい」という理由でこの提案は一度クローズされた。
その後、HTTP/2実装が golang.org/x/net/http2 から標準ライブラリへ統合されたことで TLSNextProto 由来の制約がなくなり、issueが再オープンされた。golang.org/x/net/http2 が既に採用していた「ConnectionState() tls.ConnectionState を実装する net.Conn を *tls.Conn と同等に扱う」という判定方法を net/http 本体のクライアント・サーバー双方の接続確立処理にも適用する、という解決策が採られている。
議論のハイライト
- 当初の提案は
Server.TLSNextProto/Transport.TLSNextProtoの型が具象の*tls.Connに依存している点が最大の課題とされ、良い解決策がないまま一度クローズされた。 - HTTP/2実装がstdに統合され
TLSNextProtoの制約が外れたことで、エクスポートされたシンボルの変更を伴わない機能要望として再オープンされた。 golang.org/x/net/http2が長年採用してきた「ConnectionState() tls.ConnectionStateを実装するnet.Connを*tls.Connと同等に扱う」という構造的インターフェース判定を、net/http本体にも適用する形で決着した。
移行時の注意
この判定はメソッドシグネチャのみに基づく構造的な型判定であり、対象が実際にTLS接続であるかどうかは検証されない。ConnectionState() tls.ConnectionState という名前・シグネチャのメソッドを持つ net.Conn 実装(TLSとは無関係な用途であっても)を Transport.DialContext/DialTLSContext やサーバー側の net.Listener から返している場合、意図せずTLS接続として扱われ、ALPNネゴシエーション結果次第でHTTP/2ハンドリングに切り替わる可能性がある点に注意が必要である。
実装解説
クライアント側は net/http/transport.go の dialConn(transport.go;l=1892)で、customDialTLS が返した net.Conn をローカル定義の connectionStater インターフェースに型アサーションし、ConnectionState() の結果を pconn.tlsState に保持する。この pconn.tlsState.NegotiatedProtocol が "h2" かどうかは、以降のHTTP/2判定(transport.go;l=2059, transport.go;l=2081)で *tls.Conn かどうかによらず参照される。
サーバー側は net/http/server.go の conn.serve(server.go;l=1978)で、Listener から得た c.rwc を同様に connectionStater へ型アサーションし、HandshakeContext も実装していればハンドシェイクを実行したうえで ConnectionState() を呼び出す。ネゴシエートされたプロトコルが "h2" であれば、*tls.Conn かどうかを問わず標準ライブラリ内蔵のHTTP/2サーバー(serveHTTP2Conn)に処理を委譲する(server.go;l=2015)。また、TLSハンドシェイクを経ずHTTP/1.xとして処理する場合でも、c.rwc が connectionStater を実装していれば Request.TLS をその ConnectionState() から設定する(server.go;l=2045)。