[インデックス 18644] ファイルの概要
このコミットは、Go言語の標準ライブラリである net/http
パッケージにおける Transport
の挙動を改善するものです。具体的には、TLSハンドシェイクのタイムアウト機能を追加し、デフォルトでそのタイムアウトを設定することで、TLS接続の確立における潜在的なハングアップを防ぎ、より堅牢なネットワーク通信を実現します。
コミット
commit fd4b4b56c4a1fd3426fc9ab4c36ec1b270089d29
Author: Brad Fitzpatrick <bradfitz@golang.org>
Date: Tue Feb 25 08:08:15 2014 -0800
net/http: add Transport.TLSHandshakeTimeout; set it by default
Update #3362
LGTM=agl
R=agl
CC=golang-codereviews
https://golang.org/cl/68150045
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/fd4b4b56c4a1fd3426fc9ab4c36ec1b270089d29
元コミット内容
net/http
: Transport.TLSHandshakeTimeout
を追加し、デフォルトで設定する。
変更の背景
HTTPクライアントがHTTPSエンドポイントに接続する際、TCP接続の確立後にTLSハンドシェイクが行われます。このTLSハンドシェイクは、クライアントとサーバー間で暗号化された通信チャネルを確立するための重要なステップです。しかし、ネットワークの不安定性、サーバー側の問題、または悪意のある攻撃などにより、このハンドシェイクプロセスが予期せずハングアップする可能性があります。
従来の net/http.Transport
には、TCP接続自体のタイムアウト(Dial.Timeout
)は存在しましたが、TLSハンドシェイクに特化したタイムアウト設定がありませんでした。このため、TCP接続は確立されたものの、TLSハンドシェイクが完了しない場合に、クライアントが無限に待機してしまうという問題が発生する可能性がありました。これは、リソースの枯渇やアプリケーションの応答性低下につながる重大な問題です。
このコミットは、この問題を解決するために TLSHandshakeTimeout
という新しいフィールドを Transport
構造体に追加し、デフォルトで適切なタイムアウト値を設定することで、TLSハンドシェイクのハングアップを防ぎ、クライアントの堅牢性を向上させることを目的としています。コミットメッセージに記載されている Update #3362
は、この変更が特定の課題やバグ報告に対応するものであることを示唆していますが、公開されているGoのIssueトラッカーでは該当するIssueは見つかりませんでした。これは、内部的なIssue番号であるか、あるいは古いIssueがアーカイブされたためかもしれません。
前提知識の解説
1. Go言語の net/http
パッケージ
net/http
パッケージは、Go言語でHTTPクライアントおよびサーバーを実装するための標準ライブラリです。このパッケージは、HTTP/1.1およびHTTP/2プロトコルをサポートし、リクエストの送信、レスポンスの受信、ヘッダーの操作、クッキーの管理など、HTTP通信に必要な基本的な機能を提供します。
2. http.Transport
構造体
http.Transport
は、HTTPリクエストの単一のラウンドトリップ(リクエストの送信からレスポンスの受信まで)を実行するためのHTTPクライアントの低レベルな実装を提供します。これには、TCP接続の確立、TLSハンドシェイク、プロキシの処理、接続の再利用(Keep-Alive)などの詳細が含まれます。開発者は http.Client
を通じて Transport
を利用することが一般的ですが、Transport
を直接カスタマイズすることで、より詳細なネットワーク挙動を制御できます。
3. TLS (Transport Layer Security)
TLSは、インターネット上で安全な通信を提供するための暗号化プロトコルです。HTTPS(HTTP Secure)は、HTTP通信をTLSで暗号化することで実現されます。TLSハンドシェイクは、クライアントとサーバーが安全な通信チャネルを確立するために行う一連のネゴシエーションプロセスです。このプロセスには、証明書の交換、暗号スイートの選択、セッションキーの生成などが含まれます。
4. tls.Client
と tls.Conn.Handshake()
Go言語の crypto/tls
パッケージは、TLSプロトコルを実装するための機能を提供します。
tls.Client(conn net.Conn, config *tls.Config)
: 既存のnet.Conn
(TCP接続など)をラップして、TLSクライアント接続を確立するための*tls.Conn
を返します。(*tls.Conn).Handshake()
: TLSハンドシェイクを実行します。このメソッドが成功すると、安全な通信チャネルが確立されます。
5. time.Duration
と time.AfterFunc
time.Duration
: Go言語で時間の長さを表す型です。ミリ秒、秒、分などの単位で時間を表現できます。time.AfterFunc(d Duration, f func()) *Timer
: 指定された期間d
が経過した後に、関数f
を新しいゴルーチンで一度だけ実行するタイマーを作成します。タイマーは*time.Timer
型の値を返し、これを使ってタイマーを停止したりリセットしたりできます。
6. ゴルーチンとチャネル
- ゴルーチン (Goroutine): Go言語における軽量な実行スレッドです。Goランタイムによって管理され、非常に効率的に多数のゴルーチンを並行して実行できます。
- チャネル (Channel): ゴルーチン間で値を送受信するための通信メカニズムです。チャネルは、ゴルーチン間の同期と通信を安全に行うための主要な手段です。
技術的詳細
このコミットの主要な変更点は、net/http.Transport
構造体に TLSHandshakeTimeout
フィールドを追加し、TLSハンドシェイクの実行方法を変更したことです。
TLSHandshakeTimeout
フィールドの追加
Transport
構造体に TLSHandshakeTimeout time.Duration
フィールドが追加されました。このフィールドは、TLSハンドシェイクが完了するまでに待機する最大時間を指定します。値がゼロの場合、タイムアウトは適用されません。
type Transport struct {
// ... 既存のフィールド ...
// TLSHandshakeTimeout specifies the maximum amount of time waiting to
// wait for a TLS handshake. Zero means no timeout.
TLSHandshakeTimeout time.Duration
// ... 既存のフィールド ...
}
DefaultTransport
の初期化時に、この TLSHandshakeTimeout
に 10 * time.Second
(10秒) がデフォルト値として設定されました。これにより、明示的に設定しない限り、すべてのHTTPクライアントはTLSハンドシェイクに最大10秒を費やすことになります。
var DefaultTransport RoundTripper = &Transport{
// ... 既存の設定 ...
TLSHandshakeTimeout: 10 * time.Second,
}
TLSハンドシェイクの並行処理とタイムアウトの実装
最も重要な変更は、Transport.dialConn
メソッド内でのTLSハンドシェイクの実行ロジックです。以前は、tls.Client(conn, cfg).Handshake()
が直接呼び出され、ハンドシェイクが完了するまでブロックしていました。
変更後、TLSハンドシェイクは新しいゴルーチンで実行されるようになりました。同時に、TLSHandshakeTimeout
が設定されている場合、time.AfterFunc
を使用してタイマーが開始されます。
plainConn := conn
tlsConn := tls.Client(plainConn, cfg)
errc := make(chan error, 2) // エラーを送信するためのチャネル
var timer *time.Timer // タイマーを保持する変数
// TLSHandshakeTimeout が設定されている場合、タイマーを開始
if d := t.TLSHandshakeTimeout; d != 0 {
timer = time.AfterFunc(d, func() {
errc <- tlsHandshakeTimeoutError{} // タイムアウトエラーをチャネルに送信
})
}
// TLSハンドシェイクを新しいゴルーチンで実行
go func() {
err := tlsConn.Handshake()
if timer != nil {
timer.Stop() // ハンドシェイクが完了したらタイマーを停止
}
errc <- err // ハンドシェイクの結果をチャネルに送信
}()
// ハンドシェイクの結果またはタイムアウトエラーを待機
if err := <-errc; err != nil {
plainConn.Close() // エラーが発生した場合、元の接続を閉じる
return nil, err
}
この新しいロジックでは、以下のステップが実行されます。
plainConn
(元のTCP接続) とtlsConn
(TLSクライアント接続) を準備します。errc
というバッファ付きチャネルを作成し、ハンドシェイクの結果またはタイムアウトエラーをこのチャネルに送信します。TLSHandshakeTimeout
がゼロでない場合、time.AfterFunc
を使ってタイマーを設定します。タイマーが期限切れになると、tlsHandshakeTimeoutError{}
がerrc
チャネルに送信されます。tlsConn.Handshake()
を新しいゴルーチンで実行します。- ハンドシェイクが完了すると、そのゴルーチンはタイマーを停止し(もしタイマーがまだ実行中であれば)、ハンドシェイクの結果(エラーまたはnil)を
errc
チャネルに送信します。 - メインのゴルーチンは
<-errc
でチャネルからの値を受信します。これにより、ハンドシェイクが完了するか、タイムアウトが発生するまでブロックされます。 - エラーが発生した場合(ハンドシェイクエラーまたはタイムアウトエラー)、元の
plainConn
を閉じ、エラーを返します。
tlsHandshakeTimeoutError
型の追加
タイムアウトエラーを区別するために、tlsHandshakeTimeoutError
という新しい型が定義されました。この型は net.Error
インターフェースを満たすように Timeout()
と Temporary()
メソッドを実装しており、Error()
メソッドでエラーメッセージを返します。これにより、呼び出し元はタイムアウトエラーを他のネットワークエラーと区別して処理できるようになります。
type tlsHandshakeTimeoutError struct{}
func (tlsHandshakeTimeoutError) Timeout() bool { return true }
func (tlsHandshakeTimeoutError) Temporary() bool { return true }
func (tlsHandshakeTimeoutError) Error() string { return "net/http: TLS handshake timeout" }
テストケースの追加
net/http/transport_test.go
に TestTransportTLSHandshakeTimeout
という新しいテストケースが追加されました。このテストは、TLSハンドシェイクが意図的にハングアップする状況をシミュレートし、TLSHandshakeTimeout
が正しく機能してタイムアウトエラーを発生させることを検証します。
テストでは、カスタムの Dial
関数を持つ Transport
を作成し、TLSハンドシェイクが完了しないようにサーバー側で接続をすぐに閉じないようにします。そして、TLSHandshakeTimeout
を短い時間(250ミリ秒)に設定し、http.Client.Get
を呼び出します。期待される結果は、url.Error
にラップされた net.Error
であり、その Timeout()
メソッドが true
を返し、エラーメッセージに "handshake timeout" が含まれることです。
コアとなるコードの変更箇所
src/pkg/net/http/transport.go
Transport
構造体にTLSHandshakeTimeout time.Duration
フィールドを追加。DefaultTransport
の初期化でTLSHandshakeTimeout: 10 * time.Second
を設定。(*Transport).dialConn
メソッド内で、TLSハンドシェイクを新しいゴルーチンで実行し、TLSHandshakeTimeout
を使用してタイムアウトを監視するロジックを追加。tlsHandshakeTimeoutError
型とそのメソッド (Timeout
,Temporary
,Error
) を追加。
src/pkg/net/http/transport_test.go
TestTransportTLSHandshakeTimeout
テスト関数を追加。newLocalListener
ヘルパー関数を追加。
コアとなるコードの解説
src/pkg/net/http/transport.go
の変更点
--- a/src/pkg/net/http/transport.go
+++ b/src/pkg/net/http/transport.go
@@ -36,6 +36,7 @@ var DefaultTransport RoundTripper = &Transport{
Timeout: 30 * time.Second,
KeepAlive: 30 * time.Second,
}).Dial,
+ TLSHandshakeTimeout: 10 * time.Second, // ここでデフォルトのタイムアウトを設定
}
// DefaultMaxIdleConnsPerHost is the default value of Transport's
@@ -69,6 +70,10 @@ type Transport struct {
// tls.Client. If nil, the default configuration is used.
TLSClientConfig *tls.Config
+// TLSHandshakeTimeout specifies the maximum amount of time waiting to
+// wait for a TLS handshake. Zero means no timeout.
+ TLSHandshakeTimeout time.Duration // 新しいフィールドの追加
+
// DisableKeepAlives, if true, prevents re-use of TCP connections
// between different HTTP requests.
DisableKeepAlives bool
@@ -542,16 +547,33 @@ func (t *Transport) dialConn(cm connectMethod) (*persistConn, error) {
// ... TLS設定のクローンと調整 ...
}
- conn = tls.Client(conn, cfg)
- if err = conn.(*tls.Conn).Handshake(); err != nil {
+ plainConn := conn // 元のTCP接続を保持
+ tlsConn := tls.Client(plainConn, cfg) // TLS接続をラップ
+ errc := make(chan error, 2) // エラー通知用のチャネル
+ var timer *time.Timer // タイムアウトタイマー
+ if d := t.TLSHandshakeTimeout; d != 0 {
+ timer = time.AfterFunc(d, func() {
+ errc <- tlsHandshakeTimeoutError{} // タイムアウト時にエラーを送信
+ })
+ }
+ go func() { // 新しいゴルーチンでハンドシェイクを実行
+ err := tlsConn.Handshake()
+ if timer != nil {
+ timer.Stop() // ハンドシェイク完了時にタイマーを停止
+ }
+ errc <- err // ハンドシェイクの結果を送信
+ }()
+ if err := <-errc; err != nil { // チャネルからの結果を待機
+ plainConn.Close() // エラーが発生した場合、元の接続を閉じる
return nil, err
}
if !cfg.InsecureSkipVerify {
- if err = conn.(*tls.Conn).VerifyHostname(cfg.ServerName); err != nil {
+ if err := tlsConn.VerifyHostname(cfg.ServerName); err != nil {
+ plainConn.Close() // ホスト名検証失敗時も接続を閉じる
return nil, err
}
}
- pconn.conn = conn
+ pconn.conn = tlsConn // 確立されたTLS接続をpconnに設定
}
pconn.br = bufio.NewReader(pconn.conn)
@@ -1084,3 +1106,9 @@ type readerAndCloser struct {
\tio.Reader
\tio.Closer
}\n
+type tlsHandshakeTimeoutError struct{} // 新しいエラー型
+
+func (tlsHandshakeTimeoutError) Timeout() bool { return true } // net.Error インターフェースの実装
+func (tlsHandshakeTimeoutError) Temporary() bool { return true } // net.Error インターフェースの実装
+func (tlsHandshakeTimeoutError) Error() string { return "net/http: TLS handshake timeout" } // エラーメッセージ
この差分は、Transport
構造体への TLSHandshakeTimeout
フィールドの追加と、dialConn
メソッド内でのTLSハンドシェイク処理の変更を明確に示しています。特に、ハンドシェイクをゴルーチンで実行し、チャネルとタイマーを使ってタイムアウトを実装するパターンは、Go言語における並行処理とエラーハンドリングの典型的な例です。
src/pkg/net/http/transport_test.go
の変更点
--- a/src/pkg/net/http/transport_test.go
+++ b/src/pkg/net/http/transport_test.go
@@ -1722,6 +1722,73 @@ func TestTransportClosesRequestBody(t *testing.T) {
}
}
+func TestTransportTLSHandshakeTimeout(t *testing.T) { // 新しいテスト関数
+ defer afterTest(t)
+ if testing.Short() {
+ t.Skip("skipping in short mode")
+ }
+ ln := newLocalListener(t) // ローカルリスナーを作成
+ defer ln.Close()
+ testdonec := make(chan struct{})
+ defer close(testdonec)
+
+ go func() { // サーバー側のゴルーチン
+ c, err := ln.Accept() // 接続を受け入れる
+ if err != nil {
+ t.Error(err)
+ return
+ }
+ <-testdonec // テスト完了まで待機
+ c.Close() // 接続を閉じる
+ }()
+
+ getdonec := make(chan struct{})
+ go func() { // クライアント側のゴルーチン
+ defer close(getdonec)
+ tr := &Transport{
+ Dial: func(_, _ string) (net.Conn, error) {
+ return net.Dial("tcp", ln.Addr().String()) // ローカルリスナーに接続
+ },
+ TLSHandshakeTimeout: 250 * time.Millisecond, // 短いタイムアウトを設定
+ }
+ cl := &Client{Transport: tr}
+ _, err := cl.Get("https://dummy.tld/") // HTTPSリクエストを送信
+ if err == nil {
+ t.Fatal("expected error") // エラーが期待される
+ }
+ ue, ok := err.(*url.Error)
+ if !ok {
+ t.Fatalf("expected url.Error; got %#v", err)
+ }
+ ne, ok := ue.Err.(net.Error)
+ if !ok {
+ t.Fatalf("expected net.Error; got %#v", err)
+ }
+ if !ne.Timeout() { // タイムアウトエラーであることを確認
+ t.Error("expected timeout error; got %v", err)
+ }
+ if !strings.Contains(err.Error(), "handshake timeout") { // エラーメッセージを確認
+ t.Error("expected 'handshake timeout' in error; got %v", err)
+ }
+ }()
+ select {
+ case <-getdonec: // クライアントゴルーチンの完了を待機
+ case <-time.After(5 * time.Second): // テスト全体のタイムアウト
+ t.Error("test timeout; TLS handshake hung?")
+ }
+}
+
+func newLocalListener(t *testing.T) net.Listener { // ヘルパー関数
+ ln, err := net.Listen("tcp", "127.0.0.1:0")
+ if err != nil {
+ ln, err = net.Listen("tcp6", "[::1]:0")
+ }
+ if err != nil {
+ t.Fatal(err)
+ }
+ return ln
+}
+
type countCloseReader struct {
n *int
io.Reader
このテストコードは、TLSHandshakeTimeout
が正しく機能することを検証するための重要な部分です。サーバー側でTLSハンドシェイクを意図的に完了させないことで、クライアント側でタイムアウトが発生することを確認しています。net.Error
インターフェースの Timeout()
メソッドの利用や、エラーメッセージの文字列チェックなど、Goのエラーハンドリングのベストプラクティスが反映されています。
関連リンク
- Go言語
net/http
パッケージのドキュメント: https://pkg.go.dev/net/http - Go言語
crypto/tls
パッケージのドキュメント: https://pkg.go.dev/crypto/tls - Go言語
time
パッケージのドキュメント: https://pkg.go.dev/time
参考にした情報源リンク
- Go言語の公式ドキュメント
- Go言語のソースコード
- TLSハンドシェイクに関する一般的な情報源 (RFCなど)