[インデックス 10030] Go言語におけるTLS接続のタイムアウト処理と接続終了の修正
コミット
- コミットハッシュ: 9d99d52fcb898433d58c861bd942b2caec22c16f
- 作成者: Adam Langley agl@golang.org
- 日付: 2011年10月18日 12:59:32 -0400
- コミットメッセージ: "http, crypto/tls: fix read timeouts and closing."
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/9d99d52fcb898433d58c861bd942b2caec22c16f
元コミット内容
このコミットは、Go言語の初期版(2011年)におけるTLS接続の処理に関する2つの重要な問題を修正しました:
- tls.Conn.Close()の問題: TLSのClose()メソッドが基底の接続を閉じず、close notify alertを送信するためにハンドシェイクを実行しようとしていた
- HTTPサーバーの問題: HTTPサーバーがTLSハンドシェイクからのエラーを確認していなかった
この修正により、Go issue #2281「crypto/tls: TLS client handshake never times out」が解決されました。
変更されたファイル:
- src/pkg/crypto/tls/conn.go (20行追加、4行削除)
- src/pkg/http/serve_test.go (4行削除)
- src/pkg/http/server.go (5行追加、1行削除)
変更の背景
2011年当時、Go言語はまだ比較的新しい言語(2009年リリース)で、TLS実装は活発に開発されていました。このコミットは、実際の運用環境で発見された2つの深刻な問題を修正するものでした:
問題1: TLS接続のタイムアウト処理
Go issue #2281では、TLSクライアントのハンドシェイクがタイムアウトしない問題が報告されていました。基底のソケットに読み込みタイムアウトが設定されている場合、readHandshake()
がcrypto/tls/conn.goで永続的にループし、readFromUntil
からEAGAIN
を継続的に取得する状況が発生していました。
問題2: close notify alertの不適切な処理
TLS仕様では、接続を正常に終了するためにclose notify alertを送信することが推奨されています。しかし、Go初期のTLS実装では、接続を閉じる際にclose notify alertを送信しようとしてハンドシェイクを実行し、実際の基底接続を閉じていませんでした。
前提知識の解説
TLS (Transport Layer Security)
TLSは、インターネット上でのデータ通信を暗号化し、認証と整合性を提供するプロトコルです。TLS接続は以下の段階で構成されます:
- ハンドシェイク: クライアントとサーバーが暗号化パラメータとセッション鍵を交換
- データ交換: 暗号化されたデータの送受信
- 接続終了: close notify alertを送信して接続を正常に終了
Close Notify Alert
TLS仕様(RFC 5246)では、接続を正常に終了するためにclose notify alertを送信することが定められています。これは、以下の理由で重要です:
- 切断攻撃(Truncation Attack)の防止: 攻撃者が通信を途中で切断することを検出
- データの完全性保証: 送信されたデータが完全に受信されたことを確認
- リソースの適切な解放: 両端でのリソースの適切なクリーンアップ
Go言語のTLS実装
Go言語のcrypto/tls
パッケージは、TLS 1.0-1.2(当時)の実装を提供していました。主要な構造体は:
tls.Conn
: TLS接続を表現する構造体tls.Config
: TLS設定を管理する構造体- 各種ハンドシェイク処理関数
技術的詳細
タイムアウト処理の問題
元の実装では、TLSハンドシェイクにおいて適切なタイムアウト処理が実装されていませんでした。readHandshake()
関数が以下のような無限ループに陥る可能性がありました:
for {
// readRecord()が常にEAGAINを返す場合
_, err := c.readRecord(recordTypeHandshake)
if err != nil {
// タイムアウトエラーが適切に処理されない
continue
}
// ハンドシェイク処理
}
Close処理の問題
元のtls.Conn.Close()
実装では、close notify alertを送信しようとしてハンドシェイクを実行していました:
func (c *Conn) Close() error {
// 問題: close notify alertを送信するためにハンドシェイクを実行
if err := c.Handshake(); err != nil {
return err
}
// 問題: 基底の接続を閉じていない
return c.sendAlert(alertCloseNotify)
}
この実装では、基底のnet.Conn
が閉じられず、リソースリークが発生していました。
コアとなるコードの変更箇所
1. crypto/tls/conn.go の修正
- 行数: 20行追加、4行削除
- 主な変更:
Close()
メソッドの修正- タイムアウト処理の改善
- close notify alertの適切な処理
2. http/server.go の修正
- 行数: 5行追加、1行削除
- 主な変更:
- TLSハンドシェイクエラーの適切な処理
- エラーハンドリングの改善
3. http/serve_test.go の修正
- 行数: 4行削除
- 主な変更:
- 不要なテストコードの削除
- テストの簡略化
コアとなるコードの解説
修正されたClose()メソッド
修正後のClose()メソッドは以下のような動作を行います:
- close notify alertの送信: 接続が確立されている場合のみ
- 基底接続の閉じ: 実際のソケット接続を確実に閉じる
- エラーハンドリング: 各段階でのエラーを適切に処理
func (c *Conn) Close() error {
var alertErr error
// 接続が確立されている場合のみclose notify alertを送信
if c.handshakeComplete {
alertErr = c.sendAlert(alertCloseNotify)
}
// 基底の接続を確実に閉じる
connErr := c.conn.Close()
// エラーの優先順位を考慮して返す
if connErr != nil {
return connErr
}
return alertErr
}
HTTPサーバーでのTLSエラーハンドリング
HTTPサーバーは、TLSハンドシェイクの結果を適切に確認するようになりました:
func (srv *Server) newConn(rwc net.Conn) (c *conn, err error) {
c = &conn{
server: srv,
rwc: rwc,
}
// TLS接続の場合
if tlsConfig := srv.TLSConfig; tlsConfig != nil {
c.rwc = tls.Server(c.rwc, tlsConfig)
// TLSハンドシェイクを実行し、エラーを確認
if err := c.rwc.(*tls.Conn).Handshake(); err != nil {
c.rwc.Close()
return nil, err
}
}
return c, nil
}
タイムアウト処理の改善
修正により、以下のタイムアウト処理が改善されました:
- 読み込みタイムアウト: ソケットレベルのタイムアウトが適切に処理される
- ハンドシェイクタイムアウト: ハンドシェイクプロセスでのタイムアウトを検出
- 書き込みタイムアウト: close notify alert送信時のタイムアウト処理
関連リンク
- Go Issue #2281 - 元の問題報告
- Go Code Review CL 5283045 - コードレビューページ
- RFC 5246 - The Transport Layer Security (TLS) Protocol Version 1.2 - TLS仕様
- Go crypto/tls Package Documentation - Go TLSパッケージドキュメント