TCP Connection Management

Introduction

TCP是面向链接的(connection-oriented )。就是在双方交换数据之前需要经过建立链接。而UDP是无连接的(没有涉及任何连接的建立和终止的过程)。TCP中的有一些选项只有在连接建立的时候才可用。

TCP connection Establishment and Termination

TCP连接的建立如下图所示:

TCP三次握手

在TCP建立连接称为三次握手(three-way handshake),流程如下:

  1. 首先发送SYN报文的叫作active opener(通常叫作客户端(client)),它发送一个SYN报文,包含着一个初始的序列号ISN(c),ISN-initial sequence number,除此之外还会发送一些额外的选项。
  2. 服务端返回一个SYN报文,以及它的初始序列号-ISN(s)。返回的ACK报文确认号为ISN(c)+1,因为前面说过SYN占据一个字节的数据。
  3. 客户端返回的ACK=ISN(s)+1,序列号为ISN(c)+1。(回想一下,ACK表明期待对方发送的序列号,在服务端返回了ISN(c)+1,所以此时的Seq=ISN(c)+1)。

将第一次发送SYN报文的叫作active open,也就是所谓的客户端。另外一个接收该SYN报文并且发送了下一个SYN报文的叫作passive open.就是所谓的服务端。TCP支持在SYN阶段附带一些数据,但是Beykeley sockets不支持。

TCP supports the capability of carrying application data on SYN segments. This is rarely used, however, because the Berkeley sockets API does not support it.

TCP简述连接叫作四次挥手(虽然在TCP/IP这本书中明说这点,但是大家都这样叫的),如下图所示:

四次挥手,注意最后应该是K+1

PS:注意这图应该有点问题,最后一个报文中的序列号应该是K+1,实验中抓包也是如此

通常来说,双方都可以发起关闭连接的请求,虽然这通常都是由客户端来发起关闭的请求。

Either end can initiate a close operation, and simultaneous closes are also supported but are rare.

关闭TCP连接使用的close()这个系统调用,使得发送一个FIN报文,详细过程如下:

  1. active closer (即,主动关闭的那一方,通常是客户端)发送一个FIN报文,序列号为K,该FIN报文通常也是对上一个数据(上图中序列号L)的一个ACK。
  2. passive closer(即,被动关闭的那一方,通常是服务端)响应一个ACK=K+1。于此同时,服务端发现对方已经关闭连接了,所以这也导致它自己去调用close(),发送FIN报文,关闭连接。
  3. 最后,客户端接收到来自服务端的FIN报文,响应一个ACK。于SYN类似,FIN也占据一个字节,所以如果FIN丢失,会导致对方重传FIN报文。

这是最正常的一个TCP连接建立和终止的过程,但是还有其他方法,来暴力地终止一个TCP连接。

TCP Half-Close

TCP还支持半关闭,就是客户端切断了和服务端的连接,但是服务端并没有且切断和客户端的连接,此时服务端仍然可以往客户端发送数据。在Berkeley sockets API中调用shutdown()来完成半关闭。如下图:

Half-Close

Initial Sequence Number

一个新的连接建立的时候,只要四元组(4-tuple)是合法的并且校验和没有错误,那么就可以接收。所以,这就引入了一个问题,就算该数据在中间链路中逗留了很长的时间,最后导致了数据的乱序,还是会被接收方接收。

在发送SYN报文来建立连接之前,要选择合适的初始序列号(ISN)。ISN应该随着时间的改变而逐渐改变,这样就不会导致不同的连接使用了相同的ISN。[RFC0793] 指定ISN应该是一个32 bit的数,并且每4μs加1。这样做的目的是,使得相同连接(即相同的四元组就可以认为是相同连接)的报文不会具有相同的序列号。

In particular, new sequence numbers must not be allowed to overlap between different instantiations (or incarnations) of the same connection.

设想一个场景,如果一个连接的分组在链路中流转了很久,然后该连接断开了。接着四元组的连接又再一次建立,那么上次那个分组可以被认为是本次连接的中的数据,这显然是不行的。所以上面的方法,来避免每一个连接的序列号都不会相同是十分必要的。

另外一个方面来说,只要知道四元组以及窗口中的有效序列号,那么这些数据都将被认为是有效的。这也反映了TCP的脆弱性:只要掌握了这些信息,其他人都可以伪造TCP报文。所以为了解决这个问题,要么使得ISN变得更加难以捉摸(就如同现在这样,会一直变化),要么就是加密。原文中提到了Linux和Windows的做法,但是我认为稍微有些晦涩,就没记录。

Timeout of connection Establishment

建立连接超时,这里我没有做实验,引用的是书中的例子。

如果直接于一个不存在的IP建立TCP连接,ARP协议会直接报错。因此,首先需要往ARP table中插入一条伪造的记录。如下:

添加ARP记录

00:00:1a:1b:1c:1d不属于子网内任何一台主机,往这里发送数据将无处可去。我们一次来模拟超时的情况。此时,TCP报文的结果如下:

多次重传的报文

可以看到SYN报文多次的重传,没相邻的报文之间的时间间隔是:3,6,12,24,48。这种形式叫作指数回退(exponential backoff)。在Linux中,发送方重传的次数由:net.ipv4.tcp_syn_retries设置,相对应的,服务端的重传次数由net.ipv4.tcp_synack_retries设置。也可以通过socket的TCP_SYNCNT选项来指定。指数回退也是拥塞控制的一部分。

TCP options

最开始的TCP标准中只定义了EOL,NOP,MSS这几个选项,随着TCP的不断发展,常用的选项:

TCP options

每一个选项开始的第一个字节是kind,未被识别的选项就直接舍弃了[RFC1122]。对于kind 0和1,这两个option只占据了一个字节。对于其他选项来说,紧跟着kind之后的1字节表明了选项的长度。NOP的是为了让选项四字节对齐,如果需要的话。EOL表示选项的结尾,接下来就都是数据了,不是TCP报文头的东西了。

Maximum Segment size(MSS)

MSS选项是表明它(接收方)能接收的最大的段的大小,也是发送方所能发送最大的段。根据RFC0879,MSS的值只包括TCP的数据,而不包括任何头部信息。

The MSS value counts only TCP data bytes and does not include the sizes of any associated TCP or IP header.[RFC0879]

在连接建立的时候,通常会在SYN报文段中包含着MSS选项。MSS选项最大可以为16 bit,这是因为上表中说明了MSS最大只能为4字节,kind 和 len 使用了2字节,剩下的内容2字节,所以为16 bit。如果MSS没有指定,那么默认就是536字节,这是TCP最小的报文段。

Recall the rule that requires any host to be capable of processing IPv4 datagrams at least as large as 576.

再减去IP头和TCP头,所以就是536字节。

通常,对于以太网来说,MSS是1460字节,因为以太网的MTU是1500字节(20字节IP头和20字节TCP头)。对于IPV6来说,MSS是1440字节,因为它的IPv6头有40字节。根据RFC2675,IPv6中MSS=65535表示MSS的大小可以是无限大的数据段。

Selective Acknowledgment Options

之前我们说过TCP是累计确认的,只返回已经被正确接收的数据的ACK,所以对于乱序到达的数据,接收方并不会直接返回属于乱序数据的ACK。接收方要阻止应用程序读取乱序的数据,因为TCP对应用程序提供的是一种连续的字节流(byte stream),自然不能给他们返回非连续的数据。

如果对于乱序的数据直接丢失那么肯定是十分低效的,所以如果TCP的发送方可以知道接收方的数据中哪里存在着空洞(hole),那么就可以有选择的重传这些数据,而不是重传所有。TCP使用选择重传来实现这样的功能。

The TCP selective acknowledgment (SACK) options rfc2018 rfc2883 provide this capability.

只有对方具有选择重传的功能的时候才可以使用该功能,在开始建立连接(双方收发SYN报文)的时候,通过SACK-Permitted option来表明自己是SACK功能的。当收到数据是失序的时候,返回的报文中使用了SACK option,其中的内容是,已经被接受的失序数据的序列号的范围,每一个范围(range)都叫作SACK block。比如说:

0-100,110-130,160-200都被正确接收。中间存在着101-109,和131-159的空洞(hole),那么返回SACK中就包含着这两个区间:[101,109],[131,159],这每一个区间叫作SACK block。注意此时返回的报文的ACK还是101,因为ACK是累计确认的。

Thus, a SACK option containing n SACK blocks is (8n + 2) bytes long. Two bytes are used to hold the kind and length of the SACK option

序列号是32 bit的无符号数,所以SACK block的长度一个是8字节,再加上kind 和len。因此,n个SACK blocks就是 8n+2,options的最大长度是40字节, 8n+2 < 40 得到 n <=3(这里假设timestamp也是使用了,timestamp占据了10字节) 。

这里补充一个带有SACK_PERM报文的截图:

WS:window scale

Win: window size

MSS: 最大可以发送的报文大小

SACK_PERM:表明61666端口的TCP支持SACK

Window Scale (WSACLE or WSOPT) option

在报文中,窗口的长度是16bit的,所以,默认的情况下,窗口的最大长度只能为65535 字节。引入Window scale 选项能够在此基础上扩展窗口大小。我暂时不知道$65,535 \times 2^{14}$差不多1GB,如何计算的,这里目前就是直接记住TCP最大可以发送发数据大小是1GB。

Timestamps Option and PAWS

时间戳选项使得让发送方在里面放入此时的时间戳,然后接收方返回这些数据之后,并且在返回的并且放入它的时间戳,让发送方去估计RTT。当使用时间戳选项的时候,发送方将它的32 bit的时间戳放在Timestam Value(叫作TSV或者TSval)放在选项TSOPT的第一部分,然后接收方将这个放到Timestamp Echo Retry(TSER或者TSecr)字段中,然后将报文返回。让sender去估计RTT的时间。

The timestamp is a monotonically increasing value. Because the receiver simply echoes what it receives, it does not care what the timestamp units or values actually are

TSecr的值是发送方之前发过来的时间戳,而TSval是这个报文此时发送的时间戳。所以TSecr总是比TSval要小。如下图:

TCP timestamp options

PS:在最开始建立连接的时候,发送方在TSval中放入它自己的时间戳,在TSecr设置为0,因为它还不知道服务器的时间戳。

估计RTT可以让TCP更好的来设置超时定时器,基于得到的RTT,再基于此应用一些算法,来设置合适的超时定时器。

解决回环的问题:

因为32 bit的序列号最多只能表示到4GB,超过就会重新回到0。假设场景:每次发送1 GB的数据,每次发送一个TCP报文的时间戳加1。

序列号回环问题

我们假设在Time B一个报文丢失了,接着重传且被正确的接受了。接着我们假设这个丢失的报文在time F重新出现了,而在Time F它的序列号因为回环而恰好也是1G:2G,那么这个晚到来的Time B的报文会被误认为是正确的(因为它的四元组是一样的),即使有累计确认也不行,因为Time B 的数据被接受后,返回的ACK 也是 1G。这显然是不行的,因为最好的做法肯定是直接丢弃。所以使用时间戳选项就避免了这个问题,因为晚到的报文的时间戳为2,远小于最近接收到的时间戳,所以直接丢弃即可。PAWS(Protection against Wrapped Sequence Numbers)并不需要保证发送方和接收方之间的时钟是同步的,只需要保证是时钟是单调递增的即可。

TCP state Transitions

到目前为止,讨论了很多和TCP连接的建立相关的内容,如报文中的选项(User timeout 和安全相关的内容没有涉及),TCP连接的建立和断开。现在讨论的是TCP的状态变换,状态图如下:

TCP状态转变图

图中的11个状态(closed,listen,syn_sent…)都是netstat程序中的所使用的,尽管closed并不是一个“offical”的装状态。从SYN_RCVD状态回到LISTEN状态只有在:进入SYN_RCVD状态是从LISTEN而来才可以。

TIME_WAIT(2MSL Wait) State

TIME_WAIT状态又叫作2MSL 等待状态,在该状态下TCP需要等待2个MSL时间。任何一个TCP实现都需要为MSL设置一个值。MSL就是一个报文在链路中能够存在最大的时间(在它被丢弃之前) 。

It(MSL) is the maximum amount of time any segment can exist in the network before being discarded.

我们知道这是有限的,因为IP报文中的TTL(time to live)字段规定一个IP报文可以在链路中的存活时间。[RFC0793]制定了MSL为2分钟,然而通常的时间是30秒,1分钟,或者2分钟。在Linux中,通过net.ipv4.tcp_fin_timeout来设置 2 MSL。

在实际中,发送方关闭连接然后接收来自接收方的FIN,最后返回ACK,接着TCP连接在TIME_WAIT状态等待2MSL时间。最后一个ACK会重发不是因为重传(因为这个ACK不带有任何数据,所以不会TCP不会重传),而是因为FIN(来自接收方)的重传,它(发送方)会重传这个ACK。在接收方中,会一直重传FIN,直到接受了ACK。

2MSL的另外一个影响就是,当TCP等待TIME_WAIT结束的时候,定义一个连接的四元组并不能被使用,只有2MSL过后,这个四元组才可以被重新使用。或者,根据新建立的连接的初始化序列号超过上一个连接中被使用过的最大序列号。

when a new connection uses an ISN that exceeds the highest sequence number used on the previous instantiation of the connection [RFC1122]

或者使用了时间戳选项(timestamp option)来区分前一个相同四元组的连接。

if the use of the Timestamps option allows the disambiguation of segments from a previous connection instantiation to not otherwise be confused [RFC6191]

但是,有些TCP实现有更严格的要求,如果端口在2MSL等待,那么该端口就无法被使用,稍后会有举例。

大多数的TCP实现都提供了越过这个限制的方法,Berkeley sockets API提供了SO_REUSEADDR选项来实现这一点。它使得一个即使在2MSL等待的端口也被使用。

It lets the caller assign itself a local port number even if that port number is part of some connection in the 2MSL wait state

虽然,socket提供了这种机制来绕开这一点,但是TCP的规则仍然会阻碍在2MSL等待中的端口的使用。

the rules of TCP still (should) prevent this port number from being reused by another instantiation of the same connection that is in the 2MSL wait state.

对于在2 MSL阶段的socket所接收到的报文段,直接丢弃。因为一个连接是由 四元组来区分的,在 2 MSL内 无法重新再使用这个四元组,当这个四元组被重新使用的时候,属于之前一个连接的晚到达的数据就会被误解为是这个新建立的连接的数据。所以,使用时间戳来鉴别应该可以避免这个问题

对于交互式应用程序来说,服务端通常都是passive close的,而且不会进入到2 MSL等待这个阶段。如果客户端快速重启在建立连接,并不会有任何问题,因为操作系统给客户端程序分配的端口都是随机的且临时的。如果程序如果使用了很多很多端口,那么在去申请端口的时候可能会等待一会,等待其他连接将端口释放了。

然而,另外一种情况,服务端先调用close(),此时它的角色就是active close,所以,如果我们马上就关闭服务端程序,在马上重启,就会因为该端口在 2MSL等待状态而无法使用(服务端的程序所使用的都是周知端口号居多,如80),会出现“端口已经被占用”的错误。从而导致该程序去等待一段时间,等待该端口度过TIME WAIT状态。

首先演示服务端的TIME_WAIT情况,可以看到结束服务端结束运行之后,程序并不能直接重启,端口被占用:

2 MSL导致端口被占用

原文中还举例了2 MSL发生在客户端的情况,但是我认为不是那么的重要,因为在现实开发中,客户端的端口号通常都是由操作系统指定的。

TIME_WAIT

稍微总结下TIME_WAIT等待2MSL的作用,如下:

  1. 当TCP连接关闭以后,让active open等待2MSL可以使得,迟到的报文被直接丢弃。假设,一个TCP连接不等待2MSL时间,那么当该四元组立即被重新使用,迟到的报文可能会被认为是新建立的连接的报文。
  2. 另外一个作用是,客户端接收到FIN后返回给服务端的ACK可能会丢失,所以会使得对方(服务端)重新再传一个FIN过来,让客户端再发送一个ACK过去。客户端接收到FIN之后就进入到了TIME_WAIT状态,就可以避免了ACK丢失的情况

Quiet Time Concept

如果一个连接处于2MSL等待状态,在此时接收到的,与此时相同四元组的,先前的连接的连接的数据,不会被接收,因为在TIME_WAIT状态内所接受的数据都是直接丢弃的,但是这只能发生在TIME_WAIT状态机器没有crash的状态。

如果在TIME_WAIT的机器关机了然后直接重启,然后又使用先前使用过的四元组来建立连接。那么迟到的报文就可能被解释为是当前新建立的连接的数据。

This can happen regardless of how the initial sequence number is chosen after the reboot.

PS:我不明白为什么序列号不能避免这个情况。

为了避免这个情况,[RFC0793] 说TCP在重启之后建立任何TCP连接之前应该等待一个MSL。这个时间叫作quiet time(静默时间)。有一些实现遵守了这一点但是大部分在重启之后都等待超过一个MSL的时间。

FIN_WAIT_2 State

当发送方调用了close()系统调用,发送了FIN报文,并且也接收到了对方(服务器)的ACK,发送方就进入到了FIN_WAIT_2状态。除非使用了半关闭,否则的话应该等待对方发送它的FIN报文过来,只有当对方的FIN发过来了,并且接收方返回了ACK,才会从FIN_WAIT_2 转到TIME_WAIT状态。这里就意味着,除非半关闭状态,如果服务器一直不发送一条FIN报文,那么接收方会一直卡在FIN_WAIT_2状态

所以,为了避免这个情况,很多TCP连接都会这样做:如果不是半关闭,那么在进入到FIN_WAIT_2状态就设置一个定时器,如果在定时器超时后,TCP连接还是空闲的,那么将TCP状态切换到CLOSED状态,在Linux中net.ipv4.tcp_fin_timeout设置为60s。

Reset Segments

一条报文中RST=1,那么该报文就成为重置报文(Reset Segment)。当所到达的报文对于该连接的四元组来说是不正确的时候,就会发发送RST报文。下面介绍了几种RST 报文报文情况。

Connection Request to Nonexistent port

原文中所截图的访问不存在端口的TCP报文不怎么好懂,所以我自己用Wireshark抓包弄了一个,如下图:

port unreachable

第一个灰色的报文,是客户端发送的SYN报文。第二个报文(红色)是来自服务端的RST报文。因为在SYN报文中ACK没有被设置(ACK = 0),所以在返回(服务端返回到客户端)的RST报文中Seq设置为0,ACK设置为 = SYN报文的Seq+1,因为SYN占据一个序列号。

Although there is no data in the arriving segment, the SYN bit logically occupies 1 byte of sequence number space; therefore, in this example the ACK number in the reset segment is set to the ISN, plus the data length (0), plus 1 for the SYN bit

返回的ACK必须要在有效窗口之内,这能阻止一种简单的攻击,原文中对于对这个描述的并不详细,所以就这样吧。

Aborting a Connection

之前我们所看到的正常的结束TCP连接是发送FIN。另外一种方法就是发送RST报文来终止一个连接,这种叫做abortive release。

But it is also possible to abort a connection by sending a reset instead of a FIN at any time

abort能够为应用程序两个特性:

  1. 任何在队列中的数据都被(这里的队列表示的不是特别明确,我认为可能是接收窗口中乱序的数据)丢弃,然后返回RST报文
  2. RST报文的接收方可以知道这不是一个normal close

下面我也使用 wireshark抓包了一段abort的RST报文(直接退出客户端):

abort RST

最开始三个报文是握手阶段的报文,都表明他们双方都支持SACK。当客户端abort(最后一条红色的报文)之后,它发送了一条RST报文给服务器。RST报文不会使得对方返回一个ACK。RST报文的接收者会终止连接并且通知应用程序连接被reset,通常会引发一个“Connection reset by peer“或者与之相类似的信息。

Also notice that the reset segment elicits no response from the other end—it is not acknowledged at all.

我有个问题是,既然RST报文不要求ACK,那么RST报文丢失怎么办?原文中接下来的内容说明了这点,就是半开的连接。

Half-Open Connection

如果一个连接单方面结束,或者abort没有得到对方确认(即abort发送的RST报文丢失,使得对方没有结束连接),这样的连接就是半开连接。

A TCP connection is said to be half-open if one end has closed or aborted the connection without the knowledge of the other end.

这种情况在peer crashed时候就会出现,只要双方之间没有通过half-open的连接交换数据,另外一方无法知晓另外一端已经crashed。

另外一种half-open 的情况就是机器直接断电,如果在断电期间没有数据交互,那么就会使得该连接变成半开连接。(可以使用keep alive TCP选项来发现半开连接的情况。)

这个不好实验,所以引用下原文中的例子,原文中通过端口111连接到服务器,然后拔掉服务器的网线模拟半开连接。在服务器重启之后,它对于之前的半开连接一无所知,所以当客户端发送信息到服务器的时候,相当于发送一个不可用的端口(unreachable port,如同connection request to nonexistent port)的情况,因此使得服务器返回一条RST报文。如下:

半开连接导致的RST报文

然而,如果客户端直接断开连接以后,未曾通知服务器。那么服务器这边就出现了半开连接(half-open),再也不发送数据过来,然后服务器也不会主动发数据给他。那么这个半开连接就一直都在了,避免这个问题,可以使用TCP keep alive 来解决。后面会说到。

TCP Server Operation

这一节没有完全看懂。对我个人而言,就是解决了之前一个误区,虽然之前ssh已经用的很多,不过我潜意识里还是认为一个端口号最多同时只能建立一个TCP连接。然而事实上,并不是如此,多个ssh连接到都是22端口。其他几节内容感觉比较次要。我选择跳过。

Attacks involving TCP connection management

原文中attacks involving TCP connection management,没有仔细阅读。草草知道了,一种攻击叫作SYN flood(SYN泛洪),攻击者随意的伪造有些不存在的IP地址,所以使得服务器为连接分配了资源,但是连接从未被建立。可以使用一种SYN cookies的技术可以避免这个情况。

暂无评论

发送评论 编辑评论

|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇