[インデックス 18693] ファイルの概要
このコミットは、Go言語の crypto/tls
パッケージに DialWithDialer
関数を追加するものです。これにより、net.Dialer
を用いてTLS接続の確立時にタイムアウトなどの詳細なネットワーク設定を適用できるようになります。既存の Dial
関数は DialWithDialer
を内部的に呼び出すように変更され、より柔軟な接続確立が可能になりました。
コミット
commit 1f8b2a69ec871c1e4c33b6df4b2127bbafd67495
Author: Adam Langley <agl@golang.org>
Date: Fri Feb 28 09:40:12 2014 -0500
crypto/tls: add DialWithDialer.
While reviewing uses of the lower-level Client API in code, I found
that in many cases, code was using Client only because it needed a
timeout on the connection. DialWithDialer allows a timeout (and
other values) to be specified without resorting to the low-level API.
LGTM=r
R=golang-codereviews, r, bradfitz
CC=golang-codereviews
https://golang.org/cl/68920045
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/1f8b2a69ec871c1e4c33b6df4b2127bbafd67495
元コミット内容
crypto/tls: add DialWithDialer.
このコミットは、crypto/tls
パッケージに DialWithDialer
関数を追加します。
コード内で低レベルの Client
APIの使用例をレビューしている際に、多くの場合、接続にタイムアウトが必要なためだけに Client
を使用していることが判明しました。DialWithDialer
は、低レベルAPIに頼ることなく、タイムアウト(およびその他の値)を指定できるようにします。
変更の背景
Go言語の crypto/tls
パッケージは、TLS (Transport Layer Security) プロトコルを実装し、セキュアなネットワーク通信を提供します。TLSクライアントがサーバーに接続する際、通常は tls.Dial
関数を使用します。しかし、従来の tls.Dial
関数は、TCP接続の確立やTLSハンドシェイクの過程でタイムアウトを設定する直接的なメカニズムを提供していませんでした。
開発者が接続タイムアウトを制御したい場合、しばしば net.Dial
で生のTCP接続を確立し、その接続に対して tls.Client
を使用してTLSハンドシェイクを行うという、より低レベルなアプローチを取る必要がありました。この方法は冗長であり、net.Dialer
が提供する豊富な接続設定(タイムアウト、デッドライン、ローカルアドレスのバインドなど)を tls.Dial
の高レベルな抽象化と統合することが困難でした。
このコミットの背景には、このような「タイムアウトのためだけに低レベルAPIを使う」という一般的なユースケースを解消し、crypto/tls
パッケージの使いやすさと柔軟性を向上させる目的があります。net.Dialer
の機能を活用することで、TLS接続の確立プロセス全体にわたるタイムアウトやその他のネットワーク設定を、より簡潔かつ統一された方法で指定できるようになります。
前提知識の解説
このコミットを理解するためには、以下のGo言語のネットワークプログラミングに関する基本的な概念と crypto/tls
パッケージの構造を理解しておく必要があります。
-
net
パッケージとnet.Dialer
:- Go言語の
net
パッケージは、ネットワークI/Oのプリミティブを提供します。TCP/UDP接続の確立、リスナーの作成などが含まれます。 net.Dial
関数は、指定されたネットワークアドレスへの接続を確立するためのシンプルな関数です。net.Dialer
は、より高度な接続設定を可能にする構造体です。これには、Timeout
(接続試行全体のタイムアウト)、Deadline
(接続が完了しなければならない絶対時刻)、LocalAddr
(ローカルアドレスのバインド)、KeepAlive
(TCPキープアライブの設定) などのフィールドが含まれます。Dialer.Dial
メソッドは、これらの設定を適用してネットワーク接続を確立します。
- Go言語の
-
crypto/tls
パッケージ:- Go言語でTLS/SSL通信を扱うための標準ライブラリです。
tls.Conn
は、TLSハンドシェイクが完了した後のセキュアな接続を表します。net.Conn
インターフェースを実装しているため、通常のネットワーク接続と同様に読み書きが可能です。tls.Config
は、TLS接続の振る舞いを設定するための構造体です。証明書、キー、ルートCA、サーバー名の検証、プロトコルバージョン、暗号スイートなどを指定できます。tls.Dial(network, addr, config *tls.Config)
: 指定されたネットワークアドレスに接続し、TLSハンドシェイクを開始して、結果として得られるTLS接続を返します。内部的にはnet.Dial
を使用します。tls.Client(conn net.Conn, config *tls.Config)
: 既に確立されたnet.Conn
(例えば、net.Dial
で得られたTCP接続) をラップし、その上でTLSクライアントハンドシェイクを実行するためのtls.Conn
を作成します。この関数自体はハンドシェイクを開始せず、tls.Conn
のHandshake()
メソッドを呼び出すことでハンドシェイクが実行されます。
-
タイムアウト処理:
- ネットワーク通信において、接続の確立やデータの送受信が指定された時間内に完了しない場合にエラーを発生させるメカニズムです。これにより、応答しないサーバーやネットワークの問題によってアプリケーションがハングアップするのを防ぎます。
- Goでは、
time.AfterFunc
やselect
ステートメントとチャネルを組み合わせて非同期処理のタイムアウトを実装するのが一般的です。
このコミットは、net.Dialer
の柔軟な接続設定能力と crypto/tls
のセキュアな通信機能を統合することで、より堅牢で制御可能なTLSクライアント接続の確立を可能にしています。
技術的詳細
このコミットの主要な技術的変更点は、crypto/tls
パッケージに DialWithDialer
関数を導入し、既存の Dial
関数をその上に再構築したことです。
DialWithDialer
の導入
DialWithDialer
関数は以下のようなシグネチャを持ちます。
func DialWithDialer(dialer *net.Dialer, network, addr string, config *Config) (*Conn, error)
この関数は、net.Dialer
のインスタンスを受け取ります。この dialer
オブジェクトが持つ Timeout
や Deadline
の設定が、TCP接続の確立からTLSハンドシェイクの完了までの「全体」のタイムアウトとして適用されます。
内部的な処理フローは以下の通りです。
-
タイムアウトの計算:
dialer.Timeout
とdialer.Deadline
の両方を考慮し、TLS接続確立プロセス全体のタイムアウト時間を決定します。dialer.Timeout
が設定されている場合、それが初期のタイムアウト候補となります。dialer.Deadline
が設定されている場合、現在の時刻からデッドラインまでの残り時間を計算し、それがtimeout
候補よりも短い場合は、その残り時間をtimeout
として採用します。これにより、Deadline
が優先されます。
-
タイムアウト監視チャネルの準備:
- 計算された
timeout
が0でない(つまりタイムアウトが設定されている)場合、errChannel
というエラーチャネルを作成します。 time.AfterFunc(timeout, ...)
を使用して、指定されたタイムアウト時間が経過した後にtimeoutError{}
をerrChannel
に送信するゴルーチンを起動します。これは、接続とハンドシェイクがタイムアウトした場合にエラーを通知するためのメカニズムです。
- 計算された
-
基盤となる接続の確立:
dialer.Dial(network, addr)
を呼び出して、基盤となるTCP接続 (rawConn
) を確立します。ここでnet.Dialer
の設定(タイムアウト、デッドラインなど)が適用されます。- もし
dialer.Dial
がエラーを返した場合、そのエラーを直ちに返します。
-
TLS接続の初期化:
tls.Client(rawConn, config)
を呼び出して、確立されたrawConn
上にTLSクライアント接続 (conn
) を作成します。この時点ではまだTLSハンドシェイクは行われていません。
-
TLSハンドシェイクの実行とタイムアウト処理:
timeout
が0の場合(タイムアウトが設定されていない場合)、conn.Handshake()
を直接呼び出してハンドシェイクを実行します。timeout
が0でない場合(タイムアウトが設定されている場合)、conn.Handshake()
を別のゴルーチンで実行し、その結果をerrChannel
に送信します。- メインのゴルーチンは
errChannel
からエラーを待ち受けます。これにより、ハンドシェイクが完了したエラーか、タイムアウトエラーのいずれかを受け取ることができます。 - ハンドシェイクがエラーを返した場合、またはタイムアウトエラーを受け取った場合、
rawConn
をクローズしてエラーを返します。
-
成功時の返却:
- ハンドシェイクが成功した場合、確立された
tls.Conn
を返します。
- ハンドシェイクが成功した場合、確立された
Dial
関数の再構築
既存の tls.Dial
関数は、DialWithDialer
を内部的に呼び出すように変更されました。
func Dial(network, addr string, config *Config) (*Conn, error) {
return DialWithDialer(new(net.Dialer), network, addr, config)
}
これにより、tls.Dial
を呼び出すユーザーは、明示的に net.Dialer
を設定しなくても、デフォルトの net.Dialer
を使用して DialWithDialer
の恩恵を受けることができます。これは後方互換性を保ちつつ、内部実装を改善する典型的なパターンです。
timeoutError
型の導入
タイムアウトエラーを区別するために、timeoutError
という内部的な型が導入されました。これは error
インターフェースと、net.Error
インターフェースの一部である Timeout()
および Temporary()
メソッドを実装しています。これにより、呼び出し元は返されたエラーがタイムアウトによるものかどうかを net.Error
インターフェースを通じて判別できます。
テストケースの追加
tls_test.go
に TestDialTimeout
という新しいテストケースが追加されました。このテストは、DialWithDialer
が正しくタイムアウトを処理することを確認します。具体的には、リスナーが接続を受け入れた後にTLSハンドシェイクを意図的に遅延させ、DialWithDialer
が設定されたタイムアウト時間内にエラーを返すことを検証しています。
この変更により、crypto/tls
パッケージは、より柔軟で堅牢なTLSクライアント接続の確立機能を提供できるようになり、開発者はタイムアウト処理のために低レベルなネットワークAPIに直接触れる必要がなくなりました。
コアとなるコードの変更箇所
このコミットによる主要なコード変更は、src/pkg/crypto/tls/tls.go
と src/pkg/crypto/tls/tls_test.go
の2つのファイルに集中しています。
src/pkg/crypto/tls/tls.go
import
文の追加:time
パッケージがインポートされました。timeoutError
型の定義:type timeoutError struct{} func (timeoutError) Error() string { return "tls: DialWithDialer timed out" } func (timeoutError) Timeout() bool { return true } func (timeoutError) Temporary() bool { return true }
DialWithDialer
関数の新規追加:net.Dialer
を引数として受け取る新しい関数が追加されました。dialer.Timeout
とdialer.Deadline
を考慮して、接続全体のタイムアウトを計算するロジックが追加されました。- タイムアウトが設定されている場合、
time.AfterFunc
とチャネル (errChannel
) を使用してタイムアウトを監視するメカニズムが導入されました。 dialer.Dial
を使用して基盤となるネットワーク接続を確立します。tls.Client
でTLS接続オブジェクトを作成し、TLSハンドシェイクを(必要に応じてゴルーチン内で)実行します。- ハンドシェイクがタイムアウトした場合、またはエラーが発生した場合に適切に接続をクローズし、エラーを返します。
Dial
関数の変更:- 既存の
Dial
関数が、新しく追加されたDialWithDialer
を内部的に呼び出すように変更されました。これにより、Dial
の既存の振る舞いを維持しつつ、DialWithDialer
の機能を利用できるようになりました。
func Dial(network, addr string, config *Config) (*Conn, error) { return DialWithDialer(new(net.Dialer), network, addr, config) }
- 元の
Dial
関数の実装は削除されました。
- 既存の
src/pkg/crypto/tls/tls_test.go
import
文の追加:net
,strings
,time
パッケージがインポートされました。TestDialTimeout
関数の新規追加:DialWithDialer
のタイムアウト機能をテストするための新しいテストケースが追加されました。net.Listen
でリスナーを作成し、クライアント接続を受け入れた後に意図的にハンドシェイクを遅延させることで、タイムアウトが発生する状況をシミュレートします。net.Dialer
に短いTimeout
を設定し、DialWithDialer
を呼び出します。- 返されたエラーがタイムアウトエラーであることを
strings.Contains
とエラーメッセージで検証します。
これらの変更により、crypto/tls
パッケージは、より柔軟な接続タイムアウト制御をサポートするようになりました。
コアとなるコードの解説
src/pkg/crypto/tls/tls.go
の変更点
DialWithDialer
関数の詳細
この関数は、net.Dialer
を介して提供されるネットワーク設定(特にタイムアウト)をTLS接続の確立プロセス全体に適用することを目的としています。
func DialWithDialer(dialer *net.Dialer, network, addr string, config *Config) (*Conn, error) {
// 1. タイムアウトの計算: dialerのTimeoutとDeadlineを考慮し、全体のタイムアウト時間を決定
timeout := dialer.Timeout
if !dialer.Deadline.IsZero() {
deadlineTimeout := dialer.Deadline.Sub(time.Now())
if timeout == 0 || deadlineTimeout < timeout {
timeout = deadlineTimeout
}
}
var errChannel chan error
if timeout != 0 {
// 2. タイムアウト監視チャネルの準備: タイムアウト時にエラーを送信するゴルーチンを起動
errChannel = make(chan error, 2) // バッファサイズ2は、ハンドシェイクエラーとタイムアウトエラーの両方を受け取るため
time.AfterFunc(timeout, func() {
errChannel <- timeoutError{} // タイムアウト時にカスタムエラーを送信
})
}
// 3. 基盤となる接続の確立: net.Dialerを使用してTCP接続を確立
rawConn, err := dialer.Dial(network, addr)
if err != nil {
return nil, err
}
// サーバー名の抽出 (既存のDialロジックから移植)
colonPos := strings.LastIndex(addr, ":")
if colonPos == -1 {
colonPos = len(addr)
}
hostname := addr[:colonPos]
// configがnilの場合のデフォルト設定 (既存のDialロジックから移植)
if config == nil {
config = defaultConfig()
}
c := *config
if c.ServerName == "" {
c.ServerName = hostname
config = &c
}
// 4. TLS接続の初期化: 確立されたrawConn上にTLSクライアント接続を作成
conn := Client(rawConn, config)
// 5. TLSハンドシェイクの実行とタイムアウト処理
if timeout == 0 {
// タイムアウトが設定されていない場合、直接ハンドシェイクを実行
err = conn.Handshake()
} else {
// タイムアウトが設定されている場合、別のゴルーチンでハンドシェイクを実行し、結果をチャネルに送信
go func() {
errChannel <- conn.Handshake()
}()
// チャネルからエラーを待ち受ける (ハンドシェイク完了エラーかタイムアウトエラー)
err = <-errChannel
}
// 6. エラー処理: ハンドシェイクが失敗した場合、rawConnをクローズ
if err != nil {
rawConn.Close()
return nil, err
}
return conn, nil
}
この実装の鍵は、net.Dialer
のタイムアウト設定をTCP接続だけでなく、その後のTLSハンドシェイクにも適用するために、time.AfterFunc
とチャネルを用いた非同期監視メカニズムを導入している点です。これにより、接続確立からハンドシェイク完了までの一連の処理全体が指定された時間内に完了することが保証されます。
Dial
関数の変更点
元の Dial
関数は、DialWithDialer
を呼び出すだけのシンプルなラッパーになりました。
// Dial connects to the given network address using net.Dial
// and then initiates a TLS handshake, returning the resulting
// TLS connection.
// Dial interprets a nil configuration as equivalent to
// the zero configuration; see the documentation of Config
// for the defaults.
func Dial(network, addr string, config *Config) (*Conn, error) {
// デフォルトのnet.Dialerを使用してDialWithDialerを呼び出す
return DialWithDialer(new(net.Dialer), network, addr, config)
}
これは、既存のAPIのセマンティクスを維持しつつ、内部的に新しい、より強力な機能を利用するための標準的なGoのパターンです。new(net.Dialer)
は、タイムアウトやデッドラインが設定されていない、デフォルトの net.Dialer
インスタンスを作成します。
src/pkg/crypto/tls/tls_test.go
の変更点
TestDialTimeout
関数の詳細
このテストは、DialWithDialer
のタイムアウト機能が期待通りに動作することを確認します。
func TestDialTimeout(t *testing.T) {
if testing.Short() {
t.Skip("skipping in short mode")
}
// 1. リスナーの準備: 接続を受け入れるためのTCPリスナーを作成
listener, err := net.Listen("tcp", "127.0.0.1:0")
if err != nil {
listener, err = net.Listen("tcp6", "[::1]:0") // IPv6も試行
}
if err != nil {
t.Fatal(err)
}
addr := listener.Addr().String()
defer listener.Close()
complete := make(chan bool) // テスト終了を通知するためのチャネル
defer close(complete)
// 2. サーバー側のゴルーチン: 接続を受け入れ、ハンドシェイクを意図的に遅延させる
go func() {
conn, err := listener.Accept() // クライアントからの接続を受け入れる
if err != nil {
t.Error(err)
return
}
<-complete // completeチャネルが閉じられるまで待機 (ハンドシェイクを遅延させる)
conn.Close()
}()
// 3. クライアント側の設定: 短いタイムアウトを持つnet.Dialerを作成
dialer := &net.Dialer{
Timeout: 10 * time.Millisecond, // 非常に短いタイムアウトを設定
}
// 4. DialWithDialerの呼び出し: タイムアウトが発生することを期待
if _, err = DialWithDialer(dialer, "tcp", addr, nil); err == nil {
t.Fatal("DialWithTimeout completed successfully") // タイムアウトするはずなので、成功したらエラー
}
// 5. エラーの検証: 返されたエラーがタイムアウトエラーであることを確認
if !strings.Contains(err.Error(), "timed out") {
t.Errorf("resulting error not a timeout: %s", err)
}
}
このテストは、サーバー側で <-complete
を使用して意図的にハンドシェイクをブロックすることで、クライアント側の DialWithDialer
が設定された短いタイムアウト時間内にエラーを返すことを効果的に検証しています。これにより、DialWithDialer
のタイムアウト処理の正確性が保証されます。
関連リンク
- Go言語
crypto/tls
パッケージのドキュメント: https://pkg.go.dev/crypto/tls - Go言語
net
パッケージのドキュメント: https://pkg.go.dev/net - Go言語
net.Dialer
のドキュメント: https://pkg.go.dev/net#Dialer - Go言語
time
パッケージのドキュメント: https://pkg.go.dev/time
参考にした情報源リンク
- Go CL 68920045:
crypto/tls: add DialWithDialer.
(このコミットのChange-ID): https://golang.org/cl/68920045 - Goのソースコード (GitHub): https://github.com/golang/go
- Go言語の公式ドキュメント: https://go.dev/doc/
- Go言語におけるネットワークプログラミングの基本概念に関する一般的な情報源 (例: Go by Example, Go言語の書籍など)
- TLS/SSLプロトコルに関する一般的な情報源 (例: RFC 5246など)