[インデックス 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タスクで必要とされていました。
このコミットが作成された背景には以下の要因があります:
- サーバー側実装の存在: すでにSSHサーバー実装が存在していたが、クライアント側の実装が欠如していた
- 実用性の向上: GoでSSH接続を行うアプリケーションを開発する需要の高まり
- プロトコルの完全実装: SSH仕様の完全な実装を目指す開発方針
- Go標準ライブラリの拡充: Go言語エコシステムの拡充とコミュニティの成長
このコミットは、Dave Cheney氏によって実装され、rsc(Russ Cox)、agl(Adam Langley)、n13m3y3r(Nigel Tao)によってレビューされました。
前提知識の解説
SSHプロトコルの基本概念
SSH(Secure Shell)は、ネットワーク上でコンピュータ間の安全な通信を実現するための暗号化プロトコルです。以下の3つの主要なプロトコルから構成されます:
- トランスポート層プロトコル(RFC 4253): 暗号化、認証、整合性の確保
- ユーザー認証プロトコル(RFC 4252): ユーザーの認証方法の定義
- コネクション プロトコル(RFC 4254): 複数のチャネルの多重化
Go言語での実装における特徴
2011年当時のGo言語の特徴:
- goroutine: 軽量なスレッドモデルによる並行処理
- channel: goroutine間の通信手段
- interface: 型安全性を保ちながら柔軟な抽象化を実現
- error型: エラーハンドリングの標準的な方法(当時は
os.Error
)
暗号学的基盤
SSHクライアントの実装には以下の暗号学的要素が必要です:
- Diffie-Hellman鍵交換: 共有秘密の生成
- 公開鍵暗号: サーバー認証とデジタル署名
- 対称暗号: 大量データの効率的な暗号化
- メッセージ認証コード(MAC): データの整合性確保
技術的詳細
アーキテクチャ設計
このSSHクライアント実装は、以下のレイヤー構造で設計されています:
- トランスポート層: TCP接続の管理と暗号化
- プロトコル層: SSH固有のメッセージ処理
- チャネル層: 複数のSSHチャネルの多重化
- アプリケーション層: コマンド実行、シェル、ファイル転送
主要コンポーネント
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. ハンドシェイク処理
ハンドシェイク処理は以下の手順で実行されます:
- バージョン交換: クライアントとサーバーのSSHバージョン情報の交換
- アルゴリズム交渉: 暗号化、MAC、圧縮アルゴリズムの選択
- 鍵交換: Diffie-Hellman鍵交換による共有秘密の生成
- 鍵導出: 共有秘密から暗号化鍵の生成
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に基づいています。具体的には以下の処理を行います:
- 秘密鍵生成:
rand.Int()
を使用して暗号学的に安全な乱数生成 - 公開鍵計算:
big.Int.Exp()
を使用したモジュラー指数演算 - 共有秘密計算: サーバーの公開鍵を使用した共有秘密の生成
- ハッシュ計算: 複数のパラメータを組み合わせたハッシュ値の計算
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
// ... 他のメッセージタイプの処理
}
}
}
この設計により、複数のチャネルから同時にデータを受信できます。
関連リンク
- RFC 4253 - SSH Transport Layer Protocol
- RFC 4254 - SSH Connection Protocol
- RFC 4252 - SSH Authentication Protocol
- Go SSH Package Documentation
- RFC 3526 - More Modular Exponential (MODP) Diffie-Hellman groups