[インデックス 10080] ファイルの概要
このコミットは、Go言語の実験的なSSHパッケージ(exp/ssh)において、インタラクティブなSSHセッションを扱うための主要な型をCmdからSessionへと変更するものです。これにより、SSHチャネルの抽象化が改善され、将来的にdirect-tcpipやx11といった他の種類のチャネルをサポートするための基盤が構築されます。
コミット
commit 5791233461d9eaef94f8a29cee7a1933a5c015d2
Author: Dave Cheney <dave@cheney.net>
Date: Mon Oct 24 19:13:55 2011 -0400
exp/ssh: introduce Session to replace Cmd for interactive commands
This CL replaces the Cmd type with a Session type representing
interactive channels. This lays the foundation for supporting
other kinds of channels like direct-tcpip or x11.
client.go:
* replace chanlist map with slice.
* generalize stdout and stderr into a single type.
* unexport ClientChan to clientChan.
doc.go:
* update ServerConfig/ServerConn documentation.
* update Client example for Session.
message.go:
* make channelExtendedData more like channelData.
session.go:
* added Session which replaces Cmd.
R=agl, rsc, n13m3y3r, gustavo
CC=golang-dev
https://golang.org/cl/5302054
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/5791233461d9eaef94f8a29cee7a1933a5c015d2
元コミット内容
このコミットは、Go言語のexp/sshパッケージにおいて、インタラクティブなコマンド実行やシェルセッションを扱うためのCmd型を廃止し、より汎用的なSession型を導入します。この変更の主な目的は、SSHプロトコルが提供する多様なチャネルタイプ(例: direct-tcpip、x11転送など)を将来的にサポートするための、より堅牢で拡張性の高い基盤を構築することです。
具体的な変更点としては、以下のファイルに影響があります。
client.go:- チャネルリストの管理方法が
mapからsliceに変更され、効率と予測可能性が向上します。 - 標準出力(stdout)と標準エラー出力(stderr)を扱うための型が単一の汎用的な型に統合されます。
ClientChan型がパッケージ外部から直接アクセスできないclientChan(小文字始まり)にアンエクスポートされます。これは、Session型を通じてチャネル操作を行うように設計が変更されたためです。
- チャネルリストの管理方法が
doc.go:ServerConfigとServerConnに関するドキュメントが更新されます。- クライアント側のSSHセッションの例が、新しい
Session型を使用するように修正されます。
messages.go:channelExtendedDataメッセージの構造がchannelDataメッセージの構造に近づけられ、より一貫性のあるデータ表現になります。具体的には、Data stringフィールドがPayload []byteに変更されます。
session.go:- 新たに
Session型が定義され、インタラクティブなSSHチャネルのすべての機能(環境変数の設定、擬似端末の要求、コマンド実行、シェル起動など)をカプセル化します。
- 新たに
変更の背景
SSHプロトコルは、単にリモートコマンドを実行するだけでなく、ポートフォワーディング(direct-tcpip、forwarded-tcpip)、X11転送、エージェント転送など、様々な種類の「チャネル」を多重化して利用できる強力な機能を持っています。
このコミット以前のexp/sshパッケージでは、インタラクティブなコマンド実行やシェルセッションに特化したCmd型が使用されていました。しかし、この設計では、SSHプロトコルが提供する他のチャネルタイプを統一的に扱うことが困難でした。
この変更の背景には、以下のような課題認識があったと考えられます。
- 拡張性の欠如:
Cmd型はインタラクティブセッションに特化しており、direct-tcpip(クライアントからリモートへのポートフォワーディング)やx11(X Window Systemの転送)のような、異なる性質を持つチャネルをサポートするための抽象化が不足していました。 - APIの一貫性: SSHチャネルは本質的に多重化されたストリームであり、それぞれが特定の目的を持っています。
Cmdという特定の用途に限定された型ではなく、より汎用的なSessionという概念を導入することで、SSHチャネル全体のAPI設計に一貫性を持たせることができます。 - コードの再利用性:
Session型を導入することで、チャネルの共通的なライフサイクル管理やデータフロー処理をSession内に集約し、異なるチャネルタイプ間でコードを再利用しやすくなります。 - RFC 4254への準拠と将来性: RFC 4254はSSH接続におけるチャネルの確立と管理について詳細に記述しています。
Session型への移行は、このRFCの精神により忠実に従い、将来的なプロトコル拡張や新機能の追加に対応しやすい設計を目指すものです。
このコミットは、GoのSSHパッケージがより成熟し、SSHプロトコルの全機能をより柔軟に、かつGoらしいイディオムで提供するための重要な一歩と言えます。
前提知識の解説
このコミットを理解するためには、以下の前提知識が役立ちます。
1. SSHプロトコルとチャネル
SSH(Secure Shell)は、ネットワークを介して安全にコンピュータを操作するためのプロトコルです。SSHは単一のTCP接続上で複数の論理的な「チャネル」を多重化して使用します。これにより、一つのSSH接続で同時に複数の異なるサービス(例: シェルセッション、ファイル転送、ポートフォワーディングなど)を提供できます。
- チャネル (Channel): SSH接続上で確立される論理的な通信路です。各チャネルは独立したデータストリームを持ち、特定の目的のために使用されます。
- セッションチャネル (Session Channel): 最も一般的なチャネルタイプで、リモートシェル、コマンド実行、サブシステム(例:
sftp)の起動、擬似端末(pty)の割り当て、環境変数の設定などに使用されます。このコミットでCmdからSessionに置き換えられる対象です。 - X11チャネル: X Window Systemのグラフィカルアプリケーションをリモートで実行するためのチャネルです。
- Direct-TCP/IPチャネル: クライアント側からリモートホストを介して別のTCPサービスに接続するためのポートフォワーディング(ローカルフォワード)に使用されます。
- Forwarded-TCP/IPチャネル: リモートホスト側からクライアントを介して別のTCPサービスに接続するためのポートフォワーディング(リモートフォワード)に使用されます。
- セッションチャネル (Session Channel): 最も一般的なチャネルタイプで、リモートシェル、コマンド実行、サブシステム(例:
2. Go言語のexpパッケージ
Go言語の標準ライブラリには、exp(experimental)というプレフィックスを持つパッケージが存在することがあります。これらは、まだ安定版ではないが、将来的に標準ライブラリに取り込まれる可能性のある実験的な機能やAPIを提供します。exp/sshもその一つであり、開発途上であることを示唆しています。expパッケージのAPIは、安定版になるまでに変更される可能性があります。
3. Go言語のioパッケージとインターフェース
Go言語のioパッケージは、I/O操作のための基本的なインターフェース(io.Reader, io.Writer, io.Closerなど)を定義しています。これらのインターフェースは、様々なI/Oソース(ファイル、ネットワーク接続、メモリバッファなど)に対して統一的な操作を提供するために広く利用されます。
io.Reader:Readメソッドを持つインターフェース。データを読み出すための抽象化。io.Writer:Writeメソッドを持つインターフェース。データを書き込むための抽象化。io.ReadCloser:io.Readerとio.Closerを組み合わせたインターフェース。io.WriteCloser:io.Writerとio.Closerを組み合わせたインターフェース。
このコミットでは、Session型がStdin io.WriteCloser, Stdout io.ReadCloser, Stderr io.Readerを持つことで、標準的なGoのI/Oインターフェースを通じてSSHセッションの入出力を扱うことができるようになります。
4. Go言語のsyncパッケージと並行処理
Go言語は並行処理を強力にサポートしており、syncパッケージはミューテックス(sync.Mutex)などの同期プリミティブを提供します。chanlistの変更でsync.Mutexが使用されているのは、複数のゴルーチンからチャネルリストへの同時アクセスを安全に制御するためです。
5. RFC 4253とRFC 4254
- RFC 4253 (The Secure Shell (SSH) Transport Layer Protocol): SSH接続の基盤となるトランスポート層プロトコルについて定義しています。鍵交換、暗号化、データ整合性などが含まれます。
- RFC 4254 (The Secure Shell (SSH) Connection Protocol): SSHトランスポート層上でどのようにチャネルが確立され、多重化されるかについて定義しています。セッションチャネル、X11転送、ポートフォワーディングなどの詳細が含まれます。このコミットの
Session型は、特にRFC 4254のセクション6「Interactive Session」に記述されている内容を実装しています。
これらの知識を持つことで、コミットの意図と技術的な詳細をより深く理解することができます。
技術的詳細
このコミットの技術的な核心は、SSHチャネルの抽象化をCmdからSessionへと進化させる点にあります。これは、単なる名前の変更ではなく、内部的なチャネル管理、I/O処理、およびAPI設計に大きな影響を与えています。
1. Session型の導入とCmdの廃止
session.goの新規追加: このファイルにSession型が定義されます。type Session struct { Stdin io.WriteCloser Stdout io.ReadCloser Stderr io.Reader *clientChan // the channel backing this session started bool // started is set to true once a Shell or Exec is invoked. }Sessionは、Stdin,Stdout,Stderrという標準的なI/Oインターフェースを持ち、Goの他のI/O操作とシームレスに連携できます。また、内部的にclientChanを埋め込むことで、基盤となるSSHチャネルの機能にアクセスします。startedフィールドは、ExecまたはShellが一度だけ呼び出されるべきであるというSSHセッションの特性を強制するために使用されます。Cmd型の削除:client.goからCmd型が完全に削除されます。これにより、インタラクティブセッションの責任がSession型に一元化されます。
2. clientChanへのリファクタリングとアンエクスポート
ClientChanからclientChanへ:client.goにあったClientChan型がclientChan(小文字始まり)にリネームされ、パッケージ外部からは直接アクセスできなくなります。これは、Session型がclientChanをラップし、より高レベルなAPIを提供するという設計意図を反映しています。- I/Oチャネルの汎用化: 以前の
ClientChanはstdinWriter,stdoutReader,stderrReaderという具体的なI/O型を持っていましたが、新しいclientChanはより汎用的なdata,dataExt,winというチャネルを持ちます。
これにより、type clientChan struct { 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 msg chan interface{} // incoming messages }clientChanはSSHチャネルの生データを扱い、その解釈とI/OインターフェースへのマッピングはSession型とその内部のchanWriter/chanReaderに委ねられます。
3. chanlistのmapからsliceへの変更
client.goのchanlist: 以前はmap[uint32]*ClientChanでチャネルを管理していましたが、slice([]*clientChan)に変更されます。
この変更は、チャネルIDの割り当てと管理をより効率的かつ予測可能にするためのものです。type chanlist struct { sync.Mutex chans []*clientChan }newChanメソッドは、スライス内の空きスロットを探すか、スライスの末尾に新しいチャネルを追加します。removeメソッドは、スライスからチャネルを削除する代わりに、該当するスロットをnilに設定します。これにより、スライスの再割り当てを避けることができます。
4. channelExtendedDataの変更
messages.go:channelExtendedDataメッセージのData stringフィールドがPayload []byteに変更されます。
これは、SSHプロトコルメッセージのペイロードが通常バイト列として扱われるため、より正確な表現です。type channelExtendedData struct { PeersId uint32 Datatype uint32 Payload []byte `ssh:"rest"` }ssh:"rest"タグは、残りのバイト列がこのフィールドに割り当てられることを示唆しています。また、client.goのmainLoop内で、channelExtendedDataのDatatypeが1(stderr)の場合のみペイロードを処理するように明示的なチェックが追加されています。これはRFC 4254 5.2節に準拠した動作です。
5. I/Oヘルパー型の導入
chanWriterとchanReader:client.goにstdinWriter,stdoutReader,stderrReaderに代わる汎用的なchanWriterとchanReaderが導入されます。
これらの型は、type chanWriter struct { win chan int // receives window adjustments id uint32 // this channel's id rwin int // current rwin size packetWriter // for sending channelDataMsg } type chanReader struct { data chan []byte // receives data from remote id uint32 packetWriter // for sending windowAdjustMsg buf []byte }SessionのStdin,Stdout,Stderrインターフェースの実装として機能し、基盤となるclientChanからのデータ送受信とウィンドウ調整メッセージの処理を担当します。これにより、I/OロジックがclientChanから分離され、よりモジュール化された設計になっています。
これらの技術的な変更は、SSHチャネルの管理とI/O処理をより柔軟かつ汎用的にすることで、将来的な拡張性を高めることを目的としています。
コアとなるコードの変更箇所
このコミットにおけるコアとなるコードの変更は、主に以下のファイルに集中しています。
-
src/pkg/exp/ssh/client.go:Cmd型および関連するstdinWriter,stdoutReader,stderrReader型の定義が削除されました。ClientChan型がclientChanにリネームされ、アンエクスポートされました。ClientConnのchanlistフィールドがmap[uint32]*ClientChanから[]*clientChanに変更されました。ClientConn.OpenChanメソッドがClientConn.openChanにリネームされ、*clientChanを返すようになりました。ClientConn.mainLoop内のチャネルメッセージ処理ロジックが、新しいclientChanの構造とSessionのI/O処理に合わせて変更されました。特に、channelCloseMsgとchannelEOFMsgの処理、およびchannelExtendedDataのDatatypeチェックが追加されました。chanlistのnewChan,getChan,removeメソッドの実装が、スライスベースの管理に合わせて変更されました。- 新しい
chanWriterとchanReader型が定義され、WriteおよびReadメソッドが実装されました。
-
src/pkg/exp/ssh/session.go: (新規ファイル)Session型が定義されました。この型はStdin,Stdout,StderrのI/Oインターフェースと、埋め込みの*clientChanを持ちます。Session型に、SSHセッション固有の操作(Setenv,RequestPty,Exec,Shell)をカプセル化するメソッドが追加されました。これらのメソッドは内部的にsendChanReqを呼び出し、SSHチャネルリクエストを送信します。ClientConnにNewSession()メソッドが追加され、新しいSessionインスタンスを生成し、対応するchanWriterとchanReaderを割り当てる役割を担います。
-
src/pkg/exp/ssh/doc.go:- パッケージのドキュメントとクライアントの使用例が、
Session型を使用するように更新されました。これにより、新しいAPIの利用方法が示されます。
- パッケージのドキュメントとクライアントの使用例が、
-
src/pkg/exp/ssh/messages.go:channelExtendedData構造体のData stringフィールドがPayload []byteに変更されました。
これらの変更は、SSHチャネルの抽象化を根本的に見直し、より柔軟で拡張性の高いSessionベースの設計へと移行したことを示しています。
コアとなるコードの解説
session.go の Session 型
// Session implements an interactive session described in
// "RFC 4254, section 6".
type Session struct {
// Writes to Stdin are made available to the remote command's standard input.
// Closing Stdin causes the command to observe an EOF on its standard input.
Stdin io.WriteCloser
// Reads from Stdout and Stderr consume from the remote command's standard
// output and error streams, respectively.
// There is a fixed amount of buffering that is shared for the two streams.
// Failing to read from either may eventually cause the command to block.
// Closing Stdout unblocks such writes and causes them to return errors.
Stdout io.ReadCloser
Stderr io.Reader
*clientChan // the channel backing this session
started bool // started is set to true once a Shell or Exec is invoked.
}
Session型は、SSHプロトコルにおけるインタラクティブセッション(RFC 4254, section 6)を表現します。
Stdin,Stdout,Stderr: Goの標準的なI/Oインターフェースを実装しており、リモートコマンドの標準入出力にアクセスするための手段を提供します。これにより、Goの他のI/Oユーティリティ(例:io.Copy,bufio.Reader)と組み合わせて使用できます。*clientChan:Sessionが内部的に使用するSSHチャネルの低レベルな表現です。Session型はclientChanのメソッドを直接呼び出すことができます(埋め込みフィールドのため)。started: セッションが既にExecまたはShellコマンドで開始されているかどうかを示すフラグです。SSHプロトコルでは、一つのセッションチャネルで一度だけコマンドを実行するかシェルを起動することが一般的です。
ClientConn.NewSession() メソッド
// NewSession returns a new interactive session on the remote host.
func (c *ClientConn) NewSession() (*Session, os.Error) {
ch, err := c.openChan("session") // "session"タイプのチャネルを開く
if err != nil {
return nil, err
}
return &Session{
Stdin: &chanWriter{ // StdinはchanWriterで実装
packetWriter: ch,
id: ch.id,
win: ch.win,
},
Stdout: &chanReader{ // StdoutはchanReaderで実装
packetWriter: ch,
id: ch.id,
data: ch.data,
},
Stderr: &chanReader{ // StderrもchanReaderで実装(dataExtチャネルを使用)
packetWriter: ch,
id: ch.id,
data: ch.dataExt,
},
clientChan: ch, // 基盤となるclientChanを埋め込む
}, nil
}
ClientConn(SSHクライアント接続を表す)のNewSessionメソッドは、新しいインタラクティブなSSHセッションを確立するためのエントリポイントです。
- まず、内部的に
c.openChan("session")を呼び出し、SSHサーバーに対して「セッション」タイプのチャネルを開くよう要求します。 - 成功した場合、新しく作成された
clientChan(ch)を基盤として、Session構造体を初期化して返します。 SessionのStdin,Stdout,Stderrフィールドは、それぞれchanWriterとchanReaderのインスタンスで初期化されます。これらのヘルパー型は、clientChanが提供する低レベルなチャネル(win,data,dataExt)を、Goの標準I/Oインターフェース(io.WriteCloser,io.ReadCloser,io.Reader)にマッピングする役割を担います。
Session.Exec() メソッド
// Exec runs cmd on the remote host. Typically, the remote
// server passes cmd to the shell for interpretation.
// A Session only accepts one call to Exec or Shell.
func (s *Session) Exec(cmd string) os.Error {
if s.started {
return os.NewError("session already started")
}
cmdLen := stringLength([]byte(cmd))
payload := make([]byte, cmdLen)
marshalString(payload, []byte(cmd))
s.started = true
return s.sendChanReq(channelRequestMsg{
PeersId: s.id,
Request: "exec",
WantReply: true,
RequestSpecificData: payload,
})
}
Execメソッドは、リモートホスト上で指定されたコマンドを実行します。
s.startedフラグをチェックし、セッションが既に開始されていないことを確認します。これにより、一つのセッションチャネルで複数のコマンドを実行しようとする誤用を防ぎます。- 実行するコマンド文字列をバイト列に変換し、SSHチャネルリクエストメッセージのペイロードとして準備します。
s.startedをtrueに設定します。s.sendChanReqを呼び出し、SSHサーバーに対して"exec"タイプのリクエストを送信します。このリクエストには、実行するコマンドのペイロードが含まれます。WantReply: trueは、サーバーからの応答(成功/失敗)を期待することを示します。
これらのコードは、SSHプロトコルのチャネル管理とセッションのライフサイクルをGoのイディオムに沿って抽象化し、ユーザーがSSHセッションを簡単に操作できるように設計されています。
関連リンク
- Go言語の
exp/sshパッケージのソースコード(当時のバージョンに近いもの):- https://cs.opensource.google/go/go/+/refs/tags/go1.0.3:src/pkg/exp/ssh/ (Go 1.0.3の時点でのexp/ssh)
- RFC 4254: The Secure Shell (SSH) Connection Protocol:
- Go言語の
ioパッケージのドキュメント:
参考にした情報源リンク
- Go言語の公式ドキュメント
- RFC 4254: The Secure Shell (SSH) Connection Protocol
- SSHプロトコルに関する一般的な技術解説記事