SSH连接协议提供交互的登录会话、执行远程命令、转发TCP/IP连接、转发X11连接等许多功能,而这种种功能的通讯连接都被定义到某个SSH通道中去。在两个SSH通讯的机器上,所有的通道都被复用到一个连接中。正是因为有了通道的概念,SSH所有高层的应有才能够方便地映射到SSH物理连接上去。
既然通道是SSH连接层必不可少的工作元素,SSH连接协议也就必须提供完备的通道操作和控制机制,主要分为打开通道、使用通道、关闭通道以及控制通道四大类基本功能。下面分别叙述:
3.1 打开通道
在使用SSH通讯时,任何一方如果希望打开一个新通道,它首先必须为新通道分配好一个本地的编号,这个编号在本地唯一地标识了该通道。然后它需要向另一方发送一个打开通道的消息:
byte SSH_MSG_CHANNEL_OPEN
string 通道类型(仅限于US-ASCII字符)
uint32 发送方的通道号
uint32 初始的流控窗口尺寸
uint32 最大的包尺寸
... 与特定通道类型相关的数据
消息结构中‘通道类型’是一个名字,用来区分不同用途的通道。‘发送方的通道号’也就是前面讲到的预先分配好的本地编号,它指明了上述消息的发送方所使用的通道。‘初始的流控窗口尺寸’是对通道进行流量控制的一个参数,具体请参考本文中“流控窗口”一小节内容。‘最大的包尺寸’定义了本消息的发送方可以接收的单个数据包的最大尺寸(字节数)。
远端收到上述消息后就要做出决定是否能打开通道,如果确认能够打开一个新通道,就采用如下消息应答:
byte SSH_MSG_CHANNEL_OPEN_CONFIRMATION
uint32 接收方的通道号
uint32 发送方的通道号
uint32 初始的流控窗口尺寸
uint32 最大的包尺寸
... 与特定通道类型相关的数据
这里需要注意的是,应答消息中包含了‘发送方的通道号’和‘接收方的通道号’,这两个通道号分别在SSH连接的两端标识了这个新通道。
如果远端机器收到打开通道的请求消息后不允许打开通道,就采用下面的应答消息:
byte SSH_MSG_CHANNEL_OPEN_FAILURE
uint32 接收方的通道号
uint32 错误原因编码
string 附加的文本信息(采用ISO-10646 UTF-8 编码[RFC2279])
string 语言标记(定义请参考[RFC1766])
这相当于一个拒绝请求的应答,请求方被拒绝之后,可以根据错误原因编码来确定错误情况,并将拒绝应答消息中的附加文本信息按照指定的语言编码显示给本地的操作者(用户),以此提示用户,可以帮助用户更好地进行排错工作。错误原因编码定义如下:
#define SSH_OPEN_ADMINISTRATIVELY_PROHIBITED 1
#define SSH_OPEN_CONNECT_FAILED 2
#define SSH_OPEN_UNKNOWN_CHANNEL_TYPE 3
#define SSH_OPEN_RESOURCE_SHORTAGE 4
这样,打开通道的消息过程可以解释如下图3:
图1 打开通道的消息流程
3.2 数据传输
在SSH连接协议中数据传输采用窗口方式进行流量控制,即所谓的流控窗口。流控窗口的基本原理就是双方在数据传输开始之前,协商好一个窗口尺寸,一般以字节数来表示。这个窗口尺寸意味着在数据传输进行中,一方连续向另一方发送数据,最多不能超过这个窗口尺寸,如果窗口流量空间被用完,则等待接收方的应答,收到接收方接收数据的应答之后,发送方又将窗口尺寸还原,进行下一次连续的数据传输。当然,传输过程中,窗口尺寸是可以调节的,调整窗口尺寸的消息格式如下:
byte SSH_MSG_CHANNEL_WINDOW_ADJUST
uint32 接收方的通道号
uint32 增大的字节数
流控窗口算法的基本原理可以参考下图4。
图2 流控窗口的基本原理示意图
从上面图中可以看出,通讯的双方都需要对窗口尺寸进行检查,超出窗口范围的数据既不会被发送方发出,也不会被接收方处理。
具体的数据传输消息使用下面的消息格式:
byte SSH_MSG_CHANNEL_DATA
uint32 接收方的通道号
string 数据
在通道中传输的数据消息中又可以分成若干中不同类型的数据,主要方式是将数据部分再划分成数据类型编码和实际数据两个部分,这样协议可以根据数据类型做不同的处理。这种扩展的数据消息格式如下:
byte SSH_MSG_CHANNEL_EXTENDED_DATA
uint32 接收方的通道号
uint32 数据类型编码
string 数据
这些消息中的数据与普通数据一样也会占用流控窗口的空间。
3.3 关闭通道
如果通讯的某一方将不再向某通道发送数据,它应当发送一个SSH_MSG_CHANNEL_EOF消息。
byte SSH_MSG_CHANNEL_EOF
uint32 接收方的通道号
对于这个消息无需给出明确的应答,但是应用程序在收到这个消息后也可以向通道的另一端发送一个EOF,而不管对端是什么应用。值得注意的是在这个消息之后,通道并没有被关闭,在另一端仍然可能继续发送更多的数据。这个消息的数据不受流控窗口限制,在通讯的任何时候都可以被发送。
上面只是讲到停止数据传送,如果希望终止一个通道,那么通讯的某一方就要发送一个SSH_MSG_CHANNEL_CLOSE消息。接收到这个消息的一方则必须回送一个SSH_MSG_CHANNEL_CLOSE消息,除非是首先提出关闭通道的请求并发出该消息。一旦通讯中的一方在发出这样一个消息,并且还收到这样一个消息,该通讯端就认为相关通道已经被关闭,而本地相应的通道号也就可以再利用了。需要提醒的一点是。即使通讯中某一方并没有收到任何要求数据传输停止的SSH_MSG_CHANNEL_EOF消息,它也可以发送SSH_MSG_CHANNEL_CLOSE消息来关闭。
在实际应用中,SSH协议规范中建议将SSH_MSG_CHANNEL_CLOSE消息之前的任何数据都传送给实际的目的方,以保证通道关闭之前数据的完整性。
3.4 通道相关的特殊请求
SSH协议规定不同类型的通道可以有不同的功能扩展,比如一个交互会话的通道就可以请求一个pty(也就是伪终端)来支持用户交互操作。
关于这些特殊的功能扩展也定义了消息格式,在此基础上各种不同类型的通道可以扩展各自的功能:
byte SSH_MSG_CHANNEL_REQUEST
uint32 接收方的通道号
string 请求类型(仅限US-ASCII字符)
boolean 是否需要应答
... 类型相关的特殊数据
这些消息不占用流控窗口的空间,即使在流控窗口用满的情况下也可以被正确发送。其中请求类型对于每一种通道类型来说都是局部的,也就是说不同类型的通道可以有相同类型的请求名字,但是可以含义各异,由具体的通道类型实现来具体解释。这些特殊的请求消息在发送后无需等待应答,客户程序可以继续发送其他后续消息。
上述消息的应答,格式有两种:成功与失败。
byte SSH_MSG_CHANNEL_SUCCESS
uint32 接收方的通道号
byte SSH_MSG_CHANNEL_FAILURE
uint32 接收方的通道号
图3 一个典型的通道操作过程