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

[インデックス 10510] ファイルの概要

このコミットは、Go言語の実験的なSSHパッケージ (exp/ssh) における、SSHチャネルメッセージのチャネルIDの取り扱いに関する重要な修正です。具体的には、一部のチャネルメッセージが、SSHプロトコルの仕様に反して、送信側のローカルチャネルIDを誤って使用していた問題を修正し、受信側のリモートチャネルIDを使用するように変更しています。

コミット

commit d859d7deee0845433b9e9770a99c6bcdbed3c920
Author: Gustav Paul <gustav.paul@gmail.com>
Date:   Sun Nov 27 09:59:20 2011 -0500

    exp/ssh: messages now contain remote channel's id instead of local id
    
    According to http://www.ietf.org/rfc/rfc4254.txt most channel messages contain the channel id of the recipient channel, not the sender id. This allows the recipient connection multiplexer to route the message to the correct channel.
    
    This changeset fixes several messages that incorrectly send the local channel id instead of the remote channel's id.
    
    While sessions were being created and closed in sequence channels in the channel pool were freed and reused on the server side of the connection at the same rate as was done on the client, so the channel local and remote channel ids always corresponded. As soon as I had concurrent sessions on the same clientConn the server started to complain of 'uknown channel id N' where N is the local channel id, which is actually paired with server channel id K.
    
    R=golang-dev, dave, rsc, agl
    CC=golang-dev
    https://golang.org/cl/5433063

GitHub上でのコミットページへのリンク

https://github.com/golang/go/commit/d859d7deee0845433b9e9770a99c6bcdbed3c920

元コミット内容

exp/ssh: messages now contain remote channel's id instead of local id

このコミットは、SSHチャネルメッセージがローカルチャネルIDではなくリモートチャネルIDを含むように修正します。

RFC 4254によると、ほとんどのチャネルメッセージは、送信者IDではなく、受信者チャネルのチャネルIDを含みます。これにより、受信側の接続マルチプレクサはメッセージを正しいチャネルにルーティングできます。

この変更セットは、リモートチャネルIDの代わりにローカルチャネルIDを誤って送信していたいくつかのメッセージを修正します。

セッションが順番に作成および閉じられている間は、チャネルプール内のチャネルは、クライアント側と同じ速度で接続のサーバー側で解放および再利用されていたため、チャネルのローカルIDとリモートIDは常に一致していました。しかし、同じ clientConn で複数のセッションを同時に使用し始めると、サーバーは「不明なチャネルID N」という苦情を出し始めました。ここでNはローカルチャネルIDであり、実際にはサーバーチャネルID Kとペアになっています。

変更の背景

SSHプロトコルでは、単一のTCP接続上で複数の論理的な「チャネル」を多重化(マルチプレクス)して通信を行います。これにより、例えば、シェルセッション、ポートフォワーディング、ファイル転送など、異なる種類の通信を同時に効率的に処理できます。各チャネルは一意のIDを持ち、通信の相手側(ピア)もそのチャネルに対して独自のIDを持ちます。

このコミットの背景にある問題は、Goの exp/ssh パッケージが、SSHプロトコルRFC 4254のチャネルメッセージに関する規定を一部誤って実装していたことにあります。RFC 4254のセクション 5.1 "Channel Open" およびセクション 5.2 "Channel Close" などに記載されているように、チャネルメッセージは通常、受信側のチャネルID(つまり、メッセージの送信元から見た相手側のチャネルID)を含める必要があります。これは、受信側のSSH実装が、どの論理チャネルにそのメッセージをルーティングすべきかを識別するために必要です。

コミットメッセージによると、これまでの実装では、一部のメッセージで送信側のローカルチャネルIDを誤って使用していました。セッションが逐次的に(一つずつ)確立・終了されるような単純なシナリオでは、チャネルIDの再利用パターンにより、ローカルIDとリモートIDが偶然一致することが多く、問題が顕在化しませんでした。しかし、同じSSH接続上で複数のセッションが並行して動作するようになると、チャネルIDの割り当てと再利用が複雑になり、ローカルIDとリモートIDの不一致が発生しました。この不一致が原因で、サーバー側が「不明なチャネルID」というエラーを報告し、通信が正常に行えなくなるというバグが発生しました。

この修正は、SSHプロトコル仕様への準拠を強化し、並行セッション環境下でのSSHクライアントの堅牢性と信頼性を向上させることを目的としています。

前提知識の解説

SSHプロトコルとチャネル

Secure Shell (SSH) は、ネットワークを介して安全にコンピュータを操作するためのプロトコルです。SSHは、認証、暗号化、データ転送のセキュリティを提供します。SSHプロトコルの中核的な概念の一つが「チャネル」です。

  • チャネル (Channel): SSH接続上で多重化される論理的な通信路です。各チャネルは、特定の目的(例: シェルセッション、SCP/SFTP、ポートフォワーディング)のために使用されます。
  • チャネルID (Channel ID): 各チャネルは、接続の両端(クライアントとサーバー)でそれぞれ一意のIDを持ちます。クライアントがチャネルを開くと、クライアントはローカルチャネルIDを割り当て、サーバーはリモートチャネルIDを割り当てます。これらのIDは、メッセージがどのチャネルに属するかを識別するために使用されます。
  • 多重化 (Multiplexing): 単一のTCP接続上で複数のチャネルを同時に実行する機能です。これにより、リソースを効率的に利用し、複数の操作を並行して行うことができます。

SSHチャネルメッセージとIDの役割 (RFC 4254)

SSHプロトコルは、チャネルの状態遷移やデータ転送のために様々なメッセージタイプを定義しています。RFC 4254 "The Secure Shell (SSH) Connection Protocol" は、これらのチャネルメッセージの詳細を規定しています。

重要な点は、ほとんどのチャネルメッセージ(例: SSH_MSG_CHANNEL_DATA, SSH_MSG_CHANNEL_WINDOW_ADJUST, SSH_MSG_CHANNEL_EOF, SSH_MSG_CHANNEL_CLOSE など)が、メッセージの受信側が認識しているチャネルIDを含まなければならないという点です。

  • 送信側: メッセージを送信する側。
  • 受信側: メッセージを受信する側。

例えば、クライアントがサーバーにデータを送信する場合、SSH_MSG_CHANNEL_DATA メッセージには、サーバーがそのデータを受信するチャネルのID(つまり、クライアントから見たサーバー側のチャネルID、または「リモートチャネルID」)が含まれます。サーバーは、このIDを見て、どのチャネルにデータが送られてきたのかを判断し、適切な処理を行います。

この仕組みにより、受信側は、単一の接続上で多重化された多数のチャネルの中から、特定のメッセージがどのチャネル宛てであるかを正確に識別し、ルーティングすることができます。

peersIdid の違い

コミットメッセージとコード変更から、Goの exp/ssh パッケージでは、チャネルオブジェクトが自身のローカルIDと、ピア(相手側)のチャネルIDの両方を保持していることが示唆されます。

  • id: おそらく、そのチャネルオブジェクトが自身に割り当てたローカルなチャネルID。
  • peersId: ピア(相手側)がそのチャネルに割り当てたチャネルID。これがRFC 4254でメッセージに含めるべき「受信側のチャネルID」に相当します。

このコミット以前は、peersId を使用すべき箇所で id を誤って使用していたため、プロトコル違反が発生していました。

技術的詳細

このコミットは、Go言語の exp/ssh パッケージ内の複数のファイル (client.go, session.go, tcpip.go) にわたる変更を含んでいます。主な修正は、SSHチャネルメッセージのペイロード内で使用されるチャネルIDを、ローカルID (c.id または s.id) からリモートピアのID (c.peersId または s.peersId) に変更することです。

具体的に影響を受けるメッセージタイプと構造体は以下の通りです。

  1. msgChannelClose (チャネルクローズメッセージ):

    • client.goclientChan.Close() メソッド内で、チャネルを閉じる際に送信される msgChannelClose メッセージの PeersId フィールドが c.id から c.peersId に変更されました。これは、相手側に対して「このチャネルを閉じます」と通知する際に、相手側が認識しているチャネルIDを使用する必要があるためです。
  2. msgChannelData (チャネルデータメッセージ):

    • client.gochanWriter 構造体(リモートプロセスへの標準入力などを表す)の Write メソッド内で、データ送信時に使用される msgChannelData メッセージのチャネルIDが w.id から w.peersId に変更されました。これは、リモート側がデータを受信するチャネルのIDを指定するためです。
    • また、chanWriter 構造体自体の id フィールドが peersId にリネームされ、その役割が明確化されました。
  3. msgChannelEOF (チャネルEOFメッセージ):

    • client.gochanWriter.Close() メソッド内で、EOF(End-of-File)を通知する際に送信される msgChannelEOF メッセージのチャネルIDが w.id から w.peersId に変更されました。これは、相手側に対して「このチャネルからのデータ送信は終了しました」と通知する際に、相手側が認識しているチャネルIDを使用する必要があるためです。
    • chanReaderClose() メソッドから msgChannelEOF の送信が削除されています。これは、chanReader がリモートからのデータを受信する側であり、EOFを送信する役割ではないため、不要なコードが削除されたと考えられます。
  4. msgChannelWindowAdjust (チャネルウィンドウ調整メッセージ):

    • client.gochanReader 構造体(リモートプロセスからの標準出力などを表す)の Read メソッド内で、受信ウィンドウを調整する際に送信される msgChannelWindowAdjust メッセージの PeersId フィールドが r.id から r.peersId に変更されました。これは、相手側に対して「これだけ追加のデータを受信できます」と通知する際に、相手側が認識しているチャネルIDを使用する必要があるためです。
    • chanReader 構造体自体の id フィールドも peersId にリネームされ、その役割が明確化されました。
  5. セッション関連のリクエストメッセージ (session.go):

    • session.go 内の setenvRequest, ptyRequestMsg, execMsg, channelRequestMsg などのセッション関連のリクエストメッセージ構造体において、PeersId フィールドが s.id から s.peersId に変更されました。これらは、クライアントがサーバーに対して特定のセッション関連の操作(環境変数の設定、PTYの要求、コマンドの実行、シェルセッションの開始など)を要求する際に使用されます。これらのリクエストも、サーバーがどのセッション(チャネル)に対する操作であるかを識別できるように、サーバーが認識しているチャネルIDを含める必要があります。
  6. chanWriter および chanReader の初期化 (session.go, tcpip.go):

    • session.goSession.stdin(), Session.stdout(), Session.stderr() メソッド内で chanWriter および chanReader が初期化される際、id フィールドに s.id を渡していた箇所が peersId: s.peersId に変更されました。
    • tcpip.goClientConn.dial() メソッド内で chanReader および chanWriter が初期化される際も同様に、id: ch.idpeersId: ch.peersId に変更されました。

これらの変更は、SSHプロトコルのチャネルIDの取り扱いに関する基本的な原則(メッセージは受信側のチャネルIDを含むべきである)に準拠するためのものです。これにより、特に複数のチャネルが並行して動作する複雑なシナリオにおいて、SSH接続の安定性と信頼性が向上します。

コアとなるコードの変更箇所

src/pkg/exp/ssh/client.go

--- a/src/pkg/exp/ssh/client.go
+++ b/src/pkg/exp/ssh/client.go
@@ -338,7 +338,7 @@ func newClientChan(t *transport, id uint32) *clientChan {
 // Close closes the channel. This does not close the underlying connection.
 func (c *clientChan) Close() error {
 	return c.writePacket(marshal(msgChannelClose, channelCloseMsg{
-		PeersId: c.id,
+		PeersId: c.peersId,
 	}))
 }
 
@@ -384,7 +384,7 @@ func (c *chanlist) remove(id uint32) {
 // A chanWriter represents the stdin of a remote process.
 type chanWriter struct {
 	win          chan int // receives window adjustments
-	id           uint32   // this channel's id
+	peersId      uint32   // the peers id
 	rwin         int      // current rwin size
 	packetWriter          // for sending channelDataMsg
 }
@@ -403,7 +403,7 @@ func (w *chanWriter) Write(data []byte) (n int, err) {
 		n = len(data)
 		packet := make([]byte, 0, 9+n)
 		packet = append(packet, msgChannelData,
-			byte(w.id)>>24, byte(w.id)>>16, byte(w.id)>>8, byte(w.id),
+			byte(w.peersId)>>24, byte(w.peersId)>>16, byte(w.peersId)>>8, byte(w.peersId),
 			byte(n)>>24, byte(n)>>16, byte(n)>>8, byte(n))
 		err = w.writePacket(append(packet, data...))
 		w.rwin -= n
@@ -413,7 +413,7 @@ func (w *chanWriter) Write(data []byte) (n int, err) {
 }
 
 func (w *chanWriter) Close() error {
-	return w.writePacket(marshal(msgChannelEOF, channelEOFMsg{w.id}))
+	return w.writePacket(marshal(msgChannelEOF, channelEOFMsg{w.peersId}))
 }
 
 // A chanReader represents stdout or stderr of a remote process.
@@ -422,8 +422,8 @@ 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
-	id           uint32
-	packetWriter // for sending windowAdjustMsg
+	peersId      uint32      // the peers id
+	packetWriter             // for sending windowAdjustMsg
 	buf          []byte
 }
 
@@ -435,7 +435,7 @@ func (r *chanReader) Read(data []byte) (int, error) {
 			n := copy(data, r.buf)
 			r.buf = r.buf[n:]
 			msg := windowAdjustMsg{
-				PeersId:         r.id,
+				PeersId:         r.peersId,
 				AdditionalBytes: uint32(n),
 			}
 			return n, r.writePacket(marshal(msgChannelWindowAdjust, msg))
@@ -447,7 +447,3 @@ func (r *chanReader) Read(data []byte) (int, error) {
 	}
 	panic("unreachable")
 }
-
-func (r *chanReader) Close() error {
-	return r.writePacket(marshal(msgChannelEOF, channelEOFMsg{r.id}))
-}

src/pkg/exp/ssh/session.go

--- a/src/pkg/exp/ssh/session.go
+++ b/src/pkg/exp/ssh/session.go
@@ -53,7 +53,7 @@ type setenvRequest struct {
 // command executed by Shell or Exec.
 func (s *Session) Setenv(name, value string) error {
 	req := setenvRequest{
-		PeersId:   s.id,
+		PeersId:   s.peersId,
 		Request:   "env",
 		WantReply: true,
 		Name:      name,
@@ -84,7 +84,7 @@ type ptyRequestMsg struct {
 // RequestPty requests the association of a pty with the session on the remote host.
 func (s *Session) RequestPty(term string, h, w int) error {
 	req := ptyRequestMsg{
-		PeersId:   s.id,
+		PeersId:   s.peersId,
 		Request:   "pty-req",
 		WantReply: true,
 		Term:      term,
@@ -116,7 +116,7 @@ func (s *Session) Exec(cmd string) error {
 		return errors.New("ssh: session already started")
 	}
 	req := execMsg{
-		PeersId:   s.id,
+		PeersId:   s.peersId,
 		Request:   "exec",
 		WantReply: true,
 		Command:   cmd,
@@ -140,7 +140,7 @@ func (s *Session) Shell() error {
 		return errors.New("ssh: session already started")
 	}
 	req := channelRequestMsg{
-		PeersId:   s.id,
+		PeersId:   s.peersId,
 		Request:   "shell",
 		WantReply: true,
 	}
@@ -237,7 +237,7 @@ func (s *Session) stdin() error {
 	s.copyFuncs = append(s.copyFuncs, func() error {
 		_, err := io.Copy(&chanWriter{
 			packetWriter: s,
-			id:           s.id,
+			peersId:      s.peersId,
 			win:          s.win,
 		}, s.Stdin)
 		return err
@@ -252,7 +252,7 @@ func (s *Session) stdout() error {
 	s.copyFuncs = append(s.copyFuncs, func() error {
 		_, err := io.Copy(s.Stdout, &chanReader{
 			packetWriter: s,
-			id:           s.id,
+			peersId:      s.peersId,
 			data:         s.data,
 		})
 		return err
@@ -267,7 +267,7 @@ func (s *Session) stderr() error {
 	s.copyFuncs = append(s.copyFuncs, func() error {
 		_, err := io.Copy(s.Stderr, &chanReader{
 			packetWriter: s,
-			id:           s.id,
+			peersId:      s.peersId,
 			data:         s.dataExt,
 		})
 		return err

src/pkg/exp/ssh/tcpip.go

--- a/src/pkg/exp/ssh/tcpip.go
+++ b/src/pkg/exp/ssh/tcpip.go
@@ -86,12 +86,12 @@ func (c *ClientConn) dial(laddr string, lport int, raddr string, rport int) (*tc
 	return &tcpipConn{
 		clientChan: ch,
 		Reader: &chanReader{
 			packetWriter: ch,
-			id:           ch.id,
+			peersId:      ch.peersId,
 			data:         ch.data,
 		},
 		Writer: &chanWriter{
 			packetWriter: ch,
-			id:           ch.id,
+			peersId:      ch.peersId,
 			win:          ch.win,
 		},
 	}, nil

コアとなるコードの解説

このコミットの核心は、SSHプロトコルにおけるチャネルIDの役割の正確な理解と、それに基づく実装の修正です。

  1. id から peersId への変更:

    • client.go 内の clientChan.Close() メソッドでは、msgChannelClose メッセージを送信する際に、c.id (ローカルチャネルID) ではなく c.peersId (リモートチャネルID) を使用するように変更されました。これは、相手側(サーバー)がこのメッセージを受信した際に、どのチャネルを閉じるべきかを正確に識別できるようにするためです。
    • chanWriter および chanReader 構造体内の id フィールドが peersId にリネームされ、その役割が「ピア(相手側)のチャネルID」であることを明確にしました。
    • chanWriter.Write() (データ送信) および chanWriter.Close() (EOF送信) では、w.id ではなく w.peersId を使用して msgChannelData および msgChannelEOF メッセージを構築するように変更されました。これにより、送信されるデータやEOF通知が、リモート側が認識している正しいチャネルにルーティングされるようになります。
    • chanReader.Read() (ウィンドウ調整) では、r.id ではなく r.peersId を使用して msgChannelWindowAdjust メッセージを構築するように変更されました。これは、受信ウィンドウの調整通知が、リモート側が認識している正しいチャネルに対して行われるようにするためです。
  2. セッション関連のリクエストの修正:

    • session.go 内の Setenv, RequestPty, Exec, Shell メソッドで送信される各種リクエストメッセージ(setenvRequest, ptyRequestMsg, execMsg, channelRequestMsg)において、PeersId フィールドが s.id (ローカルセッションID) ではなく s.peersId (リモートセッションID) を参照するように変更されました。これらのリクエストは、サーバーに対して特定のセッション(チャネル)に対する操作を要求するため、サーバーが認識しているチャネルIDを使用する必要があります。
  3. chanWriter および chanReader の初期化の修正:

    • session.go および tcpip.go において、chanWriter および chanReader インスタンスを生成する際に、id フィールドに s.idch.id を直接渡すのではなく、peersId: s.peersIdpeersId: ch.peersId のように、明示的に peersId フィールドにリモートチャネルIDを割り当てるように変更されました。これにより、これらの構造体が正しくリモートチャネルIDを保持し、それ以降のメッセージ送信で正しいIDを使用できるようになります。

これらの変更は、SSHプロトコルRFC 4254の「チャネルメッセージは受信側のチャネルIDを含むべきである」という原則に厳密に準拠するためのものです。これにより、特に複数のチャネルが同時にアクティブな状況で、メッセージのルーティングが正しく行われ、"unknown channel id" のようなエラーが解消されます。これは、SSHクライアントの堅牢性と並行処理能力を向上させる上で非常に重要な修正です。

関連リンク

  • Go言語の exp/ssh パッケージ (当時の実験的なパッケージ)
  • SSHプロトコルに関するRFC群 (特に RFC 4254)

参考にした情報源リンク