自前で作ってるGolang製のsshクライアントにssh port forwardingを追加したくなったので、まずはサンプルコードを書いてみることにした。 StackOverflowにちょうどいいサンプルがあったので参考にさせてもらったところ、ターミナルセッションとは別にsshの接続をする必要がありそうだ。ポートフォワード自体はio.Copyで対応できるようだ。

以下、サンプル( こちら にもおいてある)。

ssh_term_portforward.go
package main import ( "fmt" "io" "net" "os" "os/signal" "syscall" "golang.org/x/crypto/ssh" "golang.org/x/crypto/ssh/terminal" ) var ( host = "targethost" port = "22" user = "user" pass = "password" localPort = "localhost:10022" remotePort = "localhost:22" ) func forward(localConn net.Conn, config *ssh.ClientConfig) { // Setup sshClientConn (type *ssh.ClientConn) sshClientConn, err := ssh.Dial("tcp", host+":"+port, config) if err != nil { fmt.Println("ssh.Dial failed: %s", err) } // Setup sshConn (type net.Conn) sshConn, err := sshClientConn.Dial("tcp", remotePort) // Copy localConn.Reader to sshConn.Writer go func() { _, err = io.Copy(sshConn, localConn) if err != nil { fmt.Println("io.Copy failed: %v", err) } }() // Copy sshConn.Reader to localConn.Writer go func() { _, err = io.Copy(localConn, sshConn) if err != nil { fmt.Println("io.Copy failed: %v", err) } }() } func main() { // Create sshClientConfig sshConfig := &ssh.ClientConfig{ User: user, Auth: []ssh.AuthMethod{ ssh.Password(pass), }, HostKeyCallback: ssh.InsecureIgnoreHostKey(), } // Setup localListener (type net.Listener) localListener, err := net.Listen("tcp", localPort) if err != nil { fmt.Println("net.Listen failed: %v", err) } else { go func() { for { // Setup localConn (type net.Conn) localConn, err := localListener.Accept() if err != nil { fmt.Println("listen.Accept failed: %v", err) } go forward(localConn, sshConfig) } }() } // SSH connect. client, err := ssh.Dial("tcp", host+":"+port, sshConfig) // Create Session session, err := client.NewSession() defer session.Close() // キー入力を接続先が認識できる形式に変換する(ここがキモ) fd := int(os.Stdin.Fd()) state, err := terminal.MakeRaw(fd) if err != nil { fmt.Println(err) } defer terminal.Restore(fd, state) // ターミナルサイズの取得 w, h, err := terminal.GetSize(fd) if err != nil { fmt.Println(err) } modes := ssh.TerminalModes{ ssh.ECHO: 1, ssh.TTY_OP_ISPEED: 14400, ssh.TTY_OP_OSPEED: 14400, } err = session.RequestPty("xterm", h, w, modes) if err != nil { fmt.Println(err) } session.Stdout = os.Stdout session.Stderr = os.Stderr session.Stdin = os.Stdin err = session.Shell() if err != nil { fmt.Println(err) } // ターミナルサイズの変更検知・処理 signal_chan := make(chan os.Signal, 1) signal.Notify(signal_chan, syscall.SIGWINCH) go func() { for { s := <-signal_chan switch s { case syscall.SIGWINCH: fd := int(os.Stdout.Fd()) w, h, _ = terminal.GetSize(fd) session.WindowChange(h, w) } } }() err = session.Wait() if err != nil { fmt.Println(err) } }

ローカルのポートが空いてない場合は、ポートフォワードは行わずにそのまま接続するようにしている。 そのへんは、用途に合わせて書き換えてやればいいだろう。


参考