[インデックス 10528] ファイルの概要
このコミットは、Go言語の実験的なSSHパッケージ (exp/ssh
) におけるコードのリファクタリングに関するものです。具体的には、SSHチャネルを開くための内部関数 openChan
の利用方法が変更され、NewSession
メソッド内にそのロジックが直接統合されました。これにより、コードの重複が解消され、NewSession
の責務がより明確になっています。
コミット
commit 4cc64bd5bf54a89ec83d70e562c63a6e4810804b
Author: Dave Cheney <dave@cheney.net>
Date: Mon Nov 28 15:42:47 2011 -0500
exp/ssh: move openChan to NewSession
openChan was only being called by NewSession, Dial has
its own version.
R=gustav.paul, agl, rsc
CC=golang-dev
https://golang.org/cl/5435071
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/4cc64bd5bf54a89ec83d70e562c63a6e4810804b
元コミット内容
exp/ssh: move openChan to NewSession
openChan
は NewSession
からのみ呼び出されており、Dial
は独自のバージョンを持っていたため、openChan
のロジックを NewSession
に移動しました。
変更の背景
このコミットの背景には、コードの重複排除と責務の明確化というソフトウェア設計の原則があります。元の実装では、ClientConn
型に openChan
というメソッドが存在し、これはSSHチャネルを開く汎用的な役割を担っていました。しかし、コミットメッセージが示唆するように、この openChan
メソッドは実質的に NewSession
メソッドからしか呼び出されていませんでした。一方で、Dial
のような他のチャネル開設に関連する操作は、openChan
とは異なる独自のチャネル開設ロジックを持っていました。
このような状況は、以下のような問題を引き起こす可能性があります。
- コードの重複(潜在的):
openChan
とDial
のチャネル開設ロジックが異なる場合、将来的に同様のチャネル開設が必要になった際に、どちらのパターンを参考にすべきか不明瞭になり、結果的に似たようなコードが複数箇所に散らばる可能性があります。 - 責務の曖昧さ:
ClientConn.openChan
が汎用的なチャネル開設メソッドであるにもかかわらず、特定のNewSession
のためだけに存在しているように見えるため、その責務が曖昧になります。 - 保守性の低下:
openChan
がNewSession
以外で使われないのであれば、NewSession
の内部にそのロジックを直接組み込むことで、NewSession
の動作を理解するために別のメソッド定義を参照する必要がなくなり、コードの可読性と保守性が向上します。
このコミットは、openChan
のロジックを NewSession
に直接インライン化することで、これらの問題を解決し、コードベースをよりクリーンで理解しやすいものにすることを目的としています。
前提知識の解説
SSHプロトコルにおけるチャネル (Channels)
SSH (Secure Shell) プロトコルは、セキュアなリモートアクセスを提供するためのプロトコルです。その中核的な概念の一つに「チャネル (Channels)」があります。
- 多重化 (Multiplexing): SSH接続は単一のTCP接続上で複数の論理的な「チャネル」を多重化して使用します。これにより、一つのSSHセッション内で、シェルセッション、ポートフォワーディング、X11転送、ファイル転送 (SCP/SFTP) など、異なる種類の通信を同時に行うことができます。
- チャネルの開設: クライアントまたはサーバーは、特定の目的のために新しいチャネルを開設することを要求できます。例えば、クライアントがリモートシェルを実行したい場合、「セッション」チャネルを開設する要求を送信します。
- チャネルの種類: RFC 4250 4.9.1 には、標準的なチャネルの種類が定義されています。最も一般的なのは「session」チャネルで、これはシェル、コマンド実行、サブシステム実行などに使用されます。
- フロー制御: 各チャネルは独立したフロー制御メカニズムを持っています。これは、
window
とpacket size
という概念によって管理されます。受信側は、送信側が送信できるデータの最大量(ウィンドウサイズ)を通知し、送信側はその範囲内でデータを送信します。これにより、受信側のバッファオーバーフローを防ぎます。PeersWindow
: リモートピアがこのチャネルで受信できるバイト数。MaxPacketSize
: リモートピアがこのチャネルで送信できる単一のパケットの最大サイズ。
Go言語の exp/ssh
パッケージ
Go言語の標準ライブラリには、SSHクライアントおよびサーバーを実装するための golang.org/x/crypto/ssh
パッケージ(以前は exp/ssh
として実験的に提供されていた)が含まれています。このパッケージは、SSHプロトコルの低レベルな詳細を抽象化し、GoプログラムからSSH接続を容易に扱えるようにします。
ClientConn
: SSHクライアント接続を表す型です。この型を通じて、リモートSSHサーバーとの間でチャネルを開設したり、グローバルなリクエストを送信したりします。Session
: SSHプロトコルにおける「セッション」チャネルを表す型です。これは通常、リモートコマンドの実行、シェルへのアクセス、サブシステムの起動などに使用されます。ClientConn.NewSession()
メソッドによって作成されます。clientChan
:exp/ssh
パッケージ内部で、個々のSSHチャネルの低レベルな状態(チャネルID、ウィンドウサイズ、パケットサイズなど)を管理するために使用される構造体です。msgChannelOpen
: SSHプロトコルでチャネル開設要求を送信する際に使用されるメッセージタイプです。channelOpenConfirmMsg
: チャネル開設要求が成功した際にサーバーから返される確認メッセージです。channelOpenFailureMsg
: チャネル開設要求が失敗した際にサーバーから返される失敗メッセージです。
リファクタリングの原則
このコミットは、ソフトウェア開発における一般的なリファクタリングの原則に基づいています。
- DRY (Don't Repeat Yourself): コードの重複を避ける。同じロジックが複数箇所に存在すると、変更が必要になった際にすべての箇所を修正する必要があり、バグの温床となる可能性があります。
- 単一責任の原則 (Single Responsibility Principle - SRP): 各モジュールや関数は、単一の明確な責任を持つべきである。これにより、コードの理解、テスト、変更が容易になります。このコミットでは、
openChan
が実質的にNewSession
の一部としてしか機能していなかったため、NewSession
がチャネル開設の責任を完全に持つように変更されました。 - インライン化 (Inlining): 小さな関数や、特定の呼び出し元からしか使われない関数を、その呼び出し元に直接展開すること。これにより、関数呼び出しのオーバーヘッドを減らし、コードの局所性を高めることができます。
技術的詳細
このコミットの技術的な核心は、ClientConn.openChan
メソッドの削除と、その内部ロジックを ClientConn.NewSession
メソッドに直接組み込むことです。
ClientConn.openChan
の役割と削除の理由
元の ClientConn.openChan(typ string)
メソッドは、指定されたチャネルタイプ (typ
) で新しいSSHチャネルを開設する汎用的な役割を担っていました。このメソッドは以下の主要なステップを実行していました。
- 新しい
clientChan
インスタンスの作成。 msgChannelOpen
メッセージの構築と送信。このメッセージには、チャネルタイプ、クライアント側のチャネルID、初期ウィンドウサイズ、最大パケットサイズが含まれます。- サーバーからの応答(
channelOpenConfirmMsg
またはchannelOpenFailureMsg
)を待機。 - 応答に基づいて、
clientChan
のピアIDとウィンドウサイズを設定するか、エラーを返す。
コミットメッセージにあるように、この openChan
は NewSession
からしか呼び出されていませんでした。また、Dial
のような他の接続確立プロセスは、openChan
を使用せず、独自のチャネル開設ロジックを持っていました。これは、openChan
が汎用的なチャネル開設メソッドとして設計されていたにもかかわらず、実際には特定のユースケース(セッションチャネルの開設)に特化して使用されていたことを意味します。
この状況を改善するため、openChan
を削除し、そのロジックを NewSession
に直接移動することで、以下の利点が得られます。
- コードの局所性向上:
NewSession
の動作を理解するために、別のopenChan
メソッドの定義を参照する必要がなくなります。チャネル開設のロジックがNewSession
の内部に直接存在するため、NewSession
のコードを読むだけで、セッションチャネルがどのように開設されるかを完全に把握できます。 - 不要な抽象化の排除:
openChan
が単一の呼び出し元からしか使われていない場合、それは過剰な抽象化であると見なせます。その抽象化を排除することで、コードベースがシンプルになります。 - 将来的な混乱の回避: もし
openChan
が残っていた場合、将来的に別の種類のチャネルを開設する際に、openChan
を使うべきか、Dial
のように独自のロジックを実装すべきかという設計上の疑問が生じる可能性があります。openChan
を削除することで、この曖昧さが解消されます。
NewSession
へのロジック統合
NewSession
メソッドは、リモートホスト上で新しいインタラクティブなセッション(シェルやコマンド実行など)を開始するために使用されます。このコミットにより、NewSession
は以下のステップを直接実行するようになりました。
c.newChan(c.transport)
を呼び出して、新しいclientChan
インスタンスを作成します。これは、ローカル側のチャネルIDを割り当て、チャネルの状態を初期化します。msgChannelOpen
メッセージを構築し、c.writePacket
を使用してサーバーに送信します。このメッセージでは、ChanType
が"session"
に明示的に設定されます。また、初期ウィンドウサイズ (1 << 14
) と最大パケットサイズ (1 << 15
) も設定されます。これらの値はRFC 4253 6.1で推奨されているデフォルト値です。ch.msg
チャネルからサーバーからの応答を待ちます。- 受信したメッセージのタイプに基づいて処理を分岐します。
channelOpenConfirmMsg
の場合:チャネル開設が成功したことを意味します。サーバーから提供されたピアID (msg.MyId
) をch.peersId
に設定し、サーバーの初期ウィンドウサイズ (msg.MyWindow
) をch.win
チャネルに送信してフロー制御を確立します。その後、新しいSession
オブジェクトを返します。channelOpenFailureMsg
の場合:チャネル開設が失敗したことを意味します。c.chanlist.remove(ch.id)
を呼び出して、このチャネルをチャネルリストから削除し、エラーメッセージ (msg.Message
) を含むエラーを返します。- その他の予期しないメッセージの場合:同様にチャネルを削除し、予期しないメッセージタイプを含むエラーを返します。
この変更により、NewSession
はセッションチャネルの開設に関するすべてのロジックをカプセル化し、より自己完結的で理解しやすいメソッドになりました。
その他の変更点
client.go
内のコメント修正:mainLoop
のスペルミス (mainLoop
->mainLoop
) や、chanlist
のコメント内の参照 (ClientConn.mainloop
->ClientConn.mainLoop
) が修正されています。- デバッグ出力の改善:
mainLoop
内の未処理メッセージのデバッグ出力が、メッセージの型 (%T
) と値 (%v
) を両方表示するように変更され、デバッグ時の情報量が増加しています。
コアとなるコードの変更箇所
src/pkg/exp/ssh/client.go
--- a/src/pkg/exp/ssh/client.go
+++ b/src/pkg/exp/ssh/client.go
@@ -177,35 +177,7 @@ func (c *ClientConn) kexDH(group *dhGroup, hashFunc crypto.Hash, magics *handsha
return H, K, nil
}
-// openChan opens a new client channel. The most common session type is "session".
-// The full set of valid session types are listed in RFC 4250 4.9.1.\n-func (c *ClientConn) openChan(typ string) (*clientChan, error) {\n-\tch := c.newChan(c.transport)\n-\tif err := c.writePacket(marshal(msgChannelOpen, channelOpenMsg{\n-\t\tChanType: typ,\n-\t\tPeersId: ch.id,\n-\t\tPeersWindow: 1 << 14,\n-\t\tMaxPacketSize: 1 << 15, // RFC 4253 6.1\n-\t})); err != nil {\n-\t\tc.chanlist.remove(ch.id)\n-\t\treturn nil, err\n-\t}\n-\t// wait for response\n-\tswitch msg := (<-ch.msg).(type) {\n-\tcase *channelOpenConfirmMsg:\n-\t\tch.peersId = msg.MyId\n-\t\tch.win <- int(msg.MyWindow)\n-\tcase *channelOpenFailureMsg:\n-\t\tc.chanlist.remove(ch.id)\n-\t\treturn nil, errors.New(msg.Message)\n-\tdefault:\n-\t\tc.chanlist.remove(ch.id)\n-\t\treturn nil, errors.New(\"Unexpected packet\")\n-\t}\n-\treturn ch, nil
-}
-
-// mainloop reads incoming messages and routes channel messages
+// mainLoop reads incoming messages and routes channel messages
// to their respective ClientChans.
func (c *ClientConn) mainLoop() {
// TODO(dfc) signal the underlying close to all channels
@@ -271,7 +243,7 @@ func (c *ClientConn) mainLoop() {
case *windowAdjustMsg:
c.getChan(msg.PeersId).win <- int(msg.AdditionalBytes)
default:
- fmt.Printf("mainLoop: unhandled %#v\n", msg)
+ fmt.Printf("mainLoop: unhandled message %T: %v\n", msg, msg)
}
}
}
@@ -347,7 +319,7 @@ type chanlist struct {
// protects concurrent access to chans
sync.Mutex
// chans are indexed by the local id of the channel, clientChan.id.
- // The PeersId value of messages received by ClientConn.mainloop is
+ // The PeersId value of messages received by ClientConn.mainLoop is
// used to locate the right local clientChan in this slice.
chans []*clientChan
}
src/pkg/exp/ssh/session.go
--- a/src/pkg/exp/ssh/session.go
+++ b/src/pkg/exp/ssh/session.go
@@ -277,11 +277,29 @@ func (s *Session) stderr() error {
// NewSession returns a new interactive session on the remote host.
func (c *ClientConn) NewSession() (*Session, error) {
- ch, err := c.openChan("session")
- if err != nil {
+ ch := c.newChan(c.transport)
+ if err := c.writePacket(marshal(msgChannelOpen, channelOpenMsg{
+ ChanType: "session",
+ PeersId: ch.id,
+ PeersWindow: 1 << 14,
+ MaxPacketSize: 1 << 15, // RFC 4253 6.1
+ })); err != nil {
+ c.chanlist.remove(ch.id)
return nil, err
}
- return &Session{
- clientChan: ch,
- }, nil
+ // wait for response
+ 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)
+ }
+ c.chanlist.remove(ch.id)
+ return nil, fmt.Errorf("ssh: unexpected message %T: %v", msg, msg)
}
コアとなるコードの解説
src/pkg/exp/ssh/client.go
の変更点
openChan
メソッドの削除:ClientConn
型からopenChan
メソッドが完全に削除されました。これに伴い、関連するコメントも削除されています。これは、このメソッドがもはや独立した汎用的な機能として存在しないことを示しています。- コメントの修正:
mainLoop
関数のコメントで、mainloop
のスペルがmainLoop
に修正されました。chanlist
構造体のコメントで、ClientConn.mainloop
の参照がClientConn.mainLoop
に修正されました。これは、関数名の変更に合わせた整合性のある修正です。
- デバッグ出力の改善:
mainLoop
内のdefault
ケース(未処理のメッセージ)におけるfmt.Printf
のフォーマットが変更されました。- 変更前:
fmt.Printf("mainLoop: unhandled %#v\n", msg)
- 変更後:
fmt.Printf("mainLoop: unhandled message %T: %v\n", msg, msg)
この変更により、未処理のメッセージが出力される際に、そのメッセージのGoの型 (%T
) と値 (%v
) の両方が表示されるようになり、デバッグ時の情報がより詳細になりました。
- 変更前:
src/pkg/exp/ssh/session.go
の変更点
NewSession
メソッドのリファクタリング:- 変更前は
c.openChan("session")
を呼び出してチャネルを開設していました。 - 変更後は、
openChan
の内部ロジックがNewSession
メソッドに直接インライン化されました。 - 具体的には、以下のステップが
NewSession
の中で直接実行されるようになりました。ch := c.newChan(c.transport)
: 新しいclientChan
を作成します。if err := c.writePacket(marshal(msgChannelOpen, channelOpenMsg{...})); err != nil { ... }
:msgChannelOpen
メッセージを構築し、サーバーに送信します。ChanType
は"session"
に固定され、PeersId
、PeersWindow
、MaxPacketSize
が設定されます。エラーが発生した場合は、c.chanlist.remove(ch.id)
でチャネルをリストから削除し、エラーを返します。msg := <-ch.msg
: サーバーからの応答を待ちます。switch msg := msg.(type) { ... }
: 受信したメッセージの型に基づいて処理を分岐します。channelOpenConfirmMsg
の場合:ch.peersId
とch.win
を設定し、成功したSession
オブジェクトを返します。channelOpenFailureMsg
の場合:c.chanlist.remove(ch.id)
でチャネルを削除し、fmt.Errorf
を使用して具体的なエラーメッセージを含むエラーを返します。- その他の場合:
c.chanlist.remove(ch.id)
でチャネルを削除し、予期しないメッセージタイプを含むエラーを返します。
- 変更前は
この変更により、NewSession
はセッションチャネルの開設に関するすべての詳細を自身で処理するようになり、openChan
という中間的な抽象化が不要になりました。これにより、コードの依存関係が減り、NewSession
の動作がより明確になりました。
関連リンク
- RFC 4250 - The Secure Shell (SSH) Protocol Assigned Numbers: https://www.rfc-editor.org/rfc/rfc4250 (特に 4.9.1 Channel Types)
- RFC 4253 - The Secure Shell (SSH) Transport Layer Protocol: https://www.rfc-editor.org/rfc/rfc4253 (特に 6.1 Maximum Packet Length and Window Size)
- Go言語の
x/crypto/ssh
パッケージ (現在の場所): https://pkg.go.dev/golang.org/x/crypto/ssh
参考にした情報源リンク
- Go言語の公式ドキュメント
- SSHプロトコルに関するRFCドキュメント
- ソフトウェアリファクタリングに関する一般的な知識