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というパッケージを作りはじめました。
github.com
使い方は以下のような感じで 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になっていて変更できない
など。
動くようになったのでまずは作りたかったツール作成に移りたいと思っています。