1、SSH协议简介 |
SSH的英文全称为Secure Shell,是IETF(Internet Engineering Task Force)的Network Working Group所制定的一族协议,其目的是要在非安全网络上提供安全的远程登录和其他安全网络服务。如需要SSH的详细信息请参考www.ssh.com(SSH Communications Security Corporation的网站)和www.openssh.org(开放源码的OpenSSH组织的网站)。本文将要介绍的是SSH连接协议中的通道机制和TCP/IP端口转发功能。 |
2、SSH协议中的连接协议 |
SSH连接协议被设计在SSH传输层协议和用户认证协议之上。它主要进行客户服务器结构的连接控制,功能包括交互的登录会话、执行远程命令、转发TCP/IP连接、转发X11连接等等。在传输层安全认证过后,用户认证协议在请求认证时会指明所要的服务名字,消息格式如下: |
byte SSH_MSG_USERAUTH_REQUEST |
string 用户名 (采用ISO-10646 UTF-8 编码 [RFC2279]) |
string 服务名 (采用 US-ASCII字符集) |
string 认证方法名 (采用US-ASCII字符集) |
… 包的其余部分根据具体认证方法而变化。 |
对于连接协议,服务名字是“ssh-connection”。在用户认证顺利完成之后,SSH就按照服务名所指定的服务处理后续数据。SSH连接中将具体的数据和任务划分到一些通道中去,在一个连接中可以集成多个通道,从而完成对一个连接中不同应用的区分,模式如下图: |
图1 SSH的连接与通道 |
连接协议在SSH协议框架中的位置可以用下图来示意说明: |
图2 SSH连接协议在应用框架中的位置 |
3、SSH连接中的通道机制 |
如前文所述,SSH连接协议提供交互的登录会话、执行远程命令、转发TCP/IP连接、转发X11连接等许多功能,而这种种功能的通讯连接都被定义到某个SSH通道中去,而且必须通过通道的方式来进行。在两个SSH通讯的机器上,所有的通道都被复用到一个连接中(如前文图1所示)。正是因为有了通道的概念,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: |
图3 打开通道的消息流程 |
3.2 数据传输 |
在SSH连接协议中数据传输采用窗口方式进行流量控制,即所谓的流控窗口。流控窗口的基本原理就是双方在数据传输开始之前,协商好一个窗口尺寸,一般以字节数来表示。这个窗口尺寸意味着在数据传输进行中,一方连续向另一方发送数据,最多不能超过这个窗口尺寸,如果窗口流量空间被用完,则等待接收方的应答,收到接收方接收数据的应答之后,发送方又将窗口尺寸还原,进行下一次连续的数据传输。当然,传输过程中,窗口尺寸是可以调节的,调整窗口尺寸的消息格式如下: |
byte SSH_MSG_CHANNEL_WINDOW_ADJUST |
uint32 接收方的通道号 |
uint32 增大的字节数 |
流控窗口算法的基本原理可以参考下图4。 |
图4 流控窗口的基本原理示意图 |
从上面图中可以看出,通讯的双方都需要对窗口尺寸进行检查,超出窗口范围的数据既不会被发送方发出,也不会被接收方处理。 |
具体的数据传输消息使用下面的消息格式: |
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 接收方的通道号 |
图5 一个典型的通道操作过程 |
4、SSH所支持的TCP/IP端口转发 |
所谓的端口转发就是将某个机器上某个端口的数据连接原原本本地传送到另一个机器上的另外一个端口中去。在实际应用中可以通过端口转发为两端的多个不同连接在逻辑上建立映射关系,把通讯的两端逻辑相连,而由SSH协议完成中间的实际数据传输和分类工作。 |
4.1 SSH中如何请求端口转发 |
SSH连接协议中的TCP/IP端口转发请求的消息格式如下: |
byte SSH_MSG_GLOBAL_REQUEST |
string "tcpip-forward" |
boolean 是否需要应答 |
string 绑定地址(例如"0.0.0.0") |
uint32 绑定的端口号 |
消息中的‘绑定地址’和‘绑定的端口号’指定了将要监听的socket所绑定的IP地址和端口。如果允许来自任何地址的连接,则将绑定地址设置为“0.0.0.0”。这个消息也就指明了SSH将接管该绑定地址上绑定端口号的数据流。在具体实现时应当对特权端口的转发进行限制,要保证只有经过认证的特权用户才被允许转发特权端口,因为这设计到严重的安全性问题。 |
如果客户端将其中绑定的端口号指定为0,并且将“是否需要应答”指定为TRUE,那么服务器就分配下一个可用的非特权端口号,并且应答一个如下的消息,注意,这个应答消息中没有其他任何多余数据。 |
byte SSH_MSG_GLOBAL_REQUEST_SUCCESS |
uint32 服务器端已经绑定的端口号 |
下面的消息是用来消一个端口转发的。需要注意的是,打开通道的请求应该在收到这个消息的应答之后才会收到。 |
byte SSH_MSG_GLOBAL_REQUEST |
string "cancel-tcpip-forward" |
boolean 是否需要应答 |
string 绑定地址(例如"127.0.0.1") |
uint32 绑定的端口号 |
转发请求实际上规定了SSH客户端如何开始一个转发,转发时要接管哪些系统资源等。 |
4.2 TCP/IP转发通道 |
如果一个连接连到一个端口,而那个端口已经被请求进行远程转发,那么就有一个通道会被打开以转发这个端口到远程机器。 |
byte SSH_MSG_CHANNEL_OPEN |
string "forwarded-tcpip" |
uint32 发送方的通道号 |
uint32 初始流控窗口尺寸 |
uint32 最大的包尺寸 |
string 连接到的地址 |
uint32 连接到的端口 |
string 发起方的地址 |
uint32 发起方的端口 |
在具体实现中,客户端必须实现发出请求消息,以申请将给定的端口号进行一个远程的TCP/IP端口转发。 |
如果在一个本地转发的TCP/IP端口上收到一个连接,下面的数据包将会被回送给连接的另一方。注意,连接如果到了哪些没有被显式地请求进行转发的端口,下面的消息也同样可能作为应答。接收下面消息的一方必须决定是否允许转发请求。 |
byte SSH_MSG_CHANNEL_OPEN |
string "direct-tcpip" |
uint32 发送方的通道号 |
uint32 初始流控窗口的尺寸 |
uint32 最大的包尺寸 |
string 要连接的主机 |
uint32 要连接的端口 |
string 发起方的IP地址 |
uint32 发起方的端口 |
‘要连接的主机’和‘要连接的端口’指明了接收方应该把通道连接到哪儿,什么TCP/IP主机的什么端口。‘要连接的主机’域中既可以是一个域名,也可以是一个数字的IP地址。‘发起方的IP地址’是一个数字的IP地址,该地址代表了发起这个连接请求的机器,‘发起方的端口’则是这个连接在该机器上所用的端口。 |
被转发的TCP/IP通道是独立于任何会话的,关闭一个会话通道在任何情况下都不表示被转发的连接要被关闭。 |
5、一些讨论 |
端口转发存在潜在的威胁,它可能会允许一个入侵者穿过诸如防火墙之类的安全界限,因为SSH编码后的数据其安全性在防火墙层面上是得不到保证的,一旦被截获后就会带来严重后果。 |
SSH连接协议中,端口转发并没有为用户提供什么全新的东西,但是它使得打开安全的通讯隧道变得非常容易,为用户方便地使用数据传输提供了可靠的保证。 |
SSH连接协议建议,SSH连接层应当允许对端口转发的策略进行控制,而且允许管理者在适当时候关闭转发功能以避免上面所说的潜在威胁。 |
总而言之,SSH连接协议中通道和转发的概念使得更高层的应用结构变得简单清晰,也变得方便易用了。 |
前一篇:SSH:安全外壳协议
后一篇:TELNET协议规范