~/.ssh/configを使って *ssh.Client を作成する

GMOペパボ Advent Calendar 2018 の4日目の記事です。

運用しているサーバに何か問題が発生したら、SSH接続をして原因の特定をします。特定するためにいろいろ確認をします。

しかし、自分はチームメンバーの中では上記があまり速いほうではありません。勘所もまだまだ悪いです。

そこで、その差を埋めるべく、最低限の確認を一気にできるようにしようと考えてGoでツールを作ろうとしています(おそらくシェルスクリプトでもAnsibleでもいいのですが、なんとなく作った方が良さそうな気がしています。まだなんとなくで確証はありません)。

~/.ssh/config を読む

そこで、まずはSSHクライアントを書こうと思ったのですが、GoにはRubyNet::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_configssh_config をパースするライブラリで、ProxyCommand を解釈して実行するところまではサポートしません。

ところで、ProxyCommandに記載されているコマンドはローカルで実行されます。そして、大抵はProxyCommand に書かれた ssh -Wnc を使って確立した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_configssh_config のファイルパスがprivateになっていて変更できない
    • Pull Request案件

など。

動くようになったのでまずは作りたかったツール作成に移りたいと思っています。