murave

Go言語のお勉強でSSHクライアント

次の案件でGo言語を使いたいと思って勉強しているmuraveさん、サーバー間連携で使うかもしれないと”golang.org/x/crypto/ssh”を使用するサンプルを作ることにしました。

goでsshクライアントの実装メモ – Qiita

Golangでターミナル接続できるsshクライアントコマンドを作成する(制御キー対応) | 俺的備忘録 〜なんかいろいろ〜

上記の記事を参考にしてSSHクライアントを書いてみました。

コマンドラインの解析にはflagパッケージを使用し、公開鍵認証のみ対応です。

// sshクライアントサンプル(鍵ファイルでの接続のみ対応)
package main

import (
"flag"
"fmt"
"io/ioutil"
"log"
"os"
"os/user"
"path/filepath"
"strings"

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

func main() {
host, userName, absKeyPath := parseCommand()

auth := []ssh.AuthMethod{}

key, err := ioutil.ReadFile(absKeyPath)
fatalLogWhenErr(err, "private key")
signer, err := ssh.ParsePrivateKey(key)
fatalLogWhenErr(err, "signer")

auth = append(auth, ssh.PublicKeys(signer))

sshConfig := &ssh.ClientConfig{
User: userName,
Auth: auth,
HostKeyCallback: ssh.InsecureIgnoreHostKey(),
}
startSession(host, sshConfig)

}

// parseCommand コマンドラインをパースする
func parseCommand() (string, string, string) {
// ヘルプメッセージにホストの書き方を追加
flag.Usage = func() {
fmt.Fprintf(os.Stderr, "%s hostname:22\n", os.Args[0])
flag.PrintDefaults()
}
flagUsername := flag.String("user", "", "user name (default:current user)")
flagKey := flag.String("key", "~/.ssh/id_rsa", "private key")

flag.Parse()
host := flag.Arg(0)
if host == "" {
flag.Usage()
os.Exit(1)
}

fmt.Println(*flagUsername, flag.Arg(0))
usr, err := user.Current()
fatalLogWhenErr(err, "")

userName := usr.Username
if *flagUsername != "" {
userName = *flagUsername
}
keyPath := *flagKey
var absKeyPath string
if keyPath[0:1] == "~" {
absKeyPath = strings.Replace(keyPath, "~", usr.HomeDir, 1)
} else {
var err error
absKeyPath, err = filepath.Abs(keyPath)
if err != nil {
log.Fatal(err)
}
}

return host, userName, absKeyPath
}

// startSession sshセッションを開始
func startSession(host string, sshConfig *ssh.ClientConfig) {
client, err := ssh.Dial("tcp", host, sshConfig)
fatalLogWhenErr(err, "dial")

session, err := client.NewSession()
fatalLogWhenErr(err, "new session")
defer session.Close()

// キー入力を接続先が認識できる形式に変換
fd := int(os.Stdin.Fd())
state, err := terminal.MakeRaw(fd)
fatalLogWhenErr(err, "tarminal make raw")
defer terminal.Restore(fd, state)

w, h, err := terminal.GetSize(fd)
fatalLogWhenErr(err, "tarminal get size")
termmodes := ssh.TerminalModes{
ssh.ECHO: 1,
ssh.TTY_OP_ISPEED: 14400,
ssh.TTY_OP_OSPEED: 14400,
}
term := os.Getenv("TERM")
err = session.RequestPty(term, h, w, termmodes)
fatalLogWhenErr(err, "session request pty")

session.Stdout = os.Stdout
session.Stderr = os.Stderr
session.Stdin = os.Stdin

err = session.Shell()
fatalLogWhenErr(err, "session shell")

err = session.Wait()
fatalLogWhenErr(err, "session wait")
}

// fatalLogWhenErr errをチェックしてerrorの場合にはfatal logを出力
func fatalLogWhenErr(err error, msg string) {
if err != nil {
if len(msg) > 0 {
log.Fatalf("%s error: %v", msg, err)
} else {
log.Fatal(err)
}
}
}

以下のような感じで普通に使えました(./sshがビルドしたバイナリでhostname等は隠しています)

$ ./ssh
./ssh hostname:22
-key string
private key (default "~/.ssh/id_rsa")
-user string
user name (default:current user)

$ ./ssh -user root ***.***.***.***:22
Last login: Thu Jan 9 15:45:57 2020 from ***-**-**-***.***.***.**
[root ~]# pwd
/root
[root ~]# exit
logout