Go实现SSH-Server服务端(1)
1、前言概述
Go有一个SSH库(golang.org/x/crypto/ssh)提供了丰富的接口,我们将基于此包实现SSH-Server服务端(SSHD)(也可实现SSH Client、SSH Proxy等),基于此包可实现SSH体系的扩展等。
2、SSH架构
TCP传输层:建立TCP连接,后进行SSH协议处理
Handshake:SSH协议的数据传输(主要是提供数据加密传输)
Authentication:用户认证处理
Channel:SSH协议的连接建立层,主要是多个加密隧道分层逻辑孙道,可复用通道,常见的有session、x11、forwarded-tcpip、direct-tcpip
Request:用于接收创建的Channel请求
3、开发流程图
4、流程说明
配置认证回调及其他配置(注意:NoClientAuth: true,无认证连接建议关闭)
SSH交互秘钥(私钥)双方交换验证(Diffie-hellman)
监听TCP服务(ssh.NewServerConn 实现TCP转SSH连接)
建立SSH隧道(SSH链接层)一个连接可包含channel(handleChannel针对每个channel都单独处理)
处理连接上的每个channel(SSH协议交互的核心逻辑),在处理channel之前, 需要先获取 channel 的类型, 因不同类型的 channel 里面是不同类型的数据,对应不同的处理逻辑。
Channel 的类型如下:
Channel type Reference ------------ --------- session [SSH-CONNECT] x11 [SSH-CONNECT] forwarded-tcpip [SSH-CONNECT] direct-tcpip [SSH-CONNECT]
5、数据交互
交互式 tty 是打开一个终端 tty,然后在 tty 上通过键盘输入命令执行,那么数据交互的过程就是打通 tty 和 bash 的通道。
首先,使用 exec.Command 打开一个bash, 然后将 bash 与 SSH Channel 对接, 从而实现和 bash 的远程交互。
注意:
这里可以使用定制化 tty(比如 git 也可通过 ssh 连接来交互)。如gliderlabs的 sshd 库代码,对外提供了 pty 接口,该库使用 https://github.com/kr/pty 包来实现 sshd 的 tty 交互功能。
这里如果直接将 bash 的输入和输出直接对接 terminal这是错误的操作,因为 bash 没有运行在 tty 中,这里需要一个模拟 tty 来运行 bash。
tty 和 bash 的关系如下(便于理解):
最后需将 bash 的管道(输入和输出)和 Connection 的管道对接,即完成了 SSHD 的 tty 实现逻辑。
6、指令交互
主要有如下几种Request:
shell/exec/subsystem:
shell:启动的一个程序或者 shell
exec:启动的是用户的默认 shell
subsystem:启动一个子程序来执行 connection 里面的命令(如 sftp)
pty-req: 准备一个 pty 等待输入
window-change: 监听 tty 窗口改变事件,及时更新 tty size
7、完整代码
package main import ( "fmt" "golang.org/x/crypto/ssh" "github.com/creack/pty" "io/ioutil" "net" "log" "sync" "io" "os/exec" "encoding/binary" "syscall" "unsafe" ) func privateDiffie() (b ssh.Signer, err error) { private, err := ioutil.ReadFile("~/.ssh/id_rsa") if err != nil { return } b, err = ssh.ParsePrivateKey(private) return } // 开启goroutine, 处理连接的Channel func handleChannels(chans <-chan ssh.NewChannel) { for newChannel := range chans { go handleChannel(newChannel) } } // parseDims extracts two uint32s from the provided buffer. func parseDims(b []byte) (uint32, uint32) { w := binary.BigEndian.Uint32(b) h := binary.BigEndian.Uint32(b[4:]) return w, h } // Winsize stores the Height and Width of a terminal. type Winsize struct { Height uint16 Width uint16 x uint16 // unused y uint16 // unused } // SetWinsize sets the size of the given pty. func SetWinsize(fd uintptr, w, h uint32) { log.Printf("window resize %dx%d", w, h) ws := &Winsize{Width: uint16(w), Height: uint16(h)} syscall.Syscall(syscall.SYS_IOCTL, fd, uintptr(syscall.TIOCSWINSZ), uintptr(unsafe.Pointer(ws))) } func handleChannel(ch ssh.NewChannel) { // 仅处理session类型的channel(交互式tty服务端) if t := ch.ChannelType(); t != "session" { ch.Reject(ssh.UnknownChannelType, fmt.Sprintf("unknown channel type: %s", t)) return } // 返回两个队列,connection用于数据交换,requests用户控制指令交互 connection, requests, err := ch.Accept() if err != nil { log.Printf("Could not accept channel (%s)", err.Error()) return } // 为session启动一个bash bash := exec.Command("bash") // 关闭连接和session close := func() { connection.Close() _, err := bash.Process.Wait() if err != nil { log.Printf("Failed to exit bash (%s)", err.Error()) } log.Println("Session closed") } // 为channel分配一个terminal log.Print("Creating pty...") tty, err := pty.Start(bash) if err != nil { log.Printf("Could not start pty (%s)", err) close() return } // 管道session到bash和visa-versa // 使用 sync.Once 确保close只调用一次 var once sync.Once go func() { io.Copy(connection, tty) once.Do(close) }() go func() { io.Copy(tty, connection) once.Do(close) }() // session out-of-band请求有"shell"、"pty-req"、"env"等几种 go func() { for req := range requests { switch req.Type { case "shell": if len(req.Payload) == 0 { req.Reply(true, nil) } case "pty-req": termLen := req.Payload[3] w, h := parseDims(req.Payload[termLen+4:]) SetWinsize(tty.Fd(), w, h) req.Reply(true, nil) case "window-change": w, h := parseDims(req.Payload) SetWinsize(tty.Fd(), w, h) } } }() } func main() { config := &ssh.ServerConfig{ // 密码验证回调函数 PasswordCallback: func(c ssh.ConnMetadata, pass []byte) (*ssh.Permissions, error) { if c.User() == "demo" && string(pass) == "123456" { return nil, nil } return nil, fmt.Errorf("password rejected for %q", c.User()) }, // NoClientAuth: true, // 客户端不验证,即任何客户端都可以连接 // ServerVersion: "SSH-2.0-OWN-SERVER", // "SSH-2.0-",SSH版本 } // 秘钥用于SSH交互双方进行 Diffie-hellman 秘钥交换验证 if b, err := privateDiffie(); err != nil { log.Printf("private diffie host key error: %s",err.Error()) }else{ config.AddHostKey(b) } // 监听地址和端口 listener, err := net.Listen("tcp", "0.0.0.0:19022") if err != nil { log.Fatalf("Failed to listen on 1022 (%s)", err.Error()) } log.Println("listen to 0.0.0.0:19022") // 接受所有连接 for { conn, err := listener.Accept() if err != nil { log.Printf("Failed to accept incoming connection (%s)", err) continue } // 使用前,必须传入连接进行握手 net.Conn sshConn, chans, reqs, err := ssh.NewServerConn(conn, config) if err != nil { log.Printf("Failed to handshake (%s)", err) continue } log.Printf("New SSH connection from %s (%s)", sshConn.RemoteAddr(), sshConn.ClientVersion()) go ssh.DiscardRequests(reqs) // 接收所有channels go handleChannels(chans) } }
8、测试登录
# 输入密码 123456 登录 ssh -p19022 demo@127.0.0.1 The authenticity of host '[127.0.0.1]:19022 ([127.0.0.1]:19022)' can't be established. RSA key fingerprint is SHA256:kcIoiszRlDALxK2AQyWLNVDYVWWDy89mDNPXqHRX8LQ. Are you sure you want to continue connecting (yes/no)? yes Warning: Permanently added '[127.0.0.1]:19022' (RSA) to the list of known hosts. demo@127.0.0.1's password:
9、开源项目
利用(golang.org/x/crypto/ssh)此包也可以完成一些其他有趣的项目,如:
https://github.com/zachlatta/sshtron 实现了一个在线多人贪吃蛇
https://github.com/shazow/ssh-chat 基于ssh实现一个聊天工具
https://github.com/gliderlabs/ssh 一个通用的sshd框架
参考
https://blog.gopheracademy.com/advent-2015/ssh-server-in-go/
https://scalingo.com/blog/writing-a-replacement-to-openssh-using-go-12.html
https://scalingo.com/blog/writing-a-replacement-to-openssh-using-go-22.html
https://github.com/gliderlabs/ssh