Go进阶35:Go语言自定义自己的SSH-Server

Go进阶35:Go语言自定义自己的SSH-Server

1. 什么是SSH?

SSH是一种网络协议,用于计算机之间的加密登录.

如果一个用户从本地计算机,使用SSH协议登录另一台远程计算机,我们就可以认为,这种登录是安全的,即使被中途截获,密码也不会泄露.

互联网通信早期都是明文通信,一旦被截获,内容就暴露无疑. 1995年,芬兰学者Tatu Ylonen设计了SSH协议,将登录信息全部加密,成为互联网安全的一个基本解决方案,迅速在全世界获得推广, 目前已经成为Linux系统的标准配置.

1.1 使用Go语言 golang.org/x/crypto/ssh

golang.org/x/crypto/ssh 包下载 go get -u golang.org/x/crypto/ssh.

下面的代码展示我们如何在golang代码登陆到SSH. 一下代码制作代码功能展示之用, 没有解决terminal window size 问题和怎么传入ssh 登陆参数的问题.实际使用中有缺陷,通过 tab 补全时并不能正确显示.

更完整教程相见 golang-ssh-01:执行远程命令

package main
import (
    "golang.org/x/crypto/ssh"
    "log"
    "os"
)
func handlerErr(err error, msg string) {
    if err != nil {
        log.Fatalf("%s error: %v", msg, err)
    }   
}   
func main() {
    //ssh 服务地址home.mojotv.cn:22
    client, err := ssh.Dial("tcp", "home.mojotv.cn:22", &ssh.ClientConfig{
        User: "test007",//ssh 用户名
        Auth: []ssh.AuthMethod{ssh.Password("test007")},  //ssh 密码
    })  
    handlerErr(err, "dial")
    session, err := client.NewSession()
    handlerErr(err, "ssh session 创建")
    defer session.Close()
    
    //当前机器的terminal 连接到ssh-session的为终端
    session.Stdout = os.Stdout
    session.Stderr = os.Stderr
    session.Stdin = os.Stdin
    // 配置pty
    modes := ssh.TerminalModes{
        ssh.ECHO:          0,  
        ssh.TTY_OP_ISPEED: 14400,
        ssh.TTY_OP_OSPEED: 14400,
    }  
    //这里设置的固定的terminal size 25x100
    //当terminal 窗口尺寸改变的时候 会导致终端显示错位
    err = session.RequestPty("xterm", 25, 80, modes)
    handlerErr(err, "请求PTY为终端")

    err = session.Shell()
    handlerErr(err, "开始 shell")

    err = session.Wait()
    handlerErr(err, "执行完毕")
}

2. Go语言自定义自己的SSH-Server

以下代码来自我的开源项目中的一个功能模块. mojocn/sshfortress Go语言SSH-Web堡垒机

主要代码来自这个文件sshfortress/fssh/server.go

2.1 创建private key 验证HostKey

import (
	"fmt"
	"github.com/gliderlabs/ssh"
	gossh "golang.org/x/crypto/ssh"
	"io"
	"io/ioutil"
	"log"
	"os"
	"os/exec"
	"path/filepath"
	"sshfortress/util"
)

//创建key 来验证 host public
func createOrLoadKeySigner() (gossh.Signer, error) {
	//key 保存到 系统temp 目录
	keyPath := filepath.Join(os.TempDir(), "fssh.rsa")
	//如果key 不存在则 执行 ssh-keygen 创建
	if _, err := os.Stat(keyPath); os.IsNotExist(err) {
		os.MkdirAll(filepath.Dir(keyPath), os.ModePerm)
		//执行 ssh-keygen 创建 key
		stderr, err := exec.Command("ssh-keygen", "-f", keyPath, "-t", "rsa", "-N", "").CombinedOutput()
		output := string(stderr)
		if err != nil {
			return nil, fmt.Errorf("Fail to generate private key: %v - %s", err, output)
		}
	}
	//读取文件内容
	privateBytes, err := ioutil.ReadFile(keyPath)
	if err != nil {
		return nil, err
	}
	//生成ssh.Signer
	return gossh.ParsePrivateKey(privateBytes)
}

2.2 SSH-Server的handler

homeHandler 主要答应 APP 彩色文本信息( Go进阶19:如何开发多彩动感的终端UI应用 ). 进行SSH代理登陆,动态监听terminal size 变化. 连接本地ssh到远程ssh-session-pty伪终端. 详细功能见代码中的注释.

import (
	"fmt"
	"github.com/gliderlabs/ssh"
	gossh "golang.org/x/crypto/ssh"
	"io"
	"io/ioutil"
	"log"
	"os"
	"os/exec"
	"path/filepath"
	"sshfortress/util"
)


func homeHandler(s ssh.Session) {
	//tty 控制码打印彩色文字
	//mojotv.cn/tutorial/golang-term-tty-pty-vt100
	io.WriteString(s, fmt.Sprintf("\x1b[31;47mmojotv.cn sshfortress 堡垒机 自定义SSH, 当前登陆用户名: %s\x1b[0m\n", s.User()))

	ptyReq, winCh, isPty := s.Pty()
	if !isPty {
		io.WriteString(s, "不是PTY请求.\n")
		s.Exit(1)
		return
	}
	sshConf, err := util.NewSshClientConfig("test007", "test007", "password", "", "")
	if err != nil {
		io.WriteString(s, err.Error())
		s.Exit(1)
		return
	}
	//连接远程服务器SSH
	conn, err := gossh.Dial("tcp", "home.mojotv.cn:22", sshConf)
	if err != nil {
		io.WriteString(s, "unable to connect: "+err.Error())
		s.Exit(1)
		return
	}
	defer conn.Close()
	// 创建远程ssh session
	fss, err := conn.NewSession()
	if err != nil {
		io.WriteString(s, "unable to create fss: "+err.Error())
		s.Exit(1)
		return
	}
	defer fss.Close()

	// 配置terminal
	modes := gossh.TerminalModes{
		gossh.ECHO:          1,     // disable echoing
		gossh.TTY_OP_ISPEED: 14400, // input speed = 14.4kbaud
		gossh.TTY_OP_OSPEED: 14400, // output speed = 14.4kbaud
	}
	// 请求为终端
	if err := fss.RequestPty(ptyReq.Term, ptyReq.Window.Height, ptyReq.Window.Width, modes); err != nil {
		io.WriteString(s, "request for pseudo terminal failed: "+err.Error())
		s.Exit(1)
		return
	}
	//监听终端size window 变化
	go func() {
		for win := range winCh {
			err := fss.WindowChange(win.Height, win.Width)
			if err != nil {
				io.WriteString(s, "windows size changed: "+err.Error())
				s.Exit(1)
				return
			}
		}
	}()

	//linux 一切接文件 io, 连接stdin stdout stderr
	//连接为终端到server
	fss.Stderr = s
	fss.Stdin = s
	fss.Stdout = s
	if err := fss.Shell(); err != nil {
		io.WriteString(s, "failed to start shell: "+err.Error())
		s.Exit(1)
		return
	}
	fss.Wait()
}

2.3 启动Go语言SSH-Server

设置SSH-Server的监听端口,和处理Request的handler. 其次您可以自己定义Key登陆的用户校验机制,结合自己数据库开发出更加服务的SSH-Server应用. 可以开发出自己ssh 聊天服务,自己ssh 贪吃蛇服务…


import (
	"fmt"
	"github.com/gliderlabs/ssh"
	gossh "golang.org/x/crypto/ssh"
	"io"
	"io/ioutil"
	"log"
	"os"
	"os/exec"
	"path/filepath"
	"sshfortress/util"
)

func Run() {
	hostKeySigner, err := createOrLoadKeySigner()
	if err != nil {
		log.Fatal(err)
	}
	s := &ssh.Server{
		Addr:    ":88",
		Handler: homeHandler, //
		//PublicKeyHandler:
		//PasswordHandler: passwordHandler,   不需要密码验证
	}
	s.AddHostKey(hostKeySigner)
	log.Fatal(s.ListenAndServe())
}

func passwordHandler(ctx ssh.Context, password string) bool {
	//check password and username
	//user := ctx.User()
	// 可以结合DB数据库定义用户验证用户登陆
	return true
	//return model.FsshUserAuth(user, password)
}

3. 项目运行效果

3.1 编译项目

git clone https://github.com/mojocn/sshfortress.git
# 这个项目需要gcc windows 用户需要自己安装gcc 
cd sshfortress && go build
# cmd 代码 https://github.com/mojocn/sshfortress/blob/master/cmd/fssh.go
./sshfortress fssh
# ssh 服务开启在88端口  https://github.com/mojocn/sshfortress/blob/master/cmd/fssh.go

3.2 登陆到自定义的SSH-Server

登陆到88端口自定义的SSH服务 ssh mojotv.cn@localhost -p 88

终端输出结果如下

EricZhou@mojotv.cn MINGW64 /
$ ssh mojotv.cn@localhost -p 88
mojotv.cn sshfortress 堡垒机 自定义SSH, 当前登陆用户名: mojotv.cn
Linux homePi 4.14.98-v7+ #1200 SMP Tue Feb 12 20:27:48 GMT 2019 armv7l

The programs included with the Debian GNU/Linux system are free software;
the exact distribution terms for each program are described in the
individual files in /usr/share/doc/*/copyright.

Debian GNU/Linux comes with ABSOLUTELY NO WARRANTY, to the extent
permitted by applicable law.
Last login: Tue Oct 22 14:19:55 2019 from 218.30.116.184
Could not chdir to home directory /home/test007: No such file or directory
$ ls
127.0.0.1:3306  demoFE    fssh.rsa.pub  lib         mnt                      root  sys  www
bin             dev       ginbroRock    lost+found  nginx_default_site.conf  run   tmp
boot            etc       gopath        media       opt                      sbin  usr
data            fssh.rsa  home          miwifi      proc                     srv   var
$ whoami
test007
$

4. 相关文档和项目

目录