[インデックス 10743] ファイルの概要
このコミットは、Go言語の実験的なSSHクライアントパッケージ exp/ssh
における、クライアントチャネルのクローズ動作の改善を目的としています。特に、チャネルのデータフローとクローズ処理の堅牢性を高めるための変更が含まれています。
コミット
commit 2b600f77dd19b9d04f473eb12179437afefde26a
Author: Dave Cheney <dave@cheney.net>
Date: Tue Dec 13 10:27:17 2011 -0500
exp/ssh: improve client channel close behavior
R=gustav.paul
CC=golang-dev
https://golang.org/cl/5480062
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/2b600f77dd19b9d04f473eb12179437afefde26a
元コミット内容
exp/ssh: improve client channel close behavior
R=gustav.paul
CC=golang-dev
https://golang.org/cl/5480062
変更の背景
SSHプロトコルにおいて、チャネルのクローズ処理は双方向の通信が終了したことを示す重要なステップです。RFC 4254 (The Secure Shell (SSH) Connection Protocol) のセクション 5.3 には、SSH_MSG_CHANNEL_EOF
と SSH_MSG_CHANNEL_CLOSE
メッセージに関する規定があります。
SSH_MSG_CHANNEL_EOF
: データ送信側がこれ以上データを送信しないことを示す。これは、ストリームの終端(End-Of-File)を意味し、受信側はこれ以降データが来ないことを期待する。SSH_MSG_CHANNEL_CLOSE
: チャネルが完全にクローズされることを示す。これは、双方向のデータフローが終了し、チャネルに関連するリソースが解放されるべきであることを意味する。
このコミット以前の exp/ssh
クライアントの実装では、チャネルのクローズ処理、特にリモート側からのクローズ通知(SSH_MSG_CHANNEL_CLOSE
)やEOF通知(SSH_MSG_CHANNEL_EOF
)のハンドリングが不完全であった可能性があります。具体的には、データチャネルが適切に閉じられず、データが既に閉じられたチャネルに書き込まれる可能性や、チャネルのクローズ状態が正確に追跡されていない問題があったと考えられます。
この変更の背景には、SSHチャネルのライフサイクル管理をより堅牢にし、データの一貫性とリソースの適切な解放を保証するという目的があります。特に、リモート側がチャネルをクローズした際に、クライアント側も適切に応答し、データストリームを終了させるメカニズムを強化する必要がありました。
前提知識の解説
SSHプロトコルとチャネル
SSH (Secure Shell) は、ネットワークを介して安全にコンピュータを操作するためのプロトコルです。SSHプロトコルは、単一のTCP接続上で複数の論理的な「チャネル」を多重化して使用します。これらのチャネルは、シェルセッション、ポートフォワーディング、ファイル転送など、様々な目的で使用されます。
各チャネルは独立したデータストリームを持ち、双方向の通信が可能です。チャネルのライフサイクルには、オープン、データ転送、EOF(End-Of-File)、クローズといった状態があります。
RFC 4254: The Secure Shell (SSH) Connection Protocol
RFC 4254は、SSH接続プロトコルを定義する標準ドキュメントです。このRFCは、SSHチャネルの確立、データ転送、およびクローズに関する詳細な仕様を含んでいます。
- セクション 5.2. Data Transfer: チャネルを介したデータ転送について説明しています。
SSH_MSG_CHANNEL_DATA
メッセージがデータの送信に使用されます。 - セクション 5.3. Closing a Channel: チャネルのクローズ処理について説明しています。
SSH_MSG_CHANNEL_EOF
: 送信側がこれ以上データを送信しないことを示すメッセージ。受信側は、このメッセージを受け取った後も、まだ受信していないデータがある場合はそれを受け取り続けることができます。SSH_MSG_CHANNEL_CLOSE
: チャネルが完全にクローズされることを示すメッセージ。このメッセージは、双方向のデータフローが終了したことを意味し、チャネルに関連するリソースを解放する準備ができたことを示します。両側がSSH_MSG_CHANNEL_CLOSE
を送信し、受信する必要があります。
Go言語の exp/ssh
パッケージ
exp/ssh
は、Go言語の標準ライブラリの一部として提供されていた実験的なSSHパッケージです。このパッケージは、SSHクライアントおよびサーバーの実装を提供し、GoアプリケーションでSSH機能を利用できるようにします。exp
パッケージは、将来的に安定版の golang.org/x/crypto/ssh
パッケージに統合されることを意図していました。
Go言語の chan
(チャネル)
Go言語のチャネルは、ゴルーチン間で値を送受信するためのパイプのようなものです。チャネルは、Goの並行処理モデルの基本的な要素であり、安全な並行処理を実現するために使用されます。チャネルは close()
関数で閉じることができ、閉じられたチャネルからの読み取りは、チャネルにまだ値が残っていればその値を返し、値がなければゼロ値と false
を返します。
技術的詳細
このコミットは、SSHクライアントチャネルのクローズ動作を改善するために、以下の主要な変更を導入しています。
-
chanReader
の改善:dataClosed
フィールドの追加:chanReader
が持つデータチャネル (data chan []byte
) が既に閉じられているかどうかを追跡するためのブール値フラグです。これにより、チャネルが二重に閉じられることを防ぎます。eof()
メソッドの追加:chanReader
のデータチャネルを安全に閉じるためのメソッドです。dataClosed
フラグをチェックし、まだ閉じられていなければチャネルを閉じます。handleData()
メソッドの追加: リモートから受信したデータをchanReader
のデータチャネルに送信するためのメソッドです。dataClosed
フラグをチェックし、チャネルが閉じられていない場合にのみデータを送信します。これにより、既に閉じられたチャネルにデータを書き込もうとする競合状態を防ぎ、データがサイレントに破棄されるようにします。
-
clientChan
のクローズ状態管理の強化:theyClosed
フィールドの追加: リモート側がチャネルをクローズした(SSH_MSG_CHANNEL_CLOSE
を受信した)ことを示すブール値フラグです。weClosed
フィールドの追加: クライアント側がチャネルをクローズした(SSH_MSG_CHANNEL_CLOSE
を送信した)ことを示すブール値フラグです。Close()
メソッドのロジック変更:clientChan
のClose()
メソッドが、weClosed
フラグをチェックし、まだクローズメッセージを送信していなければsendClose()
を呼び出すように変更されました。これにより、SSH_MSG_CHANNEL_CLOSE
メッセージが複数回送信されることを防ぎ、チャネルのクローズ処理が冪等になります。
-
mainLoop
におけるチャネルクローズ処理の改善:channelCloseMsg
のハンドリング: リモートからSSH_MSG_CHANNEL_CLOSE
を受信した際に、clientChan
のtheyClosed
フラグをtrue
に設定し、stdin.win
チャネルを閉じ、stdout
とstderr
のeof()
メソッドを呼び出すようになりました。さらに、もしクライアント側がまだクローズメッセージを送信していなければ(!ch.weClosed
)、sendClose()
を呼び出してリモートに応答するように変更されました。これにより、RFC 4254 のチャネルクローズの双方向要件がより適切に満たされます。channelEOFMsg
のハンドリング: リモートからSSH_MSG_CHANNEL_EOF
を受信した際に、clientChan
のstdout
とstderr
のeof()
メソッドを呼び出すようになりました。これにより、EOFメッセージが受信された時点で、対応するデータストリームが適切に終了されることが保証されます。RFC 4254 はdataExt
メッセージに EOF がどのように影響するかについては明記していませんが、この変更は論理的にstderr
にも EOF を適用しています。disconnectMsg
のハンドリング:mainLoop
がdisconnectMsg
を受信した場合に、ループを中断するbreak
ステートメントが追加されました。これにより、接続が切断された際にメインループが適切に終了するようになります。
これらの変更により、SSHチャネルのデータフローとクローズ処理がより堅牢になり、競合状態やリソースリークのリスクが低減されます。特に、リモート側からの非同期的なチャネルクローズやEOF通知に対して、クライアント側がより適切に応答できるようになります。
コアとなるコードの変更箇所
変更は src/pkg/exp/ssh/client.go
ファイルに集中しています。
func (c *ClientConn) mainLoop()
内の変更
--- a/src/pkg/exp/ssh/client.go
+++ b/src/pkg/exp/ssh/client.go
@@ -200,7 +200,7 @@ func (c *ClientConn) mainLoop() {
peersId := uint32(packet[1])<<24 | uint32(packet[2])<<16 | uint32(packet[3])<<8 | uint32(packet[4])
if length := int(packet[5])<<24 | int(packet[6])<<16 | int(packet[7])<<8 | int(packet[8]); length > 0 {
packet = packet[9:]
- c.getChan(peersId).stdout.data <- packet[:length]
+ c.getChan(peersId).stdout.handleData(packet[:length])
}
case msgChannelExtendedData:
if len(packet) < 13 {
@@ -215,7 +215,7 @@ func (c *ClientConn) mainLoop() {
// for stderr on interactive sessions. Other data types are
// silently discarded.
if datatype == 1 {
- c.getChan(peersId).stderr.data <- packet[:length]
+ c.getChan(peersId).stderr.handleData(packet[:length])
}
}
default:
@@ -228,13 +228,22 @@ func (c *ClientConn) mainLoop() {
c.getChan(msg.PeersId).msg <- msg
case *channelCloseMsg:
ch := c.getChan(msg.PeersId)
+ ch.theyClosed = true
close(ch.stdin.win)
- close(ch.stdout.data)
- close(ch.stderr.data)
+ ch.stdout.eof()
+ ch.stderr.eof()
close(ch.msg)
+ if !ch.weClosed {
+ ch.weClosed = true
+ ch.sendClose()
+ }
c.chanlist.remove(msg.PeersId)
case *channelEOFMsg:
- c.getChan(msg.PeersId).sendEOF()
+ ch := c.getChan(msg.PeersId)
+ ch.stdout.eof()
+ // RFC 4254 is mute on how EOF affects dataExt messages but
+ // it is logical to signal EOF at the same time.
+ ch.stderr.eof()
case *channelRequestSuccessMsg:
c.getChan(msg.PeersId).msg <- msg
case *channelRequestFailureMsg:
@@ -243,6 +252,8 @@ func (c *ClientConn) mainLoop() {
c.getChan(msg.PeersId).msg <- msg
case *windowAdjustMsg:
c.getChan(msg.PeersId).stdin.win <- int(msg.AdditionalBytes)
+ case *disconnectMsg:
+ break
default:
fmt.Printf("mainLoop: unhandled message %T: %v\n", msg, msg)
}
type clientChan struct
の変更
--- a/src/pkg/exp/ssh/client.go
+++ b/src/pkg/exp/ssh/client.go
@@ -295,6 +306,9 @@ type clientChan struct {
stdout *chanReader // receives the payload of channelData messages
stderr *chanReader // receives the payload of channelExtendedData messages
msg chan interface{} // incoming messages
+
+ theyClosed bool // indicates the close msg has been received from the remote side
+ weClosed bool // incidates the close msg has been sent from our side
}
func (c *clientChan) sendEOF()
の変更
--- a/src/pkg/exp/ssh/client.go
+++ b/src/pkg/exp/ssh/client.go
@@ -336,20 +350,29 @@ func (c *clientChan) waitForChannelOpenResponse() error {
return errors.New("unexpected packet")
}
-// sendEOF Sends EOF to the server. RFC 4254 Section 5.3
+// sendEOF sends EOF to the server. RFC 4254 Section 5.3
func (c *clientChan) sendEOF() error {
return c.writePacket(marshal(msgChannelEOF, channelEOFMsg{
PeersId: c.peersId,
}))
}
-// Close closes the channel. This does not close the underlying connection.
-func (c *clientChan) Close() error {
+// sendClose signals the intent to close the channel.
+func (c *clientChan) sendClose() error {
return c.writePacket(marshal(msgChannelClose, channelCloseMsg{
PeersId: c.peersId,
}))
}
+// Close closes the channel. This does not close the underlying connection.
+func (c *clientChan) Close() error {
+ if !c.weClosed {
+ c.weClosed = true
+ return c.sendClose()
+ }
+ return nil
+}
+
// Thread safe channel list.
type chanlist struct {
// protects concurrent access to chans
func (w *chanWriter) Close()
の変更
--- a/src/pkg/exp/ssh/client.go
+++ b/src/pkg/exp/ssh/client.go
@@ -421,7 +444,7 @@ func (w *chanWriter) Write(data []byte) (n int, err error) {
}
func (w *chanWriter) Close() error {
- return w.clientChan.writePacket(marshal(msgChannelEOF, channelEOFMsg{w.clientChan.peersId}))
+ return w.clientChan.sendEOF()
}
type chanReader struct
の変更
--- a/src/pkg/exp/ssh/client.go
+++ b/src/pkg/exp/ssh/client.go
@@ -430,10 +453,27 @@ type chanReader struct {
// If writes to this channel block, they will block mainLoop, making
// it unable to receive new messages from the remote side.
data chan []byte // receives data from remote
+ dataClosed bool // protects data from being closed twice
clientChan *clientChan // the channel backing this reader
buf []byte
}
+// eof signals to the consumer that there is no more data to be received.
+func (r *chanReader) eof() {
+ if !r.dataClosed {
+ r.dataClosed = true
+ close(r.data)
+ }
+}
+
+// handleData sends buf to the reader's consumer. If r.data is closed
+// the data will be silently discarded
+func (r *chanReader) handleData(buf []byte) {
+ if !r.dataClosed {
+ r.data <- buf
+ }
+}
+
// Read reads data from the remote process's stdout or stderr.
func (r *chanReader) Read(data []byte) (int, error) {
var ok bool
コアとなるコードの解説
clientChan
構造体の変更
clientChan
構造体に theyClosed
と weClosed
という2つのブール値フィールドが追加されました。
theyClosed
: リモート側からSSH_MSG_CHANNEL_CLOSE
メッセージを受信したかどうかを示すフラグ。weClosed
: クライアント側がSSH_MSG_CHANNEL_CLOSE
メッセージを送信したかどうかを示すフラグ。
これらのフラグにより、チャネルのクローズ状態をより正確に追跡し、RFC 4254 で規定されている双方向のクローズハンドシェイクを適切に管理できるようになります。
chanReader
構造体と関連メソッドの変更
chanReader
は、リモートプロセスの標準出力 (stdout
) や標準エラー出力 (stderr
) からのデータを受信する役割を担います。
dataClosed bool
の追加:chanReader
のdata
チャネルが既に閉じられているかを追跡するためのフラグです。これにより、data
チャネルが複数回閉じられることによるパニックを防ぎます。eof()
メソッドの追加:
このメソッドは、func (r *chanReader) eof() { if !r.dataClosed { r.dataClosed = true close(r.data) } }
data
チャネルがまだ閉じられていない場合にのみclose(r.data)
を呼び出し、dataClosed
をtrue
に設定します。これにより、EOFが通知された際に、データチャネルが安全かつ冪等に閉じられるようになります。handleData()
メソッドの追加:
このメソッドは、リモートから受信したデータ (func (r *chanReader) handleData(buf []byte) { if !r.dataClosed { r.data <- buf } }
buf
) をr.data
チャネルに送信します。重要なのは、!r.dataClosed
のチェックがあることです。これにより、data
チャネルが既に閉じられている場合、受信したデータはサイレントに破棄されます。これは、チャネルが閉じられた後にデータが到着する可能性のある競合状態を適切に処理するために重要です。以前は直接r.data <- packet[:length]
のようにチャネルに書き込んでいましたが、チャネルが閉じられた後に書き込もうとするとパニックが発生する可能性がありました。
clientChan.Close()
メソッドの変更
--- a/src/pkg/exp/ssh/client.go
+++ b/src/pkg/exp/ssh/client.go
@@ -343,6 +357,12 @@ func (c *clientChan) sendClose() error {
// Close closes the channel. This does not close the underlying connection.
func (c *clientChan) Close() error {
+ if !c.weClosed {
+ c.weClosed = true
+ return c.sendClose()
+ }
+ return nil
+}
clientChan
の Close()
メソッドは、クライアント側がまだ SSH_MSG_CHANNEL_CLOSE
メッセージを送信していない場合 (!c.weClosed
) にのみ sendClose()
を呼び出すように変更されました。これにより、Close()
が複数回呼び出されても、クローズメッセージが重複して送信されることを防ぎ、冪等性が保証されます。
mainLoop
におけるメッセージハンドリングの変更
mainLoop
は、SSH接続を介して受信したメッセージを処理する主要なゴルーチンです。
msgChannelData
およびmsgChannelExtendedData
のハンドリング: 以前はc.getChan(peersId).stdout.data <- packet[:length]
のように直接チャネルにデータを送信していましたが、これがc.getChan(peersId).stdout.handleData(packet[:length])
に変更されました。これにより、前述のhandleData()
メソッドのロジックが適用され、閉じられたチャネルへの書き込みが安全に処理されるようになります。channelCloseMsg
のハンドリング: リモートからSSH_MSG_CHANNEL_CLOSE
を受信した場合の処理が大幅に改善されました。ch.theyClosed = true
: リモート側がチャネルを閉じたことを記録します。close(ch.stdin.win)
: 標準入力のウィンドウサイズ調整チャネルを閉じます。ch.stdout.eof()
とch.stderr.eof()
:stdout
とstderr
のchanReader
に対してeof()
メソッドを呼び出し、それぞれのデータチャネルを安全に閉じます。close(ch.msg)
: 内部メッセージチャネルを閉じます。- 双方向クローズの保証:
このブロックは、リモートがチャネルを閉じたにもかかわらず、クライアント側がまだクローズメッセージを送信していない場合に、クライアント側からもif !ch.weClosed { ch.weClosed = true ch.sendClose() }
SSH_MSG_CHANNEL_CLOSE
を送信するようにします。これは、RFC 4254 で規定されている双方向のクローズハンドシェイクを遵守するために不可欠です。
channelEOFMsg
のハンドリング: リモートからSSH_MSG_CHANNEL_EOF
を受信した場合、ch.stdout.eof()
とch.stderr.eof()
が呼び出されます。これにより、EOFが通知された時点で、対応するデータストリームが適切に終了されることが保証されます。disconnectMsg
のハンドリング:disconnectMsg
を受信した場合にbreak
が追加されました。これにより、SSH接続全体が切断された際にmainLoop
が適切に終了し、リソースが解放されるようになります。
これらの変更は、SSHチャネルのライフサイクル管理をより堅牢にし、特に非同期的なクローズイベントやEOFイベントに対するクライアントの応答性を向上させ、潜在的な競合状態やリソースリークを防ぐことを目的としています。
関連リンク
参考にした情報源リンク
- Go言語の
exp/ssh
パッケージのソースコード (当時のもの) (コミット当時のコードベースを直接参照することは困難なため、Go 1.0 リリースブランチのexp/ssh
を参考にしました) - Go言語のチャネルに関する一般的な情報
- SSHプロトコルに関する一般的な情報
- RFC 4254 の詳細な分析
- Goのコードレビューシステム (Gerrit) の変更リスト (CL) 5480062 (現在はアクセスできない可能性がありますが、コミットメッセージに記載されています)
- Dave Cheney氏のブログやGoに関する記事 (一般的なGoのプラクティスや設計思想を理解するために参照)