[インデックス 10622] ファイルの概要
コミット
このコミット bbbd41f4fff790e9a340a4be77c3c05f37491273
は、Go言語の実験的なSSHパッケージ (exp/ssh
) におけるクライアントチャネルのオープンロジックを簡素化することを目的としています。特に、チャネルのクローズ処理とブロッキング動作に関する未解決のTODOを解決するための一連の変更の第一弾として位置づけられています。
主な変更点は以下の2点です。
peersId
の割り当ての一元化: これまで複雑だったpeersId
の割り当て処理が、一箇所で行われるように簡素化されました。これにより、部分的に作成されたclientChan
の構造が導入されています。clientChan.stdin/out/err
の早期作成: チャネルがオープンされる際にclientChan
の標準入出力 (stdin
,stdout
,stderr
) が作成されるようになりました。これにより、tcpchan
やSession
といったチャネルのコンシューマが、関連するリーダー/ライターに自身を接続するだけで済むようになり、コードが簡素化されます。
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/bbbd41f4fff790e9a340a4be77c3c05f37491273
元コミット内容
commit bbbd41f4fff790e9a340a4be77c3c05f37491273
Author: Dave Cheney <dave@cheney.net>
Date: Tue Dec 6 09:33:23 2011 -0500
exp/ssh: simplify client channel open logic
This is part one of a small set of CL's that aim to resolve
the outstanding TODOs relating to channel close and blocking
behavior.
Firstly, the hairy handling of assigning the peersId is now
done in one place. The cost of this change is the slightly
paradoxical construction of the partially created clientChan.
Secondly, by creating clientChan.stdin/out/err when the channel
is opened, the creation of consumers like tcpchan and Session
is simplified; they just have to wire themselves up to the
relevant readers/writers.
R=agl, gustav.paul, rsc
CC=golang-dev
https://golang.org/cl/5448073
変更の背景
このコミットは、Go言語の実験的なSSHパッケージ (exp/ssh
) における既存の課題、特にチャネルのクローズ処理とブロッキング動作に関する未解決のTODO(To Doリスト)を解決するための一連の変更の一部として行われました。
SSHプロトコルでは、複数の論理的な「チャネル」を単一のTCP接続上で多重化して使用します。これらのチャネルは、シェルセッション、ポートフォワーディング、X11転送など、様々な目的で使用されます。各チャネルは独立したデータストリームを持ち、そのライフサイクル(オープン、データ転送、クローズ)を適切に管理する必要があります。
以前の実装では、クライアントチャネルのオープンロジック、特にリモートピアからのチャネルID (peersId
) の割り当てや、チャネルの入出力ストリームの管理が複雑で、コードの可読性や保守性を低下させていました。また、チャネルのクローズ時やデータ転送時のブロッキング動作に関する問題も存在していました。
このコミットは、これらの問題を解決し、より堅牢で理解しやすいSSHクライアントの実装を目指すための第一歩として、チャネルオープン時の内部構造を簡素化することに焦点を当てています。具体的には、peersId
の割り当てを一元化し、チャネルの入出力ストリームをより早期かつ一貫した方法で利用可能にすることで、後続のチャネル利用コードの複雑さを軽減しようとしています。
前提知識の解説
このコミットの変更内容を理解するためには、以下の概念について基本的な知識が必要です。
-
SSH (Secure Shell) プロトコル: SSHは、ネットワークを介して安全にコンピュータを操作するためのプロトコルです。クライアントとサーバー間で暗号化された通信チャネルを確立し、リモートコマンド実行、ファイル転送、ポートフォワーディングなど、様々なサービスを提供します。SSHプロトコルは、複数の論理的な「チャネル」を単一の物理的なTCP接続上で多重化する能力を持っています。
-
SSHチャネル: SSHプロトコルにおける「チャネル」は、クライアントとサーバー間で確立される論理的なデータストリームです。例えば、シェルセッション、SCP/SFTPによるファイル転送、TCPポートフォワーディングなどは、それぞれ異なるチャネルとして扱われます。各チャネルは独立したフロー制御(ウィンドウサイズ)を持ち、双方向のデータ転送が可能です。
-
peersId
: SSHチャネルは、クライアント側とサーバー側の両方にそれぞれローカルなチャネルIDを持ちます。peersId
は、リモートピア(相手側)がそのチャネルに対して割り当てたIDを指します。クライアントがチャネルをオープンする際、サーバーは自身のチャネルIDをクライアントに通知し、それがクライアント側のpeersId
となります。このpeersId
を使用して、クライアントは特定のチャネル宛のメッセージを識別します。 -
Go言語の
io.Reader
とio.Writer
インターフェース: Go言語におけるio.Reader
とio.Writer
は、それぞれデータの読み込みと書き込みのための基本的なインターフェースです。io.Reader
はRead(p []byte) (n int, err error)
メソッドを持ち、データを読み込みます。io.Writer
はWrite(p []byte) (n int, err error)
メソッドを持ち、データを書き込みます。 これらのインターフェースは、様々なデータソースやシンク(ファイル、ネットワーク接続、メモリバッファなど)に対して統一的なI/O操作を提供するために広く利用されます。
-
Go言語のチャネル (
chan
): Go言語のチャネルは、ゴルーチン間で値を送受信するための通信メカニズムです。チャネルは、Goにおける並行処理の基本的な構成要素であり、安全なデータ共有と同期を可能にします。このコミットでは、データパケットやウィンドウ調整メッセージの送受信にチャネルが使用されています。 -
packetWriter
:packetWriter
は、SSHプロトコルにおける低レベルのパケット送信機能を提供するインターフェースまたは構造体であると推測されます。SSHのデータは、特定のフォーマットに従ってパケットにカプセル化されて送受信されます。packetWriter
は、これらのパケットをネットワーク接続に書き込む役割を担います。 -
clientChan
構造体:clientChan
は、SSHクライアント側で個々の論理チャネルの状態を管理するための主要な構造体です。この構造体は、チャネルのローカルID、リモートピアのID (peersId
)、入出力バッファ、およびチャネル関連のメッセージを受信するチャネルなどを保持します。 -
chanWriter
とchanReader
構造体: これらは、clientChan
の上で動作するio.Writer
およびio.Reader
の実装です。chanWriter
は、リモートプロセスへの標準入力 (stdin
) のように、クライアントからサーバーへデータを書き込む役割を担います。chanReader
は、リモートプロセスからの標準出力 (stdout
) や標準エラー出力 (stderr
) のように、サーバーからクライアントへデータを読み込む役割を担います。 これらは、SSHチャネルのフロー制御(ウィンドウサイズ調整)を考慮しながら、データの送受信を行います。
技術的詳細
このコミットは、Goの実験的なSSHパッケージにおけるクライアントチャネルのオープンと管理の方法を根本的に変更しています。主要な技術的変更点は以下の通りです。
-
clientChan
構造体の再設計とnewClientChan
の変更:- 以前の
clientChan
は、data
(標準出力)、dataExt
(標準エラー出力)、win
(ウィンドウ調整) の各チャネルを直接メンバーとして持っていました。これらは[]byte
やint
を送受信するGoチャネルでした。 - 新しい
clientChan
では、これらの直接的なチャネルが削除され、代わりに*chanWriter
型のstdin
、*chanReader
型のstdout
、*chanReader
型のstderr
というフィールドが導入されました。これにより、clientChan
が直接I/Oストリームの概念を持つようになりました。 newClientChan
関数は、clientChan
を初期化する際に、これらのstdin
,stdout
,stderr
フィールドも初期化し、それぞれに対応するchanWriter
およびchanReader
インスタンスを割り当てます。この時点ではpeersId
はまだ不明なため、clientChan
は「部分的に作成された」状態となります。
- 以前の
-
peersId
割り当ての一元化とwaitForChannelOpenResponse
の導入:- 以前は、
peersId
の割り当てロジックがClientConn.NewSession()
やClientConn.dial()
のようなチャネルオープンを行う複数の場所で重複して存在していました。 - このコミットでは、
clientChan
にwaitForChannelOpenResponse()
という新しいメソッドが追加されました。このメソッドは、リモートピアからのchannelOpenConfirmMsg
またはchannelOpenFailureMsg
を待ち受けます。 channelOpenConfirmMsg
を受信した場合、peersId
をmsg.MyId
から取得してclientChan.peersId
に設定し、初期ウィンドウサイズ (msg.MyWindow
) をclientChan.stdin.win
チャネルに送信します。これにより、peersId
の割り当てと初期ウィンドウサイズの処理が一箇所に集約されました。ClientConn.NewSession()
とClientConn.dial()
は、チャネルオープン要求を送信した後、このwaitForChannelOpenResponse()
を呼び出すように変更されました。これにより、peersId
の「複雑なハンドリング」が解消され、コードの重複が排除されました。
- 以前は、
-
chanWriter
とchanReader
の簡素化とclientChan
への依存:- 以前の
chanWriter
とchanReader
は、それぞれpeersId
とpacketWriter
を直接メンバーとして持っていました。 - 新しい実装では、これらのフィールドが削除され、代わりに
*clientChan
型のclientChan
フィールドが追加されました。これにより、chanWriter
とchanReader
は、自身が属するclientChan
インスタンスを通じてpeersId
やpacketWriter
にアクセスするようになりました。 - この変更により、
chanWriter
やchanReader
のインスタンス化時にpeersId
やpacketWriter
を個別に渡す必要がなくなり、Session
やtcpchan
のようなコンシューマ側でのコードが大幅に簡素化されました。例えば、io.Copy
を使用してデータを転送する際に、s.clientChan.stdin
のように直接clientChan
のI/Oストリームを利用できるようになりました。
- 以前の
-
mainLoop
におけるデータ転送ロジックの変更:ClientConn.mainLoop()
は、受信したSSHパケットを処理する主要なゴルーチンです。- 以前は、
msgChannelData
やmsgChannelExtendedData
を受信した際に、c.getChan(peersId).data <- packet[:length]
のように直接clientChan
のdata
やdataExt
チャネルにデータを送信していました。 - 変更後、
c.getChan(peersId).stdout.data <- packet[:length]
やc.getChan(peersId).stderr.data <- packet[:length]
のように、clientChan
のstdout
またはstderr
フィールド内のdata
チャネルにデータを送信するように変更されました。これは、clientChan
がI/Oストリームをカプセル化する新しい設計を反映しています。 - 同様に、
channelCloseMsg
やwindowAdjustMsg
の処理も、clientChan
のstdin
,stdout
,stderr
フィールドを通じて行われるように変更されました。
これらの変更により、SSHチャネルのオープン、データ転送、およびクローズに関するロジックがよりモジュール化され、clientChan
がチャネルのライフサイクルとI/Oストリームをより一貫して管理する中心的なエンティティとなりました。これにより、コードの複雑性が軽減され、将来的な機能追加やバグ修正が容易になることが期待されます。
コアとなるコードの変更箇所
このコミットでは、主に以下の3つのファイルが変更されています。
-
src/pkg/exp/ssh/client.go
:clientChan
構造体の定義が変更され、data
,dataExt
,win
チャネルが削除され、代わりにstdin
,stdout
,stderr
という*chanWriter
および*chanReader
型のフィールドが追加されました。newClientChan
関数が変更され、clientChan
の初期化時にstdin
,stdout
,stderr
フィールドも初期化されるようになりました。waitForChannelOpenResponse
という新しいメソッドがclientChan
に追加され、チャネルオープン確認メッセージの処理とpeersId
の割り当てを一元化しています。ClientConn.mainLoop
内のチャネルデータ (msgChannelData
,msgChannelExtendedData
)、チャネルクローズ (channelCloseMsg
)、ウィンドウ調整 (windowAdjustMsg
) の処理ロジックが、新しいclientChan
の構造に合わせて変更されました。
-
src/pkg/exp/ssh/session.go
:Session
構造体のstdin()
,stdout()
,stderr()
メソッド内で、chanWriter
やchanReader
のインスタンスを直接作成していた部分が削除されました。- 代わりに、
s.clientChan.stdin
,s.clientChan.stdout
,s.clientChan.stderr
のように、Session
が持つclientChan
のI/Oストリームを直接利用するように変更されました。これにより、Session
のコードが大幅に簡素化されています。 ClientConn.NewSession()
関数内で、チャネルオープン後の応答を待つロジックが、新しく導入されたch.waitForChannelOpenResponse()
メソッドを呼び出すように変更されました。
-
src/pkg/exp/ssh/tcpip.go
:ClientConn.dial()
関数内で、channelOpenDirectMsg
構造体の定義が関数スコープからファイルスコープに移動されました。ClientConn.dial()
関数内で、チャネルオープン後の応答を待つロジックが、ClientConn.NewSession()
と同様にch.waitForChannelOpenResponse()
メソッドを呼び出すように変更されました。tcpchan
構造体の初期化時に、Reader
とWriter
フィールドにch.stdout
とch.stdin
を直接割り当てるように変更されました。以前は、chanReader
とchanWriter
の新しいインスタンスを作成していました。
全体として、この変更は clientChan
を中心としたチャネル管理の再構築であり、Session
や tcpchan
のような高レベルのコンシューマが、より抽象化されたI/Oインターフェースを通じてチャネルと対話できるようにすることで、コードの重複と複雑さを削減しています。
コアとなるコードの解説
src/pkg/exp/ssh/client.go
の変更
clientChan
構造体の変更
type clientChan struct {
packetWriter
id, peersId uint32
// 変更前:
// data chan []byte // receives the payload of channelData messages
// dataExt chan []byte // receives the payload of channelExtendedData messages
// win chan int // receives window adjustments
// 変更後:
stdin *chanWriter // receives window adjustments
stdout *chanReader // receives the payload of channelData messages
stderr *chanReader // receives the payload of channelExtendedData messages
msg chan interface{} // incoming messages
}
clientChan
構造体から、生のGoチャネルである data
, dataExt
, win
が削除され、代わりに *chanWriter
型の stdin
と、*chanReader
型の stdout
, stderr
が追加されました。これにより、clientChan
がSSHチャネルのI/Oストリームをよりオブジェクト指向的にカプセル化するようになりました。
newClientChan
関数の変更
func newClientChan(t *transport, id uint32) *clientChan {
c := &clientChan{
packetWriter: t,
id: id,
// 変更前はここで data, dataExt, win チャネルを make していた
msg: make(chan interface{}, 16),
}
// 変更後: stdin, stdout, stderr を初期化
c.stdin = &chanWriter{
win: make(chan int, 16),
clientChan: c, // chanWriter が所属する clientChan への参照を持つ
}
c.stdout = &chanReader{
data: make(chan []byte, 16),
clientChan: c, // chanReader が所属する clientChan への参照を持つ
}
c.stderr = &chanReader{
data: make(chan []byte, 16),
clientChan: c, // chanReader が所属する clientChan への参照を持つ
}
return c
}
newClientChan
は、clientChan
を作成する際に、その stdin
, stdout
, stderr
フィールドも初期化するように変更されました。注目すべきは、chanWriter
と chanReader
が、自身が属する clientChan
インスタンスへのポインタ (clientChan: c
) を持つようになった点です。これにより、これらのI/Oヘルパーが、親チャネルの peersId
や packetWriter
にアクセスできるようになります。
waitForChannelOpenResponse
メソッドの追加
func (c *clientChan) waitForChannelOpenResponse() error {
switch msg := (<-c.msg).(type) {
case *channelOpenConfirmMsg:
// fixup peersId field
c.peersId = msg.MyId // リモートピアのチャネルIDを割り当て
c.stdin.win <- int(msg.MyWindow) // 初期ウィンドウサイズを stdin の win チャネルに送信
return nil
case *channelOpenFailureMsg:
return errors.New(safeString(msg.Message))
}
return errors.New("unexpected packet")
}
この新しいメソッドは、チャネルオープン要求に対するリモートピアからの応答を待ち、peersId
の割り当てと初期ウィンドウサイズの処理を一元的に行います。これにより、ClientConn.NewSession()
や ClientConn.dial()
からの重複するロジックが削除されました。
ClientConn.mainLoop
の変更
// 変更前: c.getChan(peersId).data <- packet[:length]
// 変更後: c.getChan(peersId).stdout.data <- packet[:length]
// 変更前: c.getChan(peersId).dataExt <- packet[:length]
// 変更後: c.getChan(peersId).stderr.data <- packet[:length]
// 変更前: close(ch.win); close(ch.data); close(ch.dataExt)
// 変更後: close(ch.stdin.win); close(ch.stdout.data); close(ch.stderr.data)
// 変更前: c.getChan(msg.PeersId).win <- int(msg.AdditionalBytes)
// 変更後: c.getChan(msg.PeersId).stdin.win <- int(msg.AdditionalBytes)
mainLoop
内のデータ転送とチャネル管理のロジックは、clientChan
の新しい構造に合わせて更新されました。データは直接 clientChan
の data
や dataExt
チャネルに送られるのではなく、stdout.data
や stderr.data
のように、対応する chanReader
の data
チャネルに送られるようになりました。同様に、チャネルのクローズやウィンドウ調整も、stdin
, stdout
, stderr
フィールドを通じて行われます。
src/pkg/exp/ssh/session.go
の変更
Session.stdin()
, Session.stdout()
, Session.stderr()
の変更
// Session.stdin() の変更
// 変更前:
// w := &chanWriter{
// packetWriter: s,
// peersId: s.peersId,
// win: s.win,
// }
// _, err := io.Copy(w, s.Stdin)
// if err1 := w.Close(); err == nil {
// err = err1
// }
// 変更後:
_, err := io.Copy(s.clientChan.stdin, s.Stdin) // clientChan の stdin を直接利用
if err1 := s.clientChan.stdin.Close(); err == nil { // clientChan の stdin を直接クローズ
err = err1
}
// Session.stdout() の変更
// 変更前:
// r := &chanReader{
// packetWriter: s,
// peersId: s.peersId,
// data: s.data,
// }
// _, err := io.Copy(s.Stdout, r)
// 変更後:
_, err := io.Copy(s.Stdout, s.clientChan.stdout) // clientChan の stdout を直接利用
// Session.stderr() の変更
// 変更前:
// r := &chanReader{
// packetWriter: s,
// peersId: s.peersId,
// data: s.dataExt,
// }
// _, err := io.Copy(s.Stderr, r)
// 変更後:
_, err := io.Copy(s.Stderr, s.clientChan.stderr) // clientChan の stderr を直接利用
Session
構造体は、SSHチャネル上でシェルコマンドを実行するための高レベルなインターフェースを提供します。以前は、Session
内で chanWriter
や chanReader
のインスタンスを個別に作成し、peersId
や packetWriter
を渡していました。この変更により、Session
は自身が持つ clientChan
の stdin
, stdout
, stderr
フィールドを直接 io.Copy
の引数として使用できるようになりました。これにより、Session
のI/Oロジックが大幅に簡素化され、clientChan
が提供する抽象化の恩恵を受けています。
ClientConn.NewSession()
の変更
// 変更前:
// msg := <-ch.msg
// switch msg := msg.(type) {
// case *channelOpenConfirmMsg:
// ch.peersId = msg.MyId
// ch.win <- int(msg.MyWindow)
// return &Session{
// clientChan: ch,
// }, nil
// case *channelOpenFailureMsg:
// c.chanlist.remove(ch.id)
// return nil, fmt.Errorf("ssh: channel open failed: %s", msg.Message)
// default:
// c.chanlist.remove(ch.id)
// return nil, fmt.Errorf("ssh: unexpected message %T: %v", msg, msg)
// }
// 変更後:
if err := ch.waitForChannelOpenResponse(); err != nil { // 新しいヘルパーメソッドを呼び出し
c.chanlist.remove(ch.id)
return nil, fmt.Errorf("ssh: unable to open session: %v", err)
}
return &Session{
clientChan: ch,
}, nil
ClientConn.NewSession()
は、新しいSSHセッションを確立する際に、チャネルオープン要求に対する応答を待つロジックを、新しく導入された ch.waitForChannelOpenResponse()
メソッドに委譲するようになりました。これにより、チャネルオープン時のエラーハンドリングと peersId
の割り当てロジックが clientChan
内部にカプセル化され、NewSession
のコードがよりクリーンになりました。
src/pkg/exp/ssh/tcpip.go
の変更
ClientConn.dial()
の変更
// 変更前:
// switch msg := (<-ch.msg).(type) {
// case *channelOpenConfirmMsg:
// ch.peersId = msg.MyId
// ch.win <- int(msg.MyWindow)
// case *channelOpenFailureMsg:
// c.chanlist.remove(ch.id)
// return nil, errors.New("ssh: error opening remote TCP connection: " + msg.Message)
// default:
// c.chanlist.remove(ch.id)
// return nil, errors.New("ssh: unexpected packet")
// }
// 変更後:
if err := ch.waitForChannelOpenResponse(); err != nil { // 新しいヘルパーメソッドを呼び出し
c.chanlist.remove(ch.id)
return nil, fmt.Errorf("ssh: unable to open direct tcpip connection: %v", err)
}
return &tcpchan{
clientChan: ch,
// 変更前はここで chanReader と chanWriter の新しいインスタンスを作成していた
// 変更後: clientChan の stdout と stdin を直接利用
Reader: ch.stdout,
Writer: ch.stdin,
}, nil
ClientConn.dial()
は、SSH経由で直接TCP接続を確立するためのメソッドです。このメソッドも ClientConn.NewSession()
と同様に、チャネルオープン応答の処理を ch.waitForChannelOpenResponse()
に委譲するようになりました。さらに、tcpchan
の初期化時に、Reader
と Writer
フィールドに clientChan
の stdout
と stdin
を直接割り当てることで、コードがより簡潔になりました。これは、clientChan
がI/Oストリームを直接提供するようになったことの直接的な恩恵です。
これらの変更は、SSHチャネルの内部表現とI/O処理をより一貫性のあるオブジェクト指向的な方法で管理することで、コードの複雑性を軽減し、将来的な拡張性を高めることを目的としています。
関連リンク
- Go CL 5448073: https://golang.org/cl/5448073 このコミットの元となったGoのコードレビューシステム (Gerrit) のチェンジリストです。詳細な議論や以前のバージョンを確認できます。
参考にした情報源リンク
- Go言語の
exp/ssh
パッケージのソースコード: このコミットが適用された当時のexp/ssh
パッケージのソースコードは、Goのバージョン管理システムで確認できます。 - RFC 4254 - The Secure Shell (SSH) Connection Protocol: SSHプロトコルのチャネルに関する詳細な仕様は、このRFCで定義されています。特に、チャネルのオープン、データ転送、フロー制御に関するセクションが関連します。 https://www.rfc-editor.org/rfc/rfc4254
- Go言語の
io
パッケージのドキュメント:io.Reader
やio.Writer
インターフェースに関する公式ドキュメントは、GoのI/Oモデルを理解する上で不可欠です。 https://pkg.go.dev/io - Go言語のチャネルに関するドキュメント: Goの並行処理におけるチャネルの概念と使用法に関する公式ドキュメントです。 https://go.dev/tour/concurrency/2 (Go TourのConcurrencyセクション) https://go.dev/blog/pipelines (Goブログのパイプラインに関する記事)