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:
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
https://github.com/Scalingo/go-ssh-examples
https://datatracker.ietf.org/doc/html/rfc4250#section-4.9.1