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

[インデックス 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

openChanNewSession からのみ呼び出されており、Dial は独自のバージョンを持っていたため、openChan のロジックを NewSession に移動しました。

変更の背景

このコミットの背景には、コードの重複排除と責務の明確化というソフトウェア設計の原則があります。元の実装では、ClientConn 型に openChan というメソッドが存在し、これはSSHチャネルを開く汎用的な役割を担っていました。しかし、コミットメッセージが示唆するように、この openChan メソッドは実質的に NewSession メソッドからしか呼び出されていませんでした。一方で、Dial のような他のチャネル開設に関連する操作は、openChan とは異なる独自のチャネル開設ロジックを持っていました。

このような状況は、以下のような問題を引き起こす可能性があります。

  1. コードの重複(潜在的): openChanDial のチャネル開設ロジックが異なる場合、将来的に同様のチャネル開設が必要になった際に、どちらのパターンを参考にすべきか不明瞭になり、結果的に似たようなコードが複数箇所に散らばる可能性があります。
  2. 責務の曖昧さ: ClientConn.openChan が汎用的なチャネル開設メソッドであるにもかかわらず、特定の NewSession のためだけに存在しているように見えるため、その責務が曖昧になります。
  3. 保守性の低下: openChanNewSession 以外で使われないのであれば、NewSession の内部にそのロジックを直接組み込むことで、NewSession の動作を理解するために別のメソッド定義を参照する必要がなくなり、コードの可読性と保守性が向上します。

このコミットは、openChan のロジックを NewSession に直接インライン化することで、これらの問題を解決し、コードベースをよりクリーンで理解しやすいものにすることを目的としています。

前提知識の解説

SSHプロトコルにおけるチャネル (Channels)

SSH (Secure Shell) プロトコルは、セキュアなリモートアクセスを提供するためのプロトコルです。その中核的な概念の一つに「チャネル (Channels)」があります。

  • 多重化 (Multiplexing): SSH接続は単一のTCP接続上で複数の論理的な「チャネル」を多重化して使用します。これにより、一つのSSHセッション内で、シェルセッション、ポートフォワーディング、X11転送、ファイル転送 (SCP/SFTP) など、異なる種類の通信を同時に行うことができます。
  • チャネルの開設: クライアントまたはサーバーは、特定の目的のために新しいチャネルを開設することを要求できます。例えば、クライアントがリモートシェルを実行したい場合、「セッション」チャネルを開設する要求を送信します。
  • チャネルの種類: RFC 4250 4.9.1 には、標準的なチャネルの種類が定義されています。最も一般的なのは「session」チャネルで、これはシェル、コマンド実行、サブシステム実行などに使用されます。
  • フロー制御: 各チャネルは独立したフロー制御メカニズムを持っています。これは、windowpacket 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チャネルを開設する汎用的な役割を担っていました。このメソッドは以下の主要なステップを実行していました。

  1. 新しい clientChan インスタンスの作成。
  2. msgChannelOpen メッセージの構築と送信。このメッセージには、チャネルタイプ、クライアント側のチャネルID、初期ウィンドウサイズ、最大パケットサイズが含まれます。
  3. サーバーからの応答(channelOpenConfirmMsg または channelOpenFailureMsg)を待機。
  4. 応答に基づいて、clientChan のピアIDとウィンドウサイズを設定するか、エラーを返す。

コミットメッセージにあるように、この openChanNewSession からしか呼び出されていませんでした。また、Dial のような他の接続確立プロセスは、openChan を使用せず、独自のチャネル開設ロジックを持っていました。これは、openChan が汎用的なチャネル開設メソッドとして設計されていたにもかかわらず、実際には特定のユースケース(セッションチャネルの開設)に特化して使用されていたことを意味します。

この状況を改善するため、openChan を削除し、そのロジックを NewSession に直接移動することで、以下の利点が得られます。

  • コードの局所性向上: NewSession の動作を理解するために、別の openChan メソッドの定義を参照する必要がなくなります。チャネル開設のロジックが NewSession の内部に直接存在するため、NewSession のコードを読むだけで、セッションチャネルがどのように開設されるかを完全に把握できます。
  • 不要な抽象化の排除: openChan が単一の呼び出し元からしか使われていない場合、それは過剰な抽象化であると見なせます。その抽象化を排除することで、コードベースがシンプルになります。
  • 将来的な混乱の回避: もし openChan が残っていた場合、将来的に別の種類のチャネルを開設する際に、openChan を使うべきか、Dial のように独自のロジックを実装すべきかという設計上の疑問が生じる可能性があります。openChan を削除することで、この曖昧さが解消されます。

NewSession へのロジック統合

NewSession メソッドは、リモートホスト上で新しいインタラクティブなセッション(シェルやコマンド実行など)を開始するために使用されます。このコミットにより、NewSession は以下のステップを直接実行するようになりました。

  1. c.newChan(c.transport) を呼び出して、新しい clientChan インスタンスを作成します。これは、ローカル側のチャネルIDを割り当て、チャネルの状態を初期化します。
  2. msgChannelOpen メッセージを構築し、c.writePacket を使用してサーバーに送信します。このメッセージでは、ChanType"session" に明示的に設定されます。また、初期ウィンドウサイズ (1 << 14) と最大パケットサイズ (1 << 15) も設定されます。これらの値はRFC 4253 6.1で推奨されているデフォルト値です。
  3. ch.msg チャネルからサーバーからの応答を待ちます。
  4. 受信したメッセージのタイプに基づいて処理を分岐します。
    • 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 の中で直接実行されるようになりました。
      1. ch := c.newChan(c.transport): 新しい clientChan を作成します。
      2. if err := c.writePacket(marshal(msgChannelOpen, channelOpenMsg{...})); err != nil { ... }: msgChannelOpen メッセージを構築し、サーバーに送信します。ChanType"session" に固定され、PeersIdPeersWindowMaxPacketSize が設定されます。エラーが発生した場合は、c.chanlist.remove(ch.id) でチャネルをリストから削除し、エラーを返します。
      3. msg := <-ch.msg: サーバーからの応答を待ちます。
      4. switch msg := msg.(type) { ... }: 受信したメッセージの型に基づいて処理を分岐します。
        • channelOpenConfirmMsg の場合: ch.peersIdch.win を設定し、成功した Session オブジェクトを返します。
        • channelOpenFailureMsg の場合: c.chanlist.remove(ch.id) でチャネルを削除し、fmt.Errorf を使用して具体的なエラーメッセージを含むエラーを返します。
        • その他の場合: c.chanlist.remove(ch.id) でチャネルを削除し、予期しないメッセージタイプを含むエラーを返します。

この変更により、NewSession はセッションチャネルの開設に関するすべての詳細を自身で処理するようになり、openChan という中間的な抽象化が不要になりました。これにより、コードの依存関係が減り、NewSession の動作がより明確になりました。

関連リンク

参考にした情報源リンク

  • Go言語の公式ドキュメント
  • SSHプロトコルに関するRFCドキュメント
  • ソフトウェアリファクタリングに関する一般的な知識