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

[インデックス 10068] GoのSSHクライアント実装の追加

コミット

コミットハッシュ: 792a55f5db30c7280b2910a9621ea78ec6bd2c1c
作成者: Dave Cheney dave@cheney.net
日付: 2011年10月20日 15:44:45 UTC-4
タイトル: exp/ssh: add experimental ssh client

このコミットは、Go言語のexperimentalパッケージに初めてSSHクライアント機能を追加する歴史的に重要なコミットです。これまでサーバー側のみの実装であったSSHパッケージに、クライアント機能を実装することで、GoでSSH接続を行うことが可能になりました。

GitHub上でのコミットページへのリンク

https://github.com/golang/go/commit/792a55f5db30c7280b2910a9621ea78ec6bd2c1c

元コミット内容

exp/ssh: add experimental ssh client

Requires CL 5285044

client.go:
* add Dial, ClientConn, ClientChan, ClientConfig and Cmd.

doc.go:
* add Client documentation.

server.go:
* adjust for readVersion change.

transport.go:
* return an os.Error not a bool from readVersion.

R=rsc, agl, n13m3y3r
CC=golang-dev
https://golang.org/cl/5162047

このコミットは以下の主要な変更を含んでいます:

  • 新規ファイル: client.go(628行の新規実装)
  • 変更ファイル: doc.go(24行の変更)、server.go(6行の変更)、transport.go(17行の変更)、transport_test.go(10行の変更)
  • 全体: 6ファイルで668行の追加、18行の削除

変更の背景

2011年当時、Go言語はまだ初期段階にあり、標準ライブラリやexperimentalパッケージの基本的な機能が次々と実装されていました。SSHプロトコルは、リモートサーバーへの安全な接続を実現するための重要な技術として、多くのシステム管理やDevOpsタスクで必要とされていました。

このコミットが作成された背景には以下の要因があります:

  1. サーバー側実装の存在: すでにSSHサーバー実装が存在していたが、クライアント側の実装が欠如していた
  2. 実用性の向上: GoでSSH接続を行うアプリケーションを開発する需要の高まり
  3. プロトコルの完全実装: SSH仕様の完全な実装を目指す開発方針
  4. Go標準ライブラリの拡充: Go言語エコシステムの拡充とコミュニティの成長

このコミットは、Dave Cheney氏によって実装され、rsc(Russ Cox)、agl(Adam Langley)、n13m3y3r(Nigel Tao)によってレビューされました。

前提知識の解説

SSHプロトコルの基本概念

SSH(Secure Shell)は、ネットワーク上でコンピュータ間の安全な通信を実現するための暗号化プロトコルです。以下の3つの主要なプロトコルから構成されます:

  1. トランスポート層プロトコル(RFC 4253): 暗号化、認証、整合性の確保
  2. ユーザー認証プロトコル(RFC 4252): ユーザーの認証方法の定義
  3. コネクション プロトコル(RFC 4254): 複数のチャネルの多重化

Go言語での実装における特徴

2011年当時のGo言語の特徴:

  • goroutine: 軽量なスレッドモデルによる並行処理
  • channel: goroutine間の通信手段
  • interface: 型安全性を保ちながら柔軟な抽象化を実現
  • error型: エラーハンドリングの標準的な方法(当時はos.Error

暗号学的基盤

SSHクライアントの実装には以下の暗号学的要素が必要です:

  1. Diffie-Hellman鍵交換: 共有秘密の生成
  2. 公開鍵暗号: サーバー認証とデジタル署名
  3. 対称暗号: 大量データの効率的な暗号化
  4. メッセージ認証コード(MAC): データの整合性確保

技術的詳細

アーキテクチャ設計

このSSHクライアント実装は、以下のレイヤー構造で設計されています:

  1. トランスポート層: TCP接続の管理と暗号化
  2. プロトコル層: SSH固有のメッセージ処理
  3. チャネル層: 複数のSSHチャネルの多重化
  4. アプリケーション層: コマンド実行、シェル、ファイル転送

主要コンポーネント

1. ClientConn構造体

type ClientConn struct {
    *transport
    config *ClientConfig
    chanlist
}
  • transport: 低レベルのSSH通信を管理
  • config: クライアント設定(認証情報など)
  • chanlist: アクティブなチャネルの管理

2. ClientConfig構造体

type ClientConfig struct {
    Rand io.Reader
    User string
    Password string
}
  • Rand: 暗号学的乱数生成器
  • User: 認証に使用するユーザー名
  • Password: パスワード認証用

3. ClientChan構造体

type ClientChan struct {
    packetWriter
    *stdinWriter
    *stdoutReader
    *stderrReader
    id, peersId uint32
    msg chan interface{}
}
  • packetWriter: パケット送信インターフェース
  • stdin/stdout/stderr: 標準入出力の管理
  • id, peersId: チャネル識別子
  • msg: メッセージ受信用チャネル

プロトコル実装の詳細

1. ハンドシェイク処理

ハンドシェイク処理は以下の手順で実行されます:

  1. バージョン交換: クライアントとサーバーのSSHバージョン情報の交換
  2. アルゴリズム交渉: 暗号化、MAC、圧縮アルゴリズムの選択
  3. 鍵交換: Diffie-Hellman鍵交換による共有秘密の生成
  4. 鍵導出: 共有秘密から暗号化鍵の生成

2. 認証処理

このバージョンでは、以下の認証方法をサポートします:

  • none認証: 認証なし(テスト用)
  • password認証: パスワードによる認証

3. チャネル管理

SSH接続上で複数のチャネルを多重化して管理します:

  • チャネル開設: 新しいチャネルの作成
  • データ転送: stdin/stdout/stderrのデータ転送
  • ウィンドウ制御: フロー制御メカニズム
  • チャネル終了: チャネルの適切な終了処理

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

1. client.go:79-98 - Client関数の実装

func Client(c net.Conn, config *ClientConfig) (*ClientConn, os.Error) {
    conn := &ClientConn{
        transport: newTransport(c, config.rand()),
        config:    config,
        chanlist: chanlist{
            Mutex: new(sync.Mutex),
            chans: make(map[uint32]*ClientChan),
        },
    }
    if err := conn.handshake(); err != nil {
        conn.Close()
        return nil, err
    }
    if err := conn.authenticate(); err != nil {
        conn.Close()
        return nil, err
    }
    go conn.mainLoop()
    return conn, nil
}

2. client.go:102-187 - handshake メソッドの実装

func (c *ClientConn) handshake() os.Error {
    // バージョン文字列の送信
    if _, err := c.Write(clientVersion); err != nil {
        return err
    }
    
    // サーバーバージョンの読み取り
    version, err := readVersion(c)
    if err != nil {
        return err
    }
    
    // 鍵交換メッセージの送信
    clientKexInit := kexInitMsg{
        KexAlgos:                supportedKexAlgos,
        ServerHostKeyAlgos:      supportedHostKeyAlgos,
        CiphersClientServer:     supportedCiphers,
        CiphersServerClient:     supportedCiphers,
        MACsClientServer:        supportedMACs,
        MACsServerClient:        supportedMACs,
        CompressionClientServer: supportedCompressions,
        CompressionServerClient: supportedCompressions,
    }
    
    // Diffie-Hellman鍵交換の実行
    switch kexAlgo {
    case kexAlgoDH14SHA1:
        hashFunc = crypto.SHA1
        dhGroup14Once.Do(initDHGroup14)
        H, K, err = c.kexDH(dhGroup14, hashFunc, &magics, hostKeyAlgo)
    }
    
    // 鍵の設定
    if err = c.transport.writer.setupKeys(clientKeys, K, H, H, hashFunc); err != nil {
        return err
    }
    return c.transport.reader.setupKeys(serverKeys, K, H, H, hashFunc)
}

3. client.go:241-284 - kexDH メソッドの実装

func (c *ClientConn) kexDH(group *dhGroup, hashFunc crypto.Hash, magics *handshakeMagics, hostKeyAlgo string) ([]byte, []byte, os.Error) {
    // 秘密鍵xの生成
    x, err := rand.Int(c.config.rand(), group.p)
    if err != nil {
        return nil, nil, err
    }
    
    // 公開鍵X = g^x mod p の計算
    X := new(big.Int).Exp(group.g, x, group.p)
    
    // サーバーからの応答Y の検証
    if kexDHReply.Y.Sign() == 0 || kexDHReply.Y.Cmp(group.p) >= 0 {
        return nil, nil, os.NewError("server DH parameter out of bounds")
    }
    
    // 共有秘密K = Y^x mod p の計算
    kInt := new(big.Int).Exp(kexDHReply.Y, x, group.p)
    
    // ハッシュ値H の計算
    h := hashFunc.New()
    writeString(h, magics.clientVersion)
    writeString(h, magics.serverVersion)
    writeString(h, magics.clientKexInit)
    writeString(h, magics.serverKexInit)
    writeString(h, kexDHReply.HostKey)
    writeInt(h, X)
    writeInt(h, kexDHReply.Y)
    K := make([]byte, intLength(kInt))
    marshalInt(K, kInt)
    h.Write(K)
    
    H := h.Sum()
    return H, K, nil
}

4. client.go:359-367 - Dial 関数の実装

func Dial(network, addr string, config *ClientConfig) (*ClientConn, os.Error) {
    conn, err := net.Dial(network, addr)
    if err != nil {
        return nil, err
    }
    return Client(conn, config)
}

5. transport.go:745-776 - readVersion 関数の変更

func readVersion(r io.Reader) ([]byte, os.Error) {
    versionString := make([]byte, 0, 64)
    var ok, seenCR bool
    var buf [1]byte
    // バイト単位でのバージョン文字列読み取り
    for len(versionString) < maxVersionStringBytes {
        _, err := io.ReadFull(r, buf[:])
        if err != nil {
            return nil, err
        }
        // CRLF の検出と処理
        if buf[0] == '\r' {
            seenCR = true
        } else if buf[0] == '\n' && seenCR {
            ok = true
            break forEachByte
        }
        versionString = append(versionString, buf[0])
    }
    
    if !ok {
        return nil, os.NewError("failed to read version string")
    }
    
    // CR を削除してバージョン文字列を返す
    return versionString[:len(versionString)-1], nil
}

コアとなるコードの解説

1. Diffie-Hellman鍵交換の実装

このコミットで実装されたDiffie-Hellman鍵交換は、RFC 4253 Section 8に基づいています。具体的には以下の処理を行います:

  1. 秘密鍵生成: rand.Int()を使用して暗号学的に安全な乱数生成
  2. 公開鍵計算: big.Int.Exp()を使用したモジュラー指数演算
  3. 共有秘密計算: サーバーの公開鍵を使用した共有秘密の生成
  4. ハッシュ計算: 複数のパラメータを組み合わせたハッシュ値の計算

2. チャネル多重化の実装

SSHプロトコルでは、単一のTCP接続上で複数のチャネルを多重化します:

type chanlist struct {
    *sync.Mutex
    chans map[uint32]*ClientChan
}
  • Thread-safe: sync.Mutexによる排他制御
  • ID管理: uint32型のIDによるチャネル識別
  • 動的追加/削除: チャネルの動的な作成と削除

3. エラーハンドリングの改善

readVersion関数の変更では、従来のbool戻り値からos.Error戻り値への変更が行われました:

変更前:

func readVersion(r io.Reader) (versionString []byte, ok bool)

変更後:

func readVersion(r io.Reader) ([]byte, os.Error)

この変更により、エラーの詳細情報を呼び出し元に提供できるようになりました。

4. 標準入出力の抽象化

SSH接続における標準入出力の処理を抽象化しています:

type Cmd struct {
    Stdin io.WriteCloser
    Stdout io.ReadCloser
    Stderr io.Reader
}

これにより、ローカルのコマンド実行と同様のインターフェースでリモートコマンドを実行できます。

5. 非同期処理の実装

go conn.mainLoop()により、メッセージ受信処理を非同期で実行します:

func (c *ClientConn) mainLoop() {
    for {
        packet, err := c.readPacket()
        if err != nil {
            c.Close()
            return
        }
        switch msg := decode(packet).(type) {
        case *channelOpenMsg:
            c.getChan(msg.PeersId).msg <- msg
        // ... 他のメッセージタイプの処理
        }
    }
}

この設計により、複数のチャネルから同時にデータを受信できます。

関連リンク

参考にした情報源リンク