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

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

このコミットは、Go言語の実験的なSSHパッケージ(exp/ssh)において、インタラクティブなSSHセッションを扱うための主要な型をCmdからSessionへと変更するものです。これにより、SSHチャネルの抽象化が改善され、将来的にdirect-tcpipx11といった他の種類のチャネルをサポートするための基盤が構築されます。

コミット

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-tcpipx11転送など)を将来的にサポートするための、より堅牢で拡張性の高い基盤を構築することです。

具体的な変更点としては、以下のファイルに影響があります。

  • client.go:
    • チャネルリストの管理方法がmapからsliceに変更され、効率と予測可能性が向上します。
    • 標準出力(stdout)と標準エラー出力(stderr)を扱うための型が単一の汎用的な型に統合されます。
    • ClientChan型がパッケージ外部から直接アクセスできないclientChan(小文字始まり)にアンエクスポートされます。これは、Session型を通じてチャネル操作を行うように設計が変更されたためです。
  • doc.go:
    • ServerConfigServerConnに関するドキュメントが更新されます。
    • クライアント側のSSHセッションの例が、新しいSession型を使用するように修正されます。
  • messages.go:
    • channelExtendedDataメッセージの構造がchannelDataメッセージの構造に近づけられ、より一貫性のあるデータ表現になります。具体的には、Data stringフィールドがPayload []byteに変更されます。
  • session.go:
    • 新たにSession型が定義され、インタラクティブなSSHチャネルのすべての機能(環境変数の設定、擬似端末の要求、コマンド実行、シェル起動など)をカプセル化します。

変更の背景

SSHプロトコルは、単にリモートコマンドを実行するだけでなく、ポートフォワーディング(direct-tcpipforwarded-tcpip)、X11転送、エージェント転送など、様々な種類の「チャネル」を多重化して利用できる強力な機能を持っています。

このコミット以前のexp/sshパッケージでは、インタラクティブなコマンド実行やシェルセッションに特化したCmd型が使用されていました。しかし、この設計では、SSHプロトコルが提供する他のチャネルタイプを統一的に扱うことが困難でした。

この変更の背景には、以下のような課題認識があったと考えられます。

  1. 拡張性の欠如: Cmd型はインタラクティブセッションに特化しており、direct-tcpip(クライアントからリモートへのポートフォワーディング)やx11(X Window Systemの転送)のような、異なる性質を持つチャネルをサポートするための抽象化が不足していました。
  2. APIの一貫性: SSHチャネルは本質的に多重化されたストリームであり、それぞれが特定の目的を持っています。Cmdという特定の用途に限定された型ではなく、より汎用的なSessionという概念を導入することで、SSHチャネル全体のAPI設計に一貫性を持たせることができます。
  3. コードの再利用性: Session型を導入することで、チャネルの共通的なライフサイクル管理やデータフロー処理をSession内に集約し、異なるチャネルタイプ間でコードを再利用しやすくなります。
  4. 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サービスに接続するためのポートフォワーディング(リモートフォワード)に使用されます。

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.Readerio.Closerを組み合わせたインターフェース。
  • io.WriteCloser: io.Writerio.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チャネルの汎用化: 以前のClientChanstdinWriter, 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. chanlistmapからsliceへの変更

  • client.gochanlist: 以前はmap[uint32]*ClientChanでチャネルを管理していましたが、slice[]*clientChan)に変更されます。
    type chanlist struct {
        sync.Mutex
        chans []*clientChan
    }
    
    この変更は、チャネルIDの割り当てと管理をより効率的かつ予測可能にするためのものです。newChanメソッドは、スライス内の空きスロットを探すか、スライスの末尾に新しいチャネルを追加します。removeメソッドは、スライスからチャネルを削除する代わりに、該当するスロットをnilに設定します。これにより、スライスの再割り当てを避けることができます。

4. channelExtendedDataの変更

  • messages.go: channelExtendedDataメッセージのData stringフィールドがPayload []byteに変更されます。
    type channelExtendedData struct {
        PeersId  uint32
        Datatype uint32
        Payload  []byte `ssh:"rest"`
    }
    
    これは、SSHプロトコルメッセージのペイロードが通常バイト列として扱われるため、より正確な表現です。ssh:"rest"タグは、残りのバイト列がこのフィールドに割り当てられることを示唆しています。また、client.gomainLoop内で、channelExtendedDataDatatype1(stderr)の場合のみペイロードを処理するように明示的なチェックが追加されています。これはRFC 4254 5.2節に準拠した動作です。

5. I/Oヘルパー型の導入

  • chanWriterchanReader: client.gostdinWriter, stdoutReader, stderrReaderに代わる汎用的なchanWriterchanReaderが導入されます。
    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
    }
    
    これらの型は、SessionStdin, Stdout, Stderrインターフェースの実装として機能し、基盤となるclientChanからのデータ送受信とウィンドウ調整メッセージの処理を担当します。これにより、I/OロジックがclientChanから分離され、よりモジュール化された設計になっています。

これらの技術的な変更は、SSHチャネルの管理とI/O処理をより柔軟かつ汎用的にすることで、将来的な拡張性を高めることを目的としています。

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

このコミットにおけるコアとなるコードの変更は、主に以下のファイルに集中しています。

  1. src/pkg/exp/ssh/client.go:

    • Cmd型および関連するstdinWriter, stdoutReader, stderrReader型の定義が削除されました。
    • ClientChan型がclientChanにリネームされ、アンエクスポートされました。
    • ClientConnchanlistフィールドがmap[uint32]*ClientChanから[]*clientChanに変更されました。
    • ClientConn.OpenChanメソッドがClientConn.openChanにリネームされ、*clientChanを返すようになりました。
    • ClientConn.mainLoop内のチャネルメッセージ処理ロジックが、新しいclientChanの構造とSessionのI/O処理に合わせて変更されました。特に、channelCloseMsgchannelEOFMsgの処理、およびchannelExtendedDataDatatypeチェックが追加されました。
    • chanlistnewChan, getChan, removeメソッドの実装が、スライスベースの管理に合わせて変更されました。
    • 新しいchanWriterchanReader型が定義され、WriteおよびReadメソッドが実装されました。
  2. src/pkg/exp/ssh/session.go: (新規ファイル)

    • Session型が定義されました。この型はStdin, Stdout, StderrのI/Oインターフェースと、埋め込みの*clientChanを持ちます。
    • Session型に、SSHセッション固有の操作(Setenv, RequestPty, Exec, Shell)をカプセル化するメソッドが追加されました。これらのメソッドは内部的にsendChanReqを呼び出し、SSHチャネルリクエストを送信します。
    • ClientConnNewSession()メソッドが追加され、新しいSessionインスタンスを生成し、対応するchanWriterchanReaderを割り当てる役割を担います。
  3. src/pkg/exp/ssh/doc.go:

    • パッケージのドキュメントとクライアントの使用例が、Session型を使用するように更新されました。これにより、新しいAPIの利用方法が示されます。
  4. src/pkg/exp/ssh/messages.go:

    • channelExtendedData構造体のData stringフィールドがPayload []byteに変更されました。

これらの変更は、SSHチャネルの抽象化を根本的に見直し、より柔軟で拡張性の高いSessionベースの設計へと移行したことを示しています。

コアとなるコードの解説

session.goSession

// 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セッションを確立するためのエントリポイントです。

  1. まず、内部的にc.openChan("session")を呼び出し、SSHサーバーに対して「セッション」タイプのチャネルを開くよう要求します。
  2. 成功した場合、新しく作成されたclientChanch)を基盤として、Session構造体を初期化して返します。
  3. SessionStdin, Stdout, Stderrフィールドは、それぞれchanWriterchanReaderのインスタンスで初期化されます。これらのヘルパー型は、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メソッドは、リモートホスト上で指定されたコマンドを実行します。

  1. s.startedフラグをチェックし、セッションが既に開始されていないことを確認します。これにより、一つのセッションチャネルで複数のコマンドを実行しようとする誤用を防ぎます。
  2. 実行するコマンド文字列をバイト列に変換し、SSHチャネルリクエストメッセージのペイロードとして準備します。
  3. s.startedtrueに設定します。
  4. s.sendChanReqを呼び出し、SSHサーバーに対して"exec"タイプのリクエストを送信します。このリクエストには、実行するコマンドのペイロードが含まれます。WantReply: trueは、サーバーからの応答(成功/失敗)を期待することを示します。

これらのコードは、SSHプロトコルのチャネル管理とセッションのライフサイクルをGoのイディオムに沿って抽象化し、ユーザーがSSHセッションを簡単に操作できるように設計されています。

関連リンク

参考にした情報源リンク

  • Go言語の公式ドキュメント
  • RFC 4254: The Secure Shell (SSH) Connection Protocol
  • SSHプロトコルに関する一般的な技術解説記事