Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

[インデックス 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_EOFSSH_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クライアントチャネルのクローズ動作を改善するために、以下の主要な変更を導入しています。

  1. chanReader の改善:

    • dataClosed フィールドの追加: chanReader が持つデータチャネル (data chan []byte) が既に閉じられているかどうかを追跡するためのブール値フラグです。これにより、チャネルが二重に閉じられることを防ぎます。
    • eof() メソッドの追加: chanReader のデータチャネルを安全に閉じるためのメソッドです。dataClosed フラグをチェックし、まだ閉じられていなければチャネルを閉じます。
    • handleData() メソッドの追加: リモートから受信したデータを chanReader のデータチャネルに送信するためのメソッドです。dataClosed フラグをチェックし、チャネルが閉じられていない場合にのみデータを送信します。これにより、既に閉じられたチャネルにデータを書き込もうとする競合状態を防ぎ、データがサイレントに破棄されるようにします。
  2. clientChan のクローズ状態管理の強化:

    • theyClosed フィールドの追加: リモート側がチャネルをクローズした(SSH_MSG_CHANNEL_CLOSE を受信した)ことを示すブール値フラグです。
    • weClosed フィールドの追加: クライアント側がチャネルをクローズした(SSH_MSG_CHANNEL_CLOSE を送信した)ことを示すブール値フラグです。
    • Close() メソッドのロジック変更: clientChanClose() メソッドが、weClosed フラグをチェックし、まだクローズメッセージを送信していなければ sendClose() を呼び出すように変更されました。これにより、SSH_MSG_CHANNEL_CLOSE メッセージが複数回送信されることを防ぎ、チャネルのクローズ処理が冪等になります。
  3. mainLoop におけるチャネルクローズ処理の改善:

    • channelCloseMsg のハンドリング: リモートから SSH_MSG_CHANNEL_CLOSE を受信した際に、clientChantheyClosed フラグを true に設定し、stdin.win チャネルを閉じ、stdoutstderreof() メソッドを呼び出すようになりました。さらに、もしクライアント側がまだクローズメッセージを送信していなければ(!ch.weClosed)、sendClose() を呼び出してリモートに応答するように変更されました。これにより、RFC 4254 のチャネルクローズの双方向要件がより適切に満たされます。
    • channelEOFMsg のハンドリング: リモートから SSH_MSG_CHANNEL_EOF を受信した際に、clientChanstdoutstderreof() メソッドを呼び出すようになりました。これにより、EOFメッセージが受信された時点で、対応するデータストリームが適切に終了されることが保証されます。RFC 4254 は dataExt メッセージに EOF がどのように影響するかについては明記していませんが、この変更は論理的に stderr にも EOF を適用しています。
    • disconnectMsg のハンドリング: mainLoopdisconnectMsg を受信した場合に、ループを中断する 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 構造体に theyClosedweClosed という2つのブール値フィールドが追加されました。

  • theyClosed: リモート側から SSH_MSG_CHANNEL_CLOSE メッセージを受信したかどうかを示すフラグ。
  • weClosed: クライアント側が SSH_MSG_CHANNEL_CLOSE メッセージを送信したかどうかを示すフラグ。

これらのフラグにより、チャネルのクローズ状態をより正確に追跡し、RFC 4254 で規定されている双方向のクローズハンドシェイクを適切に管理できるようになります。

chanReader 構造体と関連メソッドの変更

chanReader は、リモートプロセスの標準出力 (stdout) や標準エラー出力 (stderr) からのデータを受信する役割を担います。

  • dataClosed bool の追加: chanReaderdata チャネルが既に閉じられているかを追跡するためのフラグです。これにより、data チャネルが複数回閉じられることによるパニックを防ぎます。
  • eof() メソッドの追加:
    func (r *chanReader) eof() {
        if !r.dataClosed {
            r.dataClosed = true
            close(r.data)
        }
    }
    
    このメソッドは、data チャネルがまだ閉じられていない場合にのみ close(r.data) を呼び出し、dataClosedtrue に設定します。これにより、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
+}

clientChanClose() メソッドは、クライアント側がまだ 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 を受信した場合の処理が大幅に改善されました。
    1. ch.theyClosed = true: リモート側がチャネルを閉じたことを記録します。
    2. close(ch.stdin.win): 標準入力のウィンドウサイズ調整チャネルを閉じます。
    3. ch.stdout.eof()ch.stderr.eof(): stdoutstderrchanReader に対して eof() メソッドを呼び出し、それぞれのデータチャネルを安全に閉じます。
    4. close(ch.msg): 内部メッセージチャネルを閉じます。
    5. 双方向クローズの保証:
      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イベントに対するクライアントの応答性を向上させ、潜在的な競合状態やリソースリークを防ぐことを目的としています。

関連リンク

参考にした情報源リンク