SSH with Go

リモートホストへの操作などをしたくなるとsshを使いたくなる。 Goにはgolang.org/x/crypto/ssh というパッケージがあり比較的簡単に利用できる。

試してみる

使う準備 ClientConfig

最初に ssh.ClientConfig を準備する。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
buf, _ := ioutil.ReadFile("path/to/id_rsa")
var key ssh.Signer
if pass := os.Getenv(envKey); pass != "" {
  key, _ = ssh.ParsePrivateKeyWithPassphrase(buf, []byte(pass))
} else {
  key, _ = ssh.ParsePrivateKey(buf)
}

config := &ssh.ClientConfig{
  User: "user",
  Auth: []ssh.AuthMethod{
    ssh.PublicKeys(key),
  },
  HostKeyCallback: ssh.InsecureIgnoreHostKey(),
}

使う準備 Connection,Session

ssh はコネクションの中で複数のセッションを確立できる。

1
2
3
4
conn, _ := ssh.Dial("tcp", rr.Hostport, config)
defer conn.Close()
session, _ := conn.NewSession()
defer session.Close()

session では Start(cmd), Run(cmd), Shell() のどれか1つを1回呼べる。 Start(cmd) の場合は Wait() で終了を待つこともできる。

Run(cmd)を使う

Run(cmd) は同期的に実行される。非同期実行したい場合は Start(cmd), Wait() を使う。

1
2
3
4
stdout, _ := session.StdoutPipe()
stdin, _ := session.StdinPipe()
stderr, _ := session.StderrPipe()
session.Run("ls")

Shell()を使う

session の入出力を繋げて Shell() を呼ぶと使える。 RequestPty() を使うとターミナルを設定できる。 ターミナルの設定に使うTerminalModesで定義するオプションの意味はRFC4254に定義されている。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
session.Stdout = os.Stdout
session.Stderr = os.Stderr
session.Stdin = os.Stdin

modes := ssh.TerminalModes{
  ssh.ECHO:          0,
  ssh.TTY_OP_ISPEED: 14400,
  ssh.TTY_OP_OSPEED: 14400,
}
term := os.Getenv("TERM")
_ = session.RequestPty(term, 25, 80, modes)

session.Shell()
_ = session.Wait()

サンプルコード(cat remote log)

サンプルとして rcat なるツールを書いてみるとしたのようになる。 環境変数 SSH_PASS にssh鍵のパスワードを与えて使う形にした。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
package main

import (
  "io"
  "os"

  "github.com/masu-mi/rcat/remotelog"
)

func main() {
  f := remotelog.RemoteReaderFactory{
    Hostport: "host:port",
    User:     "user",
    Signer: remotelog.SimpleSigner(
      "path/to/.ssh/id_rsa",
      "SSH_PASS",
    ),
  }
  io.Copy(os.Stdout, f.LogReader("/var/log/message"))
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
package remotelog

import (
  "bufio"
  "fmt"
  "io/ioutil"
  "os"
  "strings"
  "sync"

  "golang.org/x/crypto/ssh"
)

type RemoteReaderFactory struct {
  Hostport string
  User     string
  Signer   ssh.Signer
}

func (rr RemoteReaderFactory) LogReader(paths ...string) (bio *bufio.Reader) {
  wg := &sync.WaitGroup{}
  wg.Add(1)
  go func() {
    config := &ssh.ClientConfig{
      User: rr.User,
      Auth: []ssh.AuthMethod{
        ssh.PublicKeys(rr.Signer),
      },
      HostKeyCallback: ssh.InsecureIgnoreHostKey(),
    }
    conn, err := ssh.Dial("tcp", rr.Hostport, config)
    if err != nil {
      panic(err)
    }
    defer conn.Close()
    session, err := conn.NewSession()
    if err != nil {
      panic(err)
    }
    defer session.Close()
    output, err := session.StdoutPipe()
    if err != nil {
      panic(err)
    }
    bio = bufio.NewReader(output)
    wg.Done()

    cmd := fmt.Sprintf("cat %s", strings.Join(paths, " "))
    session.Run(cmd)
  }()
  wg.Wait()
  return bio
}

func SimpleSigner(idRsaPath, envKey string) ssh.Signer {
  buf, err := ioutil.ReadFile(idRsaPath)
  if err != nil {
    panic(err)
  }
  var key ssh.Signer
  if pass := os.Getenv(envKey); pass != "" {
    key, err = ssh.ParsePrivateKeyWithPassphrase(buf, []byte(pass))
  } else {
    key, err = ssh.ParsePrivateKey(buf)
  }
  if err != nil {
    panic(err)
  }
  return key
}

簡単にSSHを利用できた。goroutineと合わせて複数ホストと並列で通信させて遊ぶとかできる。

comments powered by Disqus