Fire Engine

消防士→ITエンジニア→研究者

Golangで軽量なSSHサーバを実装する

今回は、Golanggolang.org/x/crypto/sshパッケージを使って、SSHサーバを構築してみました。
かなりミニマムな実装ですが、リモートからSSH接続して、対話的にコマンドが実行できるところまで実装しました。

コード

github.com

package main

import (
    "golang.org/x/crypto/ssh"
    "log"
    "net"
    "io/ioutil"
    "fmt"
    "os/exec"
    "github.com/kr/pty"
    "sync"
    "io"
)

func main() {
    serverConfig := &ssh.ServerConfig{
        NoClientAuth: true,
    }

    privateKeyBytes, err := ioutil.ReadFile("id_rsa")
    if err != nil {
        log.Fatal("Failed to load private key (./id_rsa)")
    }

    privateKey, err := ssh.ParsePrivateKey(privateKeyBytes)
    if err != nil {
        log.Fatal("Failed to parse private key")
    }

    serverConfig.AddHostKey(privateKey)

    listener, err := net.Listen("tcp", "0.0.0.0:2222")
    if err != nil {
        log.Fatalf("Failed to listen on 2222 (%s)", err)
    }
    log.Print("Listening on 2222...")

    for {
        tcpConn, err := listener.Accept()
        if err != nil {
            log.Fatalf("Failed to accept on 2222 (%s)", err)
        }

        sshConn, chans, reqs, err := ssh.NewServerConn(tcpConn, serverConfig)
        if err != nil {
            log.Fatalf("Failed to handshake (%s)", err)
        }
        log.Printf("New SSH connection from %s (%s)", sshConn.RemoteAddr(), sshConn.ClientVersion())

        go ssh.DiscardRequests(reqs)
        go handleChannels(chans)
    }
}

func handleChannels(chans <-chan ssh.NewChannel) {
    for newChannel := range chans {
        go handleChannel(newChannel)
    }
}

func handleChannel(newChannel ssh.NewChannel) {
    if t := newChannel.ChannelType(); t != "session" {
        newChannel.Reject(ssh.UnknownChannelType, fmt.Sprintf("Unknown channel type: %s", t))
        return
    }

    sshChannel, _, err := newChannel.Accept()
    if err != nil {
        log.Fatalf("Could not accept channel (%s)", err)
        return
    }

    bash := exec.Command("bash")

    close := func() {
        sshChannel.Close()
        _, err := bash.Process.Wait()
        if err != nil {
            log.Printf("Failed to exit bash (%s)", err)
        }
        log.Printf("Session closed")
    }

    f, err := pty.Start(bash)
    if err != nil {
        log.Printf("Could not start pty (%s)", err)
        close()
        return
    }

    var once sync.Once
    go func() {
        io.Copy(sshChannel, f)
        once.Do(close)
    }()
    go func() {
        io.Copy(f, sshChannel)
        once.Do(close)
    }()
}

コードについていくつか補足します。

ローカルPCとSSHサーバのSSHセッションを確立する前に、まずTCPレイヤーのコネクションを確立する必要があります。私の中でTCPコネクションはローカルマシンとリモートマシンのソケット同士の論理的な回線というイメージで、SSHセッションというと、SSHのレイヤーすなわちアプリケーションレイヤーまで広がり、セッションの中でコネクションが管理されているイメージです。
実際のコードでは、net.ListenTCPをListenし、Acceptして返ってきたTCPコネクションを、ssh.NewServerConnに渡すことで、認証を行った後、SSHのセッションが確立されます。

SSHのセッションが確立されると、あとはSSH経由でコマンドを実行して、その結果が対話的に表示されるようにしたいです。この対話的の部分にはio.Copyを使います。
io.Copyを使うと、io.Readerインターフェースからio.Writerインターフェースにそのままデータを渡すことができます。これを使って、擬似端末(pty)とSSHチャンネル(goのchannelとは違う、SSHのコネクションのようなもの)の間で双方向にCopyしてやると、入出力が対話的にやれます。

使い方

SSHサーバのホスト認証のために、公開鍵と秘密鍵のペアを作成し、リポジトリのルートディレクトリの置きます。

ssh-keygen -t rsa -N '' -f ./id_rsa

生成された公開鍵(id_rsa.pub)はローカルPCのknown_hostsに以下のような感じで登録しておきます。

[localhost]:2222 ssh-rsa AAAAB3・・・・

あとは下のコマンドでDocker上にSSHサーバが起動します。

$ docker-compose up
Starting gosshd_gosshd_1 ... done
Attaching to gosshd_gosshd_1
gosshd_1  | ==> Installing Dependencies
gosshd_1  | go get -u github.com/golang/dep/...
gosshd_1  | dep ensure
gosshd_1  | go run main.go
gosshd_1  | 2018/07/28 12:44:31 Listening on 2222...

ローカルPCからポート2222にSSH接続すると、コンテナの中に繋がります。

$ ssh tsurubee@localhost -p 2222
root@9cd2bdaf33c0:/go/src/gosshd#

今回ユーザ認証の機能はOFFにしているのでユーザ名はなんでも大丈夫です。 あとは普通にサーバにSSHで繋いだときのようにコマンドが実行できます。

root@9cd2bdaf33c0:/go/src/gosshd# pwd
/go/src/gosshd
root@9cd2bdaf33c0:/go/src/gosshd# ls
Gopkg.lock  Makefile   docker-compose.yml  id_rsa.pub  vendor
Gopkg.toml  README.md  id_rsa          main.go

さいごに

今回はミニマム実装なので、実用的なSSHサーバになるまでには、認証周りの実装や、 SSHグローバルリクエストやチャネルリクエストの取り回しも実装しないといけないです。
これからやりたいのはRFCを読んでSSHプロトコルに関する理解を深めるのと、SSHのリバースプロキシを実装してみることです。

参考

GitHub - mattn/go-sshd: DEPRECATED: Please use https://github.com/gliderlabs/ssh instead