~/.ssh/configを使って *ssh.Client を作成する
GMOペパボ Advent Calendar 2018 の4日目の記事です。
運用しているサーバに何か問題が発生したら、SSH接続をして原因の特定をします。特定するためにいろいろ確認をします。
しかし、自分はチームメンバーの中では上記があまり速いほうではありません。勘所もまだまだ悪いです。
そこで、その差を埋めるべく、最低限の確認を一気にできるようにしようと考えてGoでツールを作ろうとしています(おそらくシェルスクリプトでもAnsibleでもいいのですが、なんとなく作った方が良さそうな気がしています。まだなんとなくで確証はありません)。
~/.ssh/config を読む
そこで、まずはSSHクライアントを書こうと思ったのですが、GoにはRubyの Net::SSH::Config
のような ~/.ssh/config を読むような機能は標準パッケージにはないようです。
探してみたところ、kevinburke/ssh_config に ~/.ssh/config を読む機能があったので、これを使って ~/.ssh/config の設定情報を使ってSSH接続をしてみます。
まず以下のような ~/.ssh/config があるとして
Host myhost HostName 203.0.113.1 User k1low Port 10022 IdentityFile /path/to/myhost_rsa
以下のように kevinburke/ssh_config で ~/.ssh/config を読んで、それを ssh.ClientConfig に設定することで、~/.ssh/configの情報でSSH接続ができます。
package main import ( "bytes" "io/ioutil" "log" "github.com/kevinburke/ssh_config" "golang.org/x/crypto/ssh" ) func main() { host := "myhost" user := ssh_config.Get(host, "User") addr := ssh_config.Get(host, "Hostname") + ":" + ssh_config.Get(host, "Port") auth := []ssh.AuthMethod{} key, _ := ioutil.ReadFile(ssh_config.Get(host, "IdentityFile")) signer, _ := ssh.ParsePrivateKey(key) auth = append(auth, ssh.PublicKeys(signer)) sshConfig := &ssh.ClientConfig{ User: user, Auth: auth, HostKeyCallback: ssh.InsecureIgnoreHostKey(), // FIXME } client, _ := ssh.Dial("tcp", addr, sshConfig) session, _ := client.NewSession() defer session.Close() var stdout = &bytes.Buffer{} session.Stdout = stdout err = session.Run("hostname") if err != nil { log.Fatalf("error: %v", err) } log.Printf("result: %s", stdout.String()) }
(エラー処理などいろいろ省略)
ProxyCommandに対応してみる
対象サーバの前段に踏み台サーバがある場合、ProxyCommand
を記述して利用します。
kevinburke/ssh_config
は ssh_config をパースするライブラリで、ProxyCommand
を解釈して実行するところまではサポートしません。
ところで、ProxyCommand
に記載されているコマンドはローカルで実行されます。そして、大抵はProxyCommand
に書かれた ssh -W
や nc
を使って確立したSSH通信を経由して(プロキシして)、目的のホストへSSH接続をすることになります。
ようは、通信をパイプでつなげられればいいはずなので net.Pipe()
を使います。
c, s := net.Pipe() cmd := exec.Command("sh", "-c", proxyCommand) cmd.Stdin = s cmd.Stdout = s cmd.Stderr = os.Stderr if err := cmd.Start(); err != nil { return nil, err } conn, incomingChannels, incomingRequests, err := ssh.NewClientConn(c, addr, sshConfig) if err != nil { return nil, err } client := ssh.NewClient(conn, incomingChannels, incomingRequests)
(%h
の変換処理などいろいろ省略)
これでProxyCommand
を利用したSSH接続ができます。
sshc
上記に書かれているような処理をまとめてsshcというパッケージを作りはじめました。
使い方は以下のような感じで sshc.NewClient("myhost")
で ~/.ssh/config を解釈してProxyCommandでのプロキシもした *ssh.Client を得ることができます。
package main import ( "bytes" "log" "github.com/k1LoW/sshc" ) func main() { client, _ := sshc.NewClient("myhost") session, _ := client.NewSession() defer session.Close() var stdout = &bytes.Buffer{} session.Stdout = stdout err = session.Run("hostname") if err != nil { log.Fatalf("error: %v", err) } log.Printf("result: %s", stdout.String()) }
とりあえず手元にある ~/.ssh/config に対応できればいいかな、というゆるい感じで作っています。
TODOとしては
- 公開鍵のパスフレーズに対応
- 現在書いているテストが実際にSSH接続が確立するかのテストで公開できないのでなんとかしたい
- 多段SSHのテストってどう書けばいいのですかね。。
exec.Command()
を使っていて結局ssh
コマンドを使っている- 若干あきらめています
kevinburke/ssh_config
の ssh_config のファイルパスがprivateになっていて変更できない- Pull Request案件
など。
動くようになったのでまずは作りたかったツール作成に移りたいと思っています。