SSH 协议 和 Go SSH 库 转载
转载自gossh协议库
导读
SSH, The Secure Shell Protocol (安全 Shell 协议),是一个使用广泛的网络协议。
在中文互联网世界,关于 SSH 协议的介绍,往往都把重点放到了安全(Secure)方面的细节。这样的文章对于开发者来说,意义并不大,原因在于:
- 此类文章是以密码学为基础的。而密码学专业程度较高,对于开发者来说理解成本高。
- 其次,SSH 安全算法部分是 SSH 协议中最不可变的部分。即使完全理解了这部分,对于对 SSH 协议的二次开发,也没有什么帮助。
因此,本文不会仔细介绍 SSH 中 Secure 的细节。而是从整体和分层的角度尝试理解协议作者的设计考量。
和 HTTP 协议一样,SSH 协议是一个标准化的协议,由 IETF 制定,主要的 RFC 有:
- RFC 4251: The Secure Shell (SSH) Protocol Architecture
- RFC 4252: The Secure Shell (SSH) Authentication Protocol
- RFC 4253: The Secure Shell (SSH) Transport Layer Protocol
- RFC 4254: The Secure Shell (SSH) Connection Protocol
还有一些其他 RFC 在实际场景中应用较窄,在此就不列举了。
RFC 文档是网络协议的完整定义,追求的是无歧义和准确性,这导致 RFC 文档对于初学者不够友好,比较晦涩。因此,本文对 SSH 协议的介绍不会按照 RFC 的顺序和结构来进行,而是按照更符合人类认知的方式来进行。对于一些重要的部分,本文会给出对应的 RFC 章节的引用,以方便定位。
本文假设读者使用过 SSH 客户端进行过远程登录。行文上,本文会以:从整体到局部,从低层到顶层,介绍 SSH 协议的包结构。然后以 SSH 登录一台主机执行一条命令的场景为例,通过追踪 Google 维护的 Go SSH 库 x/crypto/ssh
的源码,来实际感受 SSH 协议的整个流程。本文希望读者可以:真正理解 SSH 的整体流程,理解 SSH 协议的设计考量,初步具备对 SSH 协议进行二次开发的能力。
SSH 协议
SSH 协议架构
本部分主要来自于: rfc4251
high level
+-------------------------+---------------------+
| Authentication Protocol | Connection Protocol |
+-------------------------+---------------------+
| Transport Layer Protocol |
+-----------------------------------------------+
| Underlying Connection |
+-----------------------------------------------+
low level
SSH 协议由 3 个子协议构成。从底层到顶层分别是:
- 传输层协议(rfc4253),定义了 SSH 协议数据包的格式以及 Key 交换算法。
- 认证协议(rfc4252),定义了 SSH 协议支持的用户身份认证算法。
- 连接协议(rfc4254),定义了 SSH 支持功能特性:交互式登录会话、TCP/IP 端口转发、X11 Forwarding。
需要特别说明的是:
- 传输层协议底层连接默认是 TCP 协议。但是,这并不是强制的,在现实中,SSH 可以运行在任意提供可靠性保证的底层连接之上。
- 从层次再看认证协议和连接协议可以认为处于同一层。从时序上来看,认证协议是连接协议的前置条件。
SSH 传输层协议
数据包 (Packet) 结构
- 字节序:大端(网络字节序)
- SSH 最小传输单元为数据包 (Packet),两个方向的数据包格式是一致的。
- 数据包格式如下:
uint32
packet_length = len(payload) + len(padding) + 1。byte
padding_length = len(padding)。[]byte
payload。有效负载,消息 Message。[]byte
padding,随机字节数组。[]byte
mac (Message Authentication Code - MAC)
- 数据包字段加密方式如下所示:
- packet_length 和 packet_length 作为整体加密:
crypto/cipher.Stream.XORKeyStream(byte[0:5], byte[0:5])
- payload 加密:
crypto/cipher.Stream.XORKeyStream(payload, payload)
- padding 加密:
crypto/cipher.Stream.XORKeyStream(padding, padding)
- mac 不需要加密
- packet_length 和 packet_length 作为整体加密:
(更多参见:rfc4253#section-6)
消息结构
Packet 定义的是 SSH 协议的最小传输单元,SSH 协议真正的业务数据是放在 payload 部分中的。在 SSH 协议中,payload 部分被称为消息 Message。
消息的格式各不相同,总的来说是由消息的类型来决定的,因此从整体看消息的结构为:
byte
消息类型编号。[]byte
消息数据,具体定义由消息类型决定。
消息数据部分,可能包含多个字段,不同的字段的序列化方式参见:rfc4251#section-5。
SSH 协议对消息编号按照子协议类型进行了划分(rfc4251#section-7):
- 传输层协议:
- 1~19 传输层通用消息,如 disconnect, ignore, debug 等等。
- 20~29 Key 交换算法协商(参见下文:传输层协议流程)。
- 30~49 Key 交换(同一个编号,在不同的 Key 交换算法中定义是不同的)。
- 认证协议:
- 50~59 用户认证通用消息。
- 60~79 给特定的用户认证方法使用(同一个编号,在不同的认证方法中定义是不同的)。
- 连接协议:
- 80~89 连接协议通用消息。
- 90~127 Channel 相关消息。
- 为客户端协议保留:128~191。
- 本地扩展:192~255。
传输层协议流程
- 建立底层连接(以 TCP 协议为例):
- Client 请求建立 TCP连接。
- Server Accept 完成 TCP 连接建立。
- 协议版本交换(rfc4253#section-4.2):。
- Client 发送字符串,必须以
SSH-2.0-
开头,以\r\n
结尾。这部分可以是任意 ASCII 码> 32
的字符。如SSH-2.0-Go\r\n
,。 - Server 发送字符串,格式要求和 Client 一致。如
SSH-2.0-dropbear_2022.83\r\n
。
- Client 发送字符串,必须以
- Key 交换算法协商,参见:rfc4253#section-7.1,也可以参考下文具体编码示例。
- Key 交换算法执行,比如 Diffie-Hellman Key Exchange 参见:rfc4253#section-8,也可以参考下文具体编码示例。
解释:
- 上述第 2、3、4 步,是 SSH 协议中的仅有的明文传输的部分。
- 上述第 2 步,是 SSH 协议中唯一一个消息格式不符合上文包格式定义的流程。本文介绍的 SSH 协议实际上是 SSH 协议的第 2 版。和其他网络协议类似,SSH 协议也是先有了实现,再进行标准化。因此在这一步,使用了文本格式,以实现对历史上旧版本的识别和兼容。
- 上述第 2 步,Client 和 Server 发送的字符串,没有前后依赖关系,一般情况下,在建立底层连接后,Client、Server 会立即向对方发送版本信息。
- 上述第 3、4 步,是 SSH 协议号称安全的关键步骤。SSH 的核心目标就是在不安全的底层连接(如 TCP)之上,建立一个安全的连接,以实现远程登录,端口转发等特性。因此,自然而然的想法就是对传输的数据进行加密。但是,加密必然需要 Client 和 Server 拥有配对的特定秘钥(key),这就是秘钥分发问题。非对称加密算法天然不存在秘钥分发问题,一种办法是所有数据均使用非对称加密算法加密,但是非对称加密算法性能太差,加解密成本难以接受。因此实际上 SSH 协议采用了如下思路:真正的数据加密仍然使用对称加密算法,而对称加密算法的秘钥,由非对称的加密算法进行保护,此类算法在 SSH 协议中有很多种,被称为 Key 交换算法。因为 Key 交换算法是 SSH 安全性的基石。没人可以 100% 保证某个 Key 交换算法一定是安全的。因此 SSH 协议在执行 Key 交换算法之前,需先进行 Key 交换算法协商,来确定要使用哪种 Key 交换算法。
- 上述第 3、4 步,不仅仅只在第一次连接执行一次,在整个 SSH 连接期间,会根据一些策略,重新执行以生成新的 Key,以保证安全性。
SSH 认证协议
SSH 支持如下几种身份认证协议:
none
,服务端关闭身份认证,也就是说,任意用户都可以连接到该服务端(rfc4252#section-5.2)。publickey
,基于公钥的身份认证(rfc4252#section-7)。password
,基于密码的身份认证。(rfc4252#section-8)hostbased
,比较少见,略(rfc4252#section-9)。GSS-API
,校验 (rfc4462)。
具体细节本部分就不多赘述了,想了解更多,可以参考上文 RFC 文档,也可以参见下文示例代码。
SSH 连接协议
Channel
SSH 连接协议定义的交互式登录终端会话、TCP/IP 端口转发、X11 Forwarding 的这些功能,都工作在自己的通道 (Channel) 之上的。
在 SSH 协议中,Channel 实现了对底层连接的多路复用,就是一个虚拟连接,这就是该子协议叫做连接协议的原因。具体而言 Channel:
- 通过一个数字来进行标识和区分这些 Channel。
- 实现流控 (窗口)。
因此,SSH 连接协议实现的这些功能,都需先建立 Channel,流程如下:
-
服务端和客户端任意一方,发送类型为
SSH_MSG_CHANNEL_OPEN
(90) 的消息,通知对方需要建立 Channel。byte SSH_MSG_CHANNEL_OPEN (90) string channel type, 可选值为: 'session', 'x11', 'forwarded-tcpip', 'direct-tcpip' 参见 https://www.rfc-editor.org/rfc/rfc4250#section-4.9.1 uint32 sender channel 编号 uint32 初始化窗口大小 uint32 最大包大小 .... 下面是 channel type 特定数据
-
另一方接收到消息后,回复类型为
SSH_MSG_CHANNEL_OPEN_CONFIRMATION
(91) 或SSH_MSG_CHANNEL_OPEN_FAILURE
(92) 的消息来告知打开成功或者失败。 成功定义如下:byte SSH_MSG_CHANNEL_OPEN_CONFIRMATION (91) uint32 recipient channel 编号,这个是 SSH_MSG_CHANNEL_OPEN 中 sender channel 的值 uint32 sender channel 编号 uint32 初始化窗口大小 uint32 最大包大小 .... 下面是 channel type 特定数据
失败定义如下:
byte SSH_MSG_CHANNEL_OPEN_FAILURE (92) uint32 recipient channel uint32 错误码 reason code string 描述,格式为 ISO-10646 UTF-8 encoding [RFC3629] string language tag [RFC3066]
预定义的错误码定义如下:
Symbolic name reason code ------------- ----------- SSH_OPEN_ADMINISTRATIVELY_PROHIBITED 1 SSH_OPEN_CONNECT_FAILED 2 SSH_OPEN_UNKNOWN_CHANNEL_TYPE 3 SSH_OPEN_RESOURCE_SHORTAGE 4
上文介绍了 Channel 建立的过程,细节参见 rfc4254#section-5.1。
Channel 建立完成后,在 Channel 中进行数据传输,主要有:
-
流量控制类消息,调节窗口大小。
byte SSH_MSG_CHANNEL_WINDOW_ADJUST uint32 recipient channel uint32 bytes to add
-
数据消息,消息的长度为
min(数据长度, 窗口大小, 传输层协议的限制)
。-
普通数据,如交互式会话的标准输入、标准输出。
byte SSH_MSG_CHANNEL_DATA uint32 recipient channel string data
-
扩展数据,如交互式会话的标准出错,标准出错对应 data_type_code 为 1,是
data_type_code
唯一的预定义的值。byte SSH_MSG_CHANNEL_EXTENDED_DATA uint32 recipient channel uint32 data_type_code string data
-
Channel 关闭(rfc4254#section-5.3),在此不多赘述了。
最后,在打开一个特定类型的 Channel 后,需要对这个 Channel 进行 Channel 粒度的配置。如,建立了一个 session 类型的 Channel 后,请求对方创建一个伪终端 (pty、pseudo terminal)。这类的请求叫做 Channel 特定请求(Channel-Specific Requests
),这类场景使用相同的数据格式:
byte SSH_MSG_CHANNEL_REQUEST (98)
uint32 recipient channel,对方的 sender channel 编号
string request type in US-ASCII characters only 请求类型,参见:https://www.rfc-editor.org/rfc/rfc4250#section-4.9.3
boolean want reply 是否需要对方回复
.... 下面是 request type 特定数据
类似的,对于 SSH_MSG_CHANNEL_REQUEST
消息,如果 want reply 为 true,对方应使用 SSH_MSG_CHANNEL_SUCCESS
(98)、SSH_MSG_CHANNEL_FAILURE
(100) 进行回复。
交互式会话
在 SSH 语境下,会话(Session)代表远程执行一个程序。这个程序可能是 Shell、应用。同时,它可能有也可能没有一个 tty、可能涉及也可能不涉及 x11 forward。
-
客户端打开一个类型为
session
的 Channel(为了安全 ssh 客户端应该拒绝创建 session 的请求)。byte SSH_MSG_CHANNEL_OPEN (90) string "session" uint32 sender channel uint32 initial window size uint32 maximum packet size
-
服务端回复一个类型为
SSH_MSG_CHANNEL_OPEN_CONFIRMATION
的消息。至此 Session 类型的 Channel 创建完成。 -
客户端可以请求创建一个伪终端(pty、Pseudo-Terminal)。
byte SSH_MSG_CHANNEL_REQUEST uint32 recipient channel string "pty-req" boolean want_reply string TERM environment variable value (e.g., vt100) uint32 terminal width, characters (e.g., 80) uint32 terminal height, rows (e.g., 24) uint32 terminal width, pixels (e.g., 640) uint32 terminal height, pixels (e.g., 480) string encoded terminal modes
-
关于 x11 forward 参见 rfc4254#section-6.3。
-
客户端可以请求设置环境变量。
byte SSH_MSG_CHANNEL_REQUEST uint32 recipient channel string "env" boolean want reply string variable name string variable value
-
客户端启动一个 Shell、执行一个命令、调用一个子系统,如下三种情况同一个 Channel 三选一。
-
启动一个 Shell
byte SSH_MSG_CHANNEL_REQUEST uint32 recipient channel string "shell" boolean want reply
-
执行一个命令
byte SSH_MSG_CHANNEL_REQUEST uint32 recipient channel string "exec" boolean want reply string command
-
调用其他子系统(如 sftp)
byte SSH_MSG_CHANNEL_REQUEST uint32 recipient channel string "subsystem" boolean want reply string subsystem name
-
-
上述的启动的程序的输入输出通过如下类型的消息传输:
- 标准输入、标准输出:
SSH_MSG_CHANNEL_DATA
,具体参见上文。 - 标准出错:
SSH_MSG_CHANNEL_EXTENDED_DATA
,扩展类型为SSH_EXTENDED_DATA_STDERR
,具体参见上文。 -
伪终端设置终端窗口大小指令(详见:rfc4254#section-6.7):
byte SSH_MSG_CHANNEL_REQUEST uint32 recipient channel string "window-change" boolean FALSE uint32 terminal width, columns uint32 terminal height, rows uint32 terminal width, pixels uint32 terminal height, pixels
-
信号(详见:rfc4254#section-6.9):
byte SSH_MSG_CHANNEL_REQUEST uint32 recipient channel string "signal" boolean FALSE string signal name (without the "SIG" prefix)
-
退出码(详见:rfc4254#section-6.10):
byte SSH_MSG_CHANNEL_REQUEST uint32 recipient channel string "exit-status" boolean FALSE uint32 exit_status
-
退出信号(详见:rfc4254#section-6.10):
`
byte SSH_MSG_CHANNEL_REQUEST uint32 recipient channel string "exit-signal" boolean FALSE string signal name (without the "SIG" prefix) boolean core dumped string error message in ISO-10646 UTF-8 encoding string language tag [RFC3066]
- 标准输入、标准输出:
TCP/IP 端口转发
SSH 协议本质上,是建立了在 client 到 server 端这两个设备之间建立了一条加密通讯链路。SSH 基于此实现了两个方向的端口转发:
- 本地转发(direct-tcpip): 将 client 监听的 tcp 端口连接转发到 server 上。
- 远端转发(forwarded-tcpip):将 server 监听的 tcp 端口连接转发到 client 上。
如上两者,在协议层面上,最大的区别在于(forwarded-tcpip vs. direct-tcpip):
- 对于远端转发:流量入口端口位于 server 端,因此 SSH 协议需要提供一种机制,可以让 client 告知 server 监听的 tcp 端口。
- 而对于本地转发:流量入口位于 client,因此 client 程序自身就可以自助的监听 tcp 端口,而不涉及 client 和 server 端的通讯,因此 client 监听端口不是 SSH 协议需要关心的内容。
direct-tcpip 流程
- client 监听一个 tcp 端口,并 accept 连接(该步骤不属于 ssh 协议,属于 ssh 的实现部分)。
-
client accept 返回后, client 发起建立一个类型为
direct-tcpip
的 Channel。byte SSH_MSG_CHANNEL_OPEN string "direct-tcpip" uint32 sender channel uint32 initial window size uint32 maximum packet size string host to connect uint32 port to connect string originator IP address uint32 originator port
-
server 接收到消息后,和
host to connect:port to connect
TCP 端口建立 TCP 连接。 -
至此,转发 Channel 建立完成,后续通过
SSH_MSG_CHANNEL_DATA
进行双向数据的转发。 -
该流程对应的 openssh client 命令为:
ssh -L [LOCAL_IP:]LOCAL_PORT:DESTINATION:DESTINATION_PORT [USER@]SSH_SERVER
forwarded-tcpip 流程
-
准备阶段(具体参见: rfc4254#section-7.1):
-
client 请求 server 监听 tcp 端口,作为流量入口。
byte SSH_MSG_GLOBAL_REQUEST string "tcpip-forward" boolean want reply string address to bind (e.g., "0.0.0.0") uint32 port number to bind
-
server 根据请求信息,监听对应端口,并回复:
byte SSH_MSG_REQUEST_SUCCESS uint32 port that was bound on the server
-
-
server accept 返回后, server 发起建立一个类型为
direct-tcpip
的 Channel。byte SSH_MSG_CHANNEL_OPEN string "forwarded-tcpip" uint32 sender channel uint32 initial window size uint32 maximum packet size string address that was connected uint32 port that was connected string originator IP address uint32 originator port
-
client 接收到消息后,和
address that was connected:port that was connected
TCP 端口建立 TCP 连接。 -
至此,转发 Channel 建立完成,后续通过
SSH_MSG_CHANNEL_DATA
进行双向数据的转发。 -
该流程对应的 openssh client 命令为:
ssh -R [REMOTE:]REMOTE_PORT:DESTINATION:DESTINATION_PORT [USER@]SSH_SERVER
特别说明:
- 每个 TCP 连接,都会创建一个 Channel。
- 关于端口转发部分,参见:rfc4254#section-7。
Go SSH 库
主要介绍的是 golang/x/crypto 模块中的 SSH 库。
准备
fork golang/x/crypto,并 clone 下来。
git clone https://github.com/rectcircle/crypto.git
使用 IDE (VSCode) 打开。
code crypto
核心 API
该库实现了 SSH 协议,全部 API 参见:godoc。
API 可以分为两个部分,分别是 Client 和 Server。下面将分别介绍。
Client
该库客户端能力通过 ssh.Client
结构体提供。
该结构体的构造函数为: func ssh.Dial(network, addr string, config *ClientConfig) (*Client, error)
该函数流程如下:
- 使用
func net.Dail
建立底层链接,获得net.Conn
。 - 调用
func ssh.NewClientConn
完成 SSH 传输层协议和认证协议(具体参见上文)部分。 - 调用
func ssh.NewClient
返回ssh.Client
。
注意:如果想使用自定义的底层连接,可以自己构造一个实现了 net.Conn
的对象,然后参考上述的 ssh.Dial
的实现构造一个 ssh.Client
。
上述构造函数第三个参数 ssh.ClientConfig
结构体,用来配置 SSH Client。部分字段说明如下:
User string
用户名,对应 ssh 命令的ssh 用户名@xxx
用户名部分。Auth []AuthMethod
鉴权方法,支持:- 密码认证:
ssh.Password()
和ssh.PasswordCallback()
。 - gss api 认证,即 kerberos 认证:
ssh.GSSAPIWithMICAuthMethod()
。 - 公钥认证:
ssh.PublicKeys()
和ssh.PublicKeysCallback()
。 - Keyboard 认证:
ssh.KeyboardInteractiveChallenge()
。 - 多种认证依次尝试(模拟 ssh 命令的行为):
ssh.RetryableAuthMethod()
。
- 密码认证:
HostKeyCallback HostKeyCallback
SSH Server Host Key 的校验,预防 SSH 中间人攻击,关于中间人攻击本文不多介绍,可以自行搜索。BannerCallback BannerCallback
,在 SSH 认证协议部分,定义一种 Banner 消息类型,以允许服务端发送一些文本消息给客户端,具体参见 rfc4252#section-5.4。
获取到 *ssh.Client
对象后,即可通过如下 API 使用 SSH 连接协议(具体参见上文)提供的能力。
- 交互式会话:
func (c *Client) NewSession() (*Session, error)
,将返回ssh.Session
对象(交互式会话介绍,参见上文)。- 请求创建一个伪终端
func (s *Session) RequestPty(term string, h, w int, termmodes TerminalModes) error
- 请求设置环境变量
func (s *Session) Setenv(name, value string) error
- 启动一个 Shell、执行一个命令、调用一个子系统:
- 启动 Shell:
func (s *Session) Shell() error
。 - 执行命令(以下选择一个):
func (s *Session) Start(cmd string) error
,在远端启动一个命令。func (s *Session) Wait() error
,等待远端执行完成。func (s *Session) Run(cmd string) error
,等价于先 Start 再 Wait。func (s *Session) Output(cmd string) ([]byte, error)
,在远端执行一个命令,并等待完成,并将标准输出作为字节数组返回。func (s *Session) CombinedOutput(cmd string) ([]byte, error)
,执行一个命令,并等待完成,并将标准输出和标准出错合并作为字节数组返回。
- 调用子系统:
func (s *Session) RequestSubsystem(subsystem string) error
。
- 启动 Shell:
- 通过如下 API 获取到标准输入、标准输出、标准出错和远端进行交互。
- 请求创建一个伪终端
- 端口转发(本地转发和远端转发介绍,参见上文):
- 本地转发:
func (c *Client) Dial(n, addr string) (net.Conn, error)
除了支持 TCP/IP 外,还支持 openssh 扩展的转发unix domain socket
中,即 n 参数支持:”tcp”, “tcp4”, “tcp6”, “unix”。 - 本地转发:
func (c *Client) DialTCP(n string, laddr, raddr *net.TCPAddr) (net.Conn, error)
,只支持 TCP/IP。 - 远端转发:
func (c *Client) Listen(n, addr string) (net.Listener, error)
除了支持 TCP/IP 外,还支持 openssh 扩展的转发unix domain socket
中,即 n 参数支持:”tcp”, “tcp4”, “tcp6”, “unix”。 - 远端转发:
func (c *Client) ListenTCP(n, addr string) (net.Listener, error)
只支持 TCP/IP。 - 远端转发:
func (c *Client) ListenUnix(socketPath string) (net.Listener, error)
支持unix domain socket
。
- 本地转发:
Server
该库 server 能力通过 func NewServerConn(c net.Conn, config *ServerConfig) (*ServerConn, <-chan NewChannel, <-chan *Request, error)
函数提供。,该函数,完成了 SSH 传输层协议和认证协议(具体参见上文)部分。该函数的返回值说明如下:
*ssh.ServerConn
对net.Conn
的封装,主要用于远端转发,具体参见下文。<-chan NewChannel
获取由 ssh client 创建的 Channel,用来实现交互式会话、本地转发。<-chan *Request
主要用于远端转发,对应SSH_MSG_GLOBAL_REQUEST
消息,具体参见下文。error
处理 SSH 传输层协议和认证协议出现错误,如认证失败。
ssh.NewChannel
接口对应一个 client 创建的 Channel 的消息(SSH_MSG_CHANNEL_OPEN
具体参见上文:Channel 部分),方法有如下几个:
ChannelType() string
channel 的类型,可选值为:’session’, ‘x11’, ‘forwarded-tcpip’, ‘direct-tcpip’ 详见: rfc4250#section-4.9.1Accept() (Channel, <-chan *Request, error)
同意建立该 Channel。ssh.Channel
接口对应一个已经建立 Channel,在 server 端,该接口方法解释如下:Read(data []byte) (int, error)
从 Channel 中读取数据,对应 client -> server 的SSH_MSG_CHANNEL_DATA
消息(参见上文 channel),在 session 场景对应 stdin。Write(data []byte) (int, error)
向 Channel 中写入数据,对应 server -> client 的SSH_MSG_CHANNEL_DATA
消息(参见上文 channel),在 session 场景对应 stdout。Close() error
关闭该 channel,对应SSH_MSG_CHANNEL_CLOSE
消息 (参见上文 channel)。CloseWrite() error
SendRequest(name string, wantReply bool, payload []byte) (bool, error)
对应 server -> client 在该 Channel 上的SSH_MSG_CHANNEL_REQUEST
(参见上文 SSH 连接协议),主要在 session channel 场景有如下几个类型:"window-change"
,发送 pty 的 window-change 信息。"exit-status"
,发送 cmd 的退出码消息。
ssh.Request
结构体对应 client -> server 在该 Channel 上的SSH_MSG_CHANNEL_REQUEST
(参见上文 SSH 连接协议)该结构体有如下几个字段和方法:Type string
字段,在 session channel 场景有用,有如下几个类型:"pty-req"
"shell"
"subsystem"
"env"
"exec"
WantReply bool
字段,是否需要回复。Payload []byte
字段,type 特定数据,可以使用ssh.Unmarshal()
方法进行反序列化。func (r *Request) Reply(ok bool, payload []byte) error
方法,对WantReply = true
的方法,必须调用该函数进行回复。
Stderr() io.ReadWriter
server -> client,对应SSH_MSG_CHANNEL_EXTENDED_DATA
,参见上文 交互式会话,在 session 场景对应 stderr。Reject(reason RejectionReason, message string) error
拒绝建立该 Channel。ExtraData() []byte
类型特定数据。
从 API 上来看,Go SSH 库 Server API 比 Client API 更加的底层,需要开发者理解 SSH 连接协议 消息相关细节才能很好的进行开发。而 Go SSH 库的 Client API 在比较高的层次,使用起来比较容易。
因此,如果想使用 Go 语言开发 SSH Server 相关需求,建议直接使用或者参考:github.com/gliderlabs/ssh 库,该库提供了类似于 http.Server 的,更高层次的 API。如:
- 远端转发的示例和实现,主要逻辑是对上文
NewServerConn
返回的:<-chan *Request
,接收到"tcpip-forward"
监听端口,接收到"cancel-tcpip-forward"
取消监听。*ssh.ServerConn
,用户请求上面监听的端口时,调用OpenChannel
建立一个 server -> client 的 channel,并进行数据拷贝。
本文主要是探索 SSH 协议的相关结构,因此下文的示例的 server 仍然使用 Go SSH 库来实现。
通用 API
func Unmarshal(data []byte, out interface{}) error
ssh 协议消息字段反序列函数。主要用于ssh.Request.Payload
字段。func Marshal(msg interface{}) []byte
ssh 协议消息字段序列函数。主要用于: