在本小节中,我们更为仔细地观察如何建立和拆除一条 TCP 连接。尽管这个主题并不特别令人兴奋,但是它很重要,因为 TCP 连接的建立会显著地增加人们感受到的时延 (如在 Web 上冲浪时)。此外,许多常见的网络攻击(包括极为流行的 SYN 洪泛攻击)利用了 TCP 连接管理中的弱点。
现在我们观察一下一条 TCP 连接是如何建立的。假设运行在一台主机(客户)上的一个进程想与另一台主机(服务器)上的一个进程建立一条连接。客户应用进程首先通知客户 TCP,它想建立一个与服务器上某个进程之间的连接。客户中的 TCP 会用以下方式与服务器中的 TCP 建立一条 TCP 连接:
第一步:客户端的 TCP 首先向服务器端的 TCP 发送一个特殊的 TCP 报文段。该报文段中不包含应用层数据。但是在报文段的首部中的一个标志位(即 SYN 比特)被置为 1。因此,这个特殊报文段被称为 SYN 报文段。
另外,客户会随机地选择一个初始序号(client_isn),并将此编号放置于该起始的 TCP SYN 报文段的序号字段中。该报文段会被封装在一个 IP 数据报中,并发送给服务器。为了避免某些安全性攻击,在适当地随机化选择 client_isn 方面有着不少有趣的研究。
第二步:一旦包含 TCP SYN 报文段的 IP 数据报到达服务器主机(假定它的确到达了!),服务器会从该数据报中提取出 TCP SYN 报文段,为该 TCP 连接分配 TCP 缓存和变量,并向该客户 TCP 发送允许连接的报文段。
在完成三次握手的第三步之前分配这些缓存和变量,使得 TCP 易于受到称为 SYN 洪泛的拒绝服务攻击。这个允许连接的报文段也不包含应用层数据。但是,在报文段的首部却包含 3 个重要的信息。
首先,SYN 比特被置为 1。其次,该 TCP 报文段首部的确认号字段被置为 client _ isn+1。最后,服务器选择自己的初始序号(server_isn),并将其放置到 TCP 报文段首部的序号字段中。这个允许连接的报文段实际上表明了:“我收到了你发起建立连接的 SYN 分组,该分组带有初始序号 client_isn。我同意建立该连接。我自己的初始序号是 server_isn“。该允许连接的报文段被称为 SYNACK 报文段(SYNACK segment)。
第三步:在收到 SYNACK 报文段后,客户也要给该连接分配缓存和变量。客户主机则向服务器发送另外一个报文段;这最后一个报文段对服务器的允许连接的报文段进行了确认(该客户通过将值 server_isn+1 放置到 TCP 报文段首部的确认字段中来完成此项工作)。因为连接已经建立了,所以该 SYN 比特被置为 0。该三次握手的第三个阶段可以在报文段负载中携带客户到服务器的数据。
一旦完成这 3 个步骤,客户和服务器主机就可以相互发送包括数据的报文段了。在以后每一个报文段中,SYN 比特都将被置为 0。注意到为了创建该连接,在两台主机之间发送了 3 个分组,如下图所示。
由于这个原因,这种连接创建过程通常被称为 3 次握手(three-way handshake)。TCP 3 次握手的几个方面将在课后习题中讨论(为什么需要初始序号?为什么需要 3 次握手,而不是两次握手?)。注意到这样一件事是很有趣的,一个攀岩者和一个保护者(他位于攀岩者的下面,他的任务是处理好攀岩者的安全绳索)就使用了与 TCP 相同的 3 次握手通信协议,以确保在攀岩者开始攀爬前双方都已经准备好了。
天下没有不散的宴席,对于 TCP 连接也是如此。参与一条 TCP 连接的两个进程中的任何一个都能终止该连接。当连接结束后,,主机中的 “资源”(即缓存和变量)将被客户释放。举一个例子,假设某客户打算关闭连接,如下图所示。
客户应用进程发出一个关闭连接命令。这会引起客户 TCP 向服务器进程发送一个特殊的 TCP 报文段。这个特殊的报文段让其首部中的一个标志位即 FIN 比特被设置为 1。当服务器接收到该报文段后,就向发送方回送一个确认报文段。然后,服务器发送它自己的终止报文段,其 FIN 比特被置为 1。最后 2 该客户对这个服务器的终止报文段进行确认。此时,在两台主机上用于该连接的所有资源都被释放了。
在一个 TCP 连接的生命周期内,运行在每台主机中的 TCP 协议在各种 TCP 状态(TCP state)之间变迁。下图说明了客户 TCP 会经历的一系列典型 TCP 状态。
假设客户应用程序决定要关闭该连接。(注意到服务器也能选择关闭该连接。)这引起客户 TCP 发送一个带有 FIN 比特被置为 1 的 TCP 报文段,并进入 FIN_WAIT_1 状态。当处在 FIN_WAIT_1 状态时,客户 TCP 等待一个来自服务器的带有确认的 TCP 报文段。当它收到该报文段时,客户 TCP 进入 FIN_WAIT_2 状态。当处在 FIN_WAIT_2 状态时,客户等待来自服务器的 FIN 比特被置为 1 的另一个报文段;当收到该报文段后,客户 TCP 对服务器的报文段进行确认,并进入 TIME_WAIT 状态。
假定 ACK 丢失,TIME_WAIT 状态使 TCP 客户重传最后的确认报文。在 TIME_WAIT 状态中所消耗的时间是与具体实现有关的,而典型的值是 30 秒、1 分钟或 2 分钟。 经过等待后,连接就正式关闭,客户端所有资源(包括端口号)将被释放。
下图显示了服务器端的 TCP 通常要经历的一系列状态,其中假设客户开始连接拆除。这些状态变迁是自解释的。
这些状态变迁是自解释的。在这两个状态变迁图中,我们只给出了 TCP 连接是如何正常地被建立和拆除的。我们没有描述在某些不正常的情况下(例如当连接的双方同时都要发起或终止一条连接时)发生的事情。如果你对此问题及其他与 TCP 有关的高级问题感兴趣,推荐阅读 Stevens 的内容更全面的书籍。
我们上面的讨论假定了客户和服务器都准备通信,即服务器正在监听客户发送其 SYN 报文段的端口。我们来考虑当一台主机接收到一个 TCP 报文段,其端口号或源 IP 地址与该主机上进行中的套接字都不匹配的情况。
例如,假如一台主机接收了具有目的端口 80 的一个 TCP SYN 分组,但该主机在端口 80 不接受连接(即它不在端口 80 上运行 Web 服务器)。则该主机将向源发送一个特殊重置报文段。该 TCP 报文段将 RST 标志位置为 1。
因此,当主机发送一个重置报文段时,它告诉该源 “我没有那个报文段的套接字。请不要再发送该报文段了”。当一台主机接收一个 UDP 分组,它的目的端口与进行中的 UDP 套接字不匹配,该主机发送一个特殊的 ICMP 数据报。
既然我们已经对 TCP 连接管理有了深入的了解,我们再次回顾 nrnap 端口扫描工具,并更为详细地研究它的工作原理。为了探索目标主机上的一个特定的 TCP 端口,如端口 6789,nmap 将对那台主机的目的端口 6789 发送一个特殊的 TCP SYN 报文段。有 3 种可能的输出:
- 源主机从目标主机接收到一个 TCP SYNACK 报文段。因为这意味着在目标主机上一个应用程序使用 TCP 端口 6789 运行,nmap 返回 “打开”。
- 源主机从目标主机接收到一个 TCP RST 报文段。这意味着该 SYN 报文段到达了目标主机,但目标主机没有运行一个使用 TCP 端口 6789 的应用程序。但攻击者至少知道发向该主机端口 6789 的报文段没有被源和目标主机之间的任何防火墙所阻挡。
- 源什么也没有收到。这很可能表明该 SYN 报文段被中间的防火墙所阻挡,无法到达目标主机。
nmap 是一个功能强大的工具,该工具不仅能 “侦察” 打开的TCP端口,也能 “侦察” 打开的 UDP 端口,还能 “侦察” 防火墙及其配置,甚至能 “侦察” 应用程序的版本
和操作系统。其中的大多数都能通过操作 TCP 连接管理报文段完成。
SYN 洪泛攻击
我们在 TCP 三次握手的讨论中已经看到,服务器为了响应一个收到的 SYN,分配并初始化连接变量和缓存。然后服务器发送一个 SYNACK 进行响应,并等待来自客户的 ACK 报文段。如果某客户不发送 ACK 来完成该三次握手的第三步,最终(通常在一分多钟之后)服务器将终止该半开连接并回收资源。
这种 TCP 连接管理协议为经典的 DoS 攻击即 SYN 洪泛攻击(SYN flood attack)提供了环境。在这种攻击中,攻击者发送大量的 TCP SYN 报文段,而不完成第三次握手的步骤。
随着这种 SYN 报文段纷至沓来,服务器不断为这些半开连接分配资源(但从未使用),导致服务器的连接资源被消耗殆尽。这种 SYN 洪泛攻击是被记载的众多 DoS 攻击中的第一种。
幸运的是,现在有一种有效的防御系统,称为 SYN cookie,它们被部署在大多数主流操作系统中。SYN cookie 以下列方式工作:
当服务器接收到一个 SYN 报文段时,它并不知道该报文段是来自一个合法的用户,还是一个 SYN 洪泛攻击的一部分。因此服务器不会为该报文段生成一个半开连接。相反,服务器生成一个初始 TCP 序列号,该序列号是 SYN 报文段的源和目的 IP 地址与端口号以及仅有该服务器知道的秘密数的一个复杂函数(散列函数)。
这种精心制作的初始序列号被称为 “cookie”。服务器则发送具有这种特殊初始序列号的 SYNACK 分组。重要的是,服务器并不记忆该 cookie 或任何对应于 SYN 的其他状态信息。
如果客户是合法的,则它将返回一个 ACK 报文段。当服务器收到该 ACK,需要验证该 ACK 是与前面发送的某些 SYN 相对应的。如果服务器没有维护有关 SYN 报文段的记忆,这是怎样完成的呢?
正如你可能猜测的那样,它是借助于 cookie 来做到的。前面讲过对于一个合法的 ACK,在确认字段中的值等于在 SYNACK 字段(此时为 cookie 值)中的值加 1。服务器则将使用在 SYNACK 报文段中的源和目的地 IP 地址与端口号(它们与初始的 SYN 中的相同)以及秘密数运行相同的散列函数。如果该函数的结果加 1 与在客户的 SYNACK 中的确认(cookie)值相同的话,服务器认为该 ACK 对应于较早的 SYN 报文段,因此它是合法的。服务器则生成一个具有套接字的全开的连接。
在另一方面,如果客户没有返回一个 ACK 报文段,则初始的 SYN 并没有对服务器产生危害,因为服务器没有为它分配任何资源。