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

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

コミット

このコミット bbbd41f4fff790e9a340a4be77c3c05f37491273 は、Go言語の実験的なSSHパッケージ (exp/ssh) におけるクライアントチャネルのオープンロジックを簡素化することを目的としています。特に、チャネルのクローズ処理とブロッキング動作に関する未解決のTODOを解決するための一連の変更の第一弾として位置づけられています。

主な変更点は以下の2点です。

  1. peersId の割り当ての一元化: これまで複雑だった peersId の割り当て処理が、一箇所で行われるように簡素化されました。これにより、部分的に作成された clientChan の構造が導入されています。
  2. clientChan.stdin/out/err の早期作成: チャネルがオープンされる際に clientChan の標準入出力 (stdin, stdout, stderr) が作成されるようになりました。これにより、tcpchanSession といったチャネルのコンシューマが、関連するリーダー/ライターに自身を接続するだけで済むようになり、コードが簡素化されます。

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.Readerio.Writer インターフェース: Go言語における io.Readerio.Writer は、それぞれデータの読み込みと書き込みのための基本的なインターフェースです。

    • io.ReaderRead(p []byte) (n int, err error) メソッドを持ち、データを読み込みます。
    • io.WriterWrite(p []byte) (n int, err error) メソッドを持ち、データを書き込みます。 これらのインターフェースは、様々なデータソースやシンク(ファイル、ネットワーク接続、メモリバッファなど)に対して統一的なI/O操作を提供するために広く利用されます。
  • Go言語のチャネル (chan): Go言語のチャネルは、ゴルーチン間で値を送受信するための通信メカニズムです。チャネルは、Goにおける並行処理の基本的な構成要素であり、安全なデータ共有と同期を可能にします。このコミットでは、データパケットやウィンドウ調整メッセージの送受信にチャネルが使用されています。

  • packetWriter: packetWriter は、SSHプロトコルにおける低レベルのパケット送信機能を提供するインターフェースまたは構造体であると推測されます。SSHのデータは、特定のフォーマットに従ってパケットにカプセル化されて送受信されます。packetWriter は、これらのパケットをネットワーク接続に書き込む役割を担います。

  • clientChan 構造体: clientChan は、SSHクライアント側で個々の論理チャネルの状態を管理するための主要な構造体です。この構造体は、チャネルのローカルID、リモートピアのID (peersId)、入出力バッファ、およびチャネル関連のメッセージを受信するチャネルなどを保持します。

  • chanWriterchanReader 構造体: これらは、clientChan の上で動作する io.Writer および io.Reader の実装です。

    • chanWriter は、リモートプロセスへの標準入力 (stdin) のように、クライアントからサーバーへデータを書き込む役割を担います。
    • chanReader は、リモートプロセスからの標準出力 (stdout) や標準エラー出力 (stderr) のように、サーバーからクライアントへデータを読み込む役割を担います。 これらは、SSHチャネルのフロー制御(ウィンドウサイズ調整)を考慮しながら、データの送受信を行います。

技術的詳細

このコミットは、Goの実験的なSSHパッケージにおけるクライアントチャネルのオープンと管理の方法を根本的に変更しています。主要な技術的変更点は以下の通りです。

  1. clientChan 構造体の再設計と newClientChan の変更:

    • 以前の clientChan は、data (標準出力)、dataExt (標準エラー出力)、win (ウィンドウ調整) の各チャネルを直接メンバーとして持っていました。これらは []byteint を送受信するGoチャネルでした。
    • 新しい clientChan では、これらの直接的なチャネルが削除され、代わりに *chanWriter 型の stdin*chanReader 型の stdout*chanReader 型の stderr というフィールドが導入されました。これにより、clientChan が直接I/Oストリームの概念を持つようになりました。
    • newClientChan 関数は、clientChan を初期化する際に、これらの stdin, stdout, stderr フィールドも初期化し、それぞれに対応する chanWriter および chanReader インスタンスを割り当てます。この時点では peersId はまだ不明なため、clientChan は「部分的に作成された」状態となります。
  2. peersId 割り当ての一元化と waitForChannelOpenResponse の導入:

    • 以前は、peersId の割り当てロジックが ClientConn.NewSession()ClientConn.dial() のようなチャネルオープンを行う複数の場所で重複して存在していました。
    • このコミットでは、clientChanwaitForChannelOpenResponse() という新しいメソッドが追加されました。このメソッドは、リモートピアからの channelOpenConfirmMsg または channelOpenFailureMsg を待ち受けます。
    • channelOpenConfirmMsg を受信した場合、peersIdmsg.MyId から取得して clientChan.peersId に設定し、初期ウィンドウサイズ (msg.MyWindow) を clientChan.stdin.win チャネルに送信します。これにより、peersId の割り当てと初期ウィンドウサイズの処理が一箇所に集約されました。
    • ClientConn.NewSession()ClientConn.dial() は、チャネルオープン要求を送信した後、この waitForChannelOpenResponse() を呼び出すように変更されました。これにより、peersId の「複雑なハンドリング」が解消され、コードの重複が排除されました。
  3. chanWriterchanReader の簡素化と clientChan への依存:

    • 以前の chanWriterchanReader は、それぞれ peersIdpacketWriter を直接メンバーとして持っていました。
    • 新しい実装では、これらのフィールドが削除され、代わりに *clientChan 型の clientChan フィールドが追加されました。これにより、chanWriterchanReader は、自身が属する clientChan インスタンスを通じて peersIdpacketWriter にアクセスするようになりました。
    • この変更により、chanWriterchanReader のインスタンス化時に peersIdpacketWriter を個別に渡す必要がなくなり、Sessiontcpchan のようなコンシューマ側でのコードが大幅に簡素化されました。例えば、io.Copy を使用してデータを転送する際に、s.clientChan.stdin のように直接 clientChan のI/Oストリームを利用できるようになりました。
  4. mainLoop におけるデータ転送ロジックの変更:

    • ClientConn.mainLoop() は、受信したSSHパケットを処理する主要なゴルーチンです。
    • 以前は、msgChannelDatamsgChannelExtendedData を受信した際に、c.getChan(peersId).data <- packet[:length] のように直接 clientChandatadataExt チャネルにデータを送信していました。
    • 変更後、c.getChan(peersId).stdout.data <- packet[:length]c.getChan(peersId).stderr.data <- packet[:length] のように、clientChanstdout または stderr フィールド内の data チャネルにデータを送信するように変更されました。これは、clientChan がI/Oストリームをカプセル化する新しい設計を反映しています。
    • 同様に、channelCloseMsgwindowAdjustMsg の処理も、clientChanstdin, stdout, stderr フィールドを通じて行われるように変更されました。

これらの変更により、SSHチャネルのオープン、データ転送、およびクローズに関するロジックがよりモジュール化され、clientChan がチャネルのライフサイクルとI/Oストリームをより一貫して管理する中心的なエンティティとなりました。これにより、コードの複雑性が軽減され、将来的な機能追加やバグ修正が容易になることが期待されます。

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

このコミットでは、主に以下の3つのファイルが変更されています。

  1. 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 の構造に合わせて変更されました。
  2. src/pkg/exp/ssh/session.go:

    • Session 構造体の stdin(), stdout(), stderr() メソッド内で、chanWriterchanReader のインスタンスを直接作成していた部分が削除されました。
    • 代わりに、s.clientChan.stdin, s.clientChan.stdout, s.clientChan.stderr のように、Session が持つ clientChan のI/Oストリームを直接利用するように変更されました。これにより、Session のコードが大幅に簡素化されています。
    • ClientConn.NewSession() 関数内で、チャネルオープン後の応答を待つロジックが、新しく導入された ch.waitForChannelOpenResponse() メソッドを呼び出すように変更されました。
  3. src/pkg/exp/ssh/tcpip.go:

    • ClientConn.dial() 関数内で、channelOpenDirectMsg 構造体の定義が関数スコープからファイルスコープに移動されました。
    • ClientConn.dial() 関数内で、チャネルオープン後の応答を待つロジックが、ClientConn.NewSession() と同様に ch.waitForChannelOpenResponse() メソッドを呼び出すように変更されました。
    • tcpchan 構造体の初期化時に、ReaderWriter フィールドに ch.stdoutch.stdin を直接割り当てるように変更されました。以前は、chanReaderchanWriter の新しいインスタンスを作成していました。

全体として、この変更は clientChan を中心としたチャネル管理の再構築であり、Sessiontcpchan のような高レベルのコンシューマが、より抽象化された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 フィールドも初期化するように変更されました。注目すべきは、chanWriterchanReader が、自身が属する clientChan インスタンスへのポインタ (clientChan: c) を持つようになった点です。これにより、これらのI/Oヘルパーが、親チャネルの peersIdpacketWriter にアクセスできるようになります。

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 の新しい構造に合わせて更新されました。データは直接 clientChandatadataExt チャネルに送られるのではなく、stdout.datastderr.data のように、対応する chanReaderdata チャネルに送られるようになりました。同様に、チャネルのクローズやウィンドウ調整も、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 内で chanWriterchanReader のインスタンスを個別に作成し、peersIdpacketWriter を渡していました。この変更により、Session は自身が持つ clientChanstdin, 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 の初期化時に、ReaderWriter フィールドに clientChanstdoutstdin を直接割り当てることで、コードがより簡潔になりました。これは、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.Readerio.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ブログのパイプラインに関する記事)