TCP协议详解之TCP Flag标志位来判断TCP会话的开始和结束

本文深入解析TCP协议中的标志位,包括SYN、ACK、FIN、RST等的用途及组合规则,阐述三次握手和四次挥手的过程,以及TCP连接状态转移机制。

首先回顾一下TCP标志位的具体含义。

TCP Flag标志位(控制位)

一个TCP包的详细内容:

TCP FLAG 标记占1.5个byte,12bit(4bit+8bit,前半个byte与Header Length公用)。

12bit中前三个bit是保留,默认为全0:

000. .... .... = Reserved: Not set

第4个bit为:

...0 .... .... = Nonce: Not set

第5个bit为:

.... 0... .... = Congestion Window Reduced (CWR): Not set

第6个bit为:

.... .0.. .... = ECN-Echo: Not set

从第7个bit到第12个bit之间的六个bit是我们常说的6个标志位分别是:

.... ..0. .... = Urgent: Not set

.... ...1 .... = Acknowledgment: Set

.... .... 0... = Push: Not set

.... .... .0.. = Reset: Not set

.... .... ..0. = Syn: Not set

.... .... ...1 = Fin: Set

TCP标记的具体意义如下所列:

W : CWR(Congestion Window Reduced) - 拥塞窗口减少。CWR标志与后面的ECE标志都用于IP首部的ECN字段。ECE标志为1时,则通知对方已将拥塞窗口缩小。

E : ECE(ECN-Echo) - 显式拥塞提醒回应。置为1会通知通信对方,从对方到这边的网络有拥塞。在收到数据包的IP首部中ECN为1时,将TCP首部中的ECE设置为1.

U : URG - 紧急指针,表明发送端向另一端使用紧急方式发送数据。 包中有需要紧急处理的数据。

A : ACK - 应答响应,表示确认序号有效。确认应答的字段有效。TCP规定除了最初建立连接时的SYN包之外该位必须设置为1。

P : PUSH - 推送; 数据包立即发送 。表示接收方应该尽快将这个报文交给应用层。表示需要将收到的数据立刻传给上层应用协议。PSH为0,也就是普通情况下,则不需要立即传,而是先进行缓存。

R : RST - 复位;中断一个连接,表示连接重置。​​​​​​​ 表示TCP连接中出现异常必须强制断开连接。例如,一个没有被使用的端口即使发来了连接请求,也无法通信。此时就可以返回一个RST设置为1的包。此外,程序宕掉或切断电源等原因导致主机重启的情况下,由于所有的连接信息将全部被初始化,所以原有的TCP通信也将不能继续进行。这种情况下,如果通信对方发送了一个设置为1的RST包,就会使通信强制被断开。

S : SYN - 同步; 表示开始会话请求,用来发起一个连接,建立连接。SYN为1表示希望建立连接,并在其序列号的字段进行序列号初始值的设定。(Synchronize本身有同步的意思。也就是意味着建立连接的双方,序列号和确认应答号要保持同步) 

F : FIN - 结束; 结束会话,关闭连接。表示发送方完成任务,今后不会有数据发送,希望断开连接。当通信结束希望断开连接,通信双方的主机之间就可以相互交换FIN位置为1的TCP段。每个主机又对对方的FIN包进行确认应答以后就可以断开连接。不过,主机收到FIN设置为1的TCP段以后不必马上回复一个FIN包,而是可以等到缓冲区中的所有数据都已成功发送而被自动删除之后再发FIN包
因此,02表明该数据包发送的是一个同步序号,用来发起一个新的连接。Flags: 0x002 (SYN)

注意:URG和PUSH的区别是:

当URG = 1时表明紧急指针字段有效,告诉系统此报文段中有紧急数据,应尽快传送,而不要按原来的排队顺序来传送(进入缓存,等缓存区满再提交给应用),发送方的TCP就把紧急数据放到本报文段数据的最前面(因为紧急指针从头开始算)。URG标志位要与首部中的紧急指针字段配合使用,紧急指针指向数据段中的某个字节,(数据从第一个字节到指针所指的字节就是紧急数据,若URG为0,则紧急指针没有意义、)。值得注意的是即使窗口为0时也可以发送紧急数据,紧急数据不进入接收缓冲区直接交给上层进程(本来在缓冲区中的数据无影响)。

当发送端将PSH置为1时,TCP会立即创建一个报文并发送。接受端收到PSH为1的报文后,首先将发送过来的PSH报文的数据加入到接收缓冲区,然后立即将接收缓冲区内数据向上交付给应用程序,而不是等待缓冲区满后再交付。当两个应用进程进行交互式通信时,有时客户发一个请求给服务器时希望立即能够收到对方的响应,这种情况下,客户应用程序通知TCP使用推送(push)操作,TCP就把PSH置为1,并立即创建一个报文段发送过去;服务器端收到一个设了PSH标志的报文段时就尽快将所有收到的数据立即提交给服务进程,而不在等到整个缓存都填满了再向上交付。
虽然应用进程可以选择推送操作,但推送操作还是很少使用。两者都可理解为处理紧急数据的标志位,只是处理方法不同。URG的紧急数据仅在报文内,而PSH的紧急数据还在接受缓冲区内。也就是URG不进入缓存。

注意:

其他文献和博客中关于TCP报文中Flag位看到了几种不同的版本:
第一种:保留6位,Flag位分别是URG,ACK,PSH,RST,SYN,FIN
第二种:保留4位,Flag位分别是CWR,ECE,URG,ACK,PSH,RST,SYN,FIN(图解TCP/IP)
第三种:保留3位,Flag位分别是Nonce,CWR,ECE,URG,ACK,PSH,RST,SYN,FIN(Wireshark)

不管怎么样后六位一定是标志位,而且是最重要的。

CWR,ECE是用于传输过程中的拥塞控制的,与tcp的窗口协同工作,不过好像极少用的到。

不合法的标志位搭配

不合法的标志位组合。

1、所有标志位都为0。

2、SYN和FIN同时被置1。

3、SYN和RST同时被置1。

4、FIN和RST同时被置1。

5、FIN位被置1,但ACK位没有被置1。

6、PSH位被置1,但ACK位没有被置1。

7、URG位被置1,但ACK位没有被置1。

有几个字段与flags有关,在此专门介绍一下:

序列号(Sequence Number)

字段长32位。序列号(也称为序号)是指发送数据的位置。每发送一次数据,就累加一次该数据字节数的大小。序列号不会从0或1开始(因为防止被猜解,重放攻击),而是在建立连接时由计算机生成的随机数作为其初始值(不同操作系统产生随机数的机制不太一样),通过SYN包传给接收端主机。然后再将每转发出去的字节数累加到初始值上表示数据的位置。此外,在建立连接和断开连接时,发送的SYN包和FIN包虽然并不携带数据,但是也作为一个字节增加对应的序列号。

确认应答号(Acknowledgement Number)

确认应答号字段长度32位。指下一次应该收到的数据的序列号。实际上指当前已收到确认应答号减一为止的数据。发送端收到这个确认应答以后可以认为在这个序号以前的数据都已经被正常接收。

紧急指针

该字段长为16位。只有在URG控制位为1时有效。该字段的数值表示本报文段中紧急数据的指针。也就是,从数据部分的首位到紧急指针所指的位置为止为紧急数据。因此也可以说紧急指针指出了紧急数据的末尾在报文段中的位置。一般在暂时中断通信的情况下使用。例如在Web浏览器点击停止按钮,或者使用TELNET时输入Ctrl+C时都会有URG为1的包。此外,紧急指针也可用作表示数据流分段的标志。


下面对每一个标志位进行详解,如果你了解了,就不需要看了。

第一个标志位 - 紧急指针(URGENT POINTER)

第一个标志是紧急指针标志,此标志用于将输入数据标识为“紧急”。这样的进入段不必等待直到先前段被接收端消耗,而是直接发送并立即处理

在数据传输流中,主机正在向远程机器上运行的应用程序发送数据,可以使用紧急指针。如果出现问题,主机需要中止数据传输,并在另一端停止数据处理。在正常情况下,中止信号将在远程机器发送和排队,直到所有先前发送的数据都被处理,但是在这种情况下,我们需要立即处理中止信号。

通过将中止信号的段紧急指针标志设置为“1”,远程机器将不会等待所有排队的数据被处理,然后执行中止。相反,它会给出特定的段优先级,立即处理它,并停止进一步的数据处理。

另外要注意的是存在紧急指针字段。当紧急指针标志设置为'1'时,紧急指针字段指定紧急数据结束的段中的位置,无效时默认为0。

第二个标志位 - 确认(ACKNOWLEDGEMENT)

确认标志用于确认数据包的成功接收,也用于握手和挥手的确认

如果在使用TCP传输数据的同时运行数据包嗅探器,您会注意到,在大多数情况下,对于发送或接收的每个数据包,都会进行确认。在发送方每发送一个数据包需要一个确认,接收端将发送一次预期的ACK(接收到第三个连续的数据包)。这也称为窗口化。

TCP连接是可靠的,其可靠性的表现就是即使在传输过程中丢包,TCP协议也能自动重传丢失的包,并保证所有数据包的顺序。重传其实就依赖于ACK。其实在每次发送数据包后,发送端会监听等待接收端发送一个ACK来表示其已经收到了数据,如果发送端在等待某个特定时间后(也就是重传超时)仍然没有收到接收端的消息,则发送端就开始重新传送那个数据包。所以,ACK并不单纯存在于握手中,还存在于整个数据传输中。

千万不要被wireshark骗了,ACK并不是只在建立连接和结束连接时候使用。其实每个tcp请求除了首次建立连接,都是有应答ACK的,不过wireshark有些没有显示ACK标志而已。filter输入tcp.flags.ack==1,会发现所有的序号都是连续的。(wireshark太智能会造成一些误会,比如它只会认为请求头和响应头是HTTP,中间的数据包是tcp,那是因为从不同的层去看,而且你如果是过滤http,中间的tcp数据包不会显示,但是如果保存则会显示中间传输的tcp包)。

第三个标志 - PUSH

存在推送标志,以确保数据被给予优先级,并在发送或接收端处理。这个特定的标志在数据传输的开始和结束时被非常频繁地使用,影响数据在两端处理的方式。

当开发人员创建新的应用程序时,他们必须确保它们遵循RFC的特定指导,以确保其应用程序正常工作,并且无缺陷地管理进出OSI模型的应用程序层的数据流。使用时,推送位确保数据段被正确处理,并在虚拟连接的两端给出适当的优先级。

当主机发送其数据时,它将暂时排队在TCP缓冲区中,即存储器中的一个特殊区域,直到该段达到一定大小,然后发送到接收器。这种设计保证数据传输尽可能高效,而不需要通过创建多个片段而不需要花费时间和带宽,而是将它们组合成一个或多个较大的片段。

当段到达接收端时,它被传递到应用层之前被放置在TCP接收缓冲器中。在进入缓冲区中排队的数据将保留在那里,直到缓冲区满,并且一旦完成,数据就被传递到等待它的应用层。

虽然这个过程在大多数情况下运行良好,但是有很多情况下这种“排队”的数据是不期望的,因为排队期间的任何延迟都可能导致等待应用程序出现问题。一个简单的例子将是一个TCP流,例如真正的播放器,其中数据必须立即发送和处理(由接收器),以确保平滑的流,没有任何切断。

这里提到的最后一点是,Push标志通常设置在文件的最后一段,以防止缓冲区死锁,一直等待数据。当用于通过代理发送HTTP或其他类型的请求时也可以看到,确保请求得到适当和有效的处理。

第四个标志位 - 复位(RST)标志

段到达不用于当前连接时,使用复位标志。换句话说,如果要向主机发送数据包以建立连接,并且没有这样的服务等待在远程主机上回答,则主机将自动拒绝您的请求,然后向您发送回复RST标志置1。这表示远程主机已重置连接

虽然这可能是非常简单和合乎逻辑的,但事实是在大多数情况下,这个“特征”被大多数黑客使用,以便扫描“打开”端口的主机。所有现代端口扫描仪都能够通过“复位”功能检测“打开”或“收听”端口。

用于检测这些端口的方法非常简单:当尝试扫描远程主机时,将使用SYN标志位(1)构建有效的TCP段,并将其发送到目标主机。如果没有服务监听特定端口上的入站连接,则远程主机将回复ACK和RST标志设置(1)。另一方面,如果端口上有服务监听,则远程主机将构造一个具有ACK标志位的TCP段(1)。这是标准三次握手的一部分。

一旦主机扫描开放端口接收到该段,它将完成三次握手,然后使用FIN(见下文)标志终止它,并将特定端口标记为“活动”。

第五个标志位 - 同步标志(SYNCHRONISATION FLAG)

TCP标志选项中包含的第五个标志可能是TCP通信中使用最知名的标志。在建立两台主机之间的经典3次握手时,SYN标志的作用是初始化TCP连接的。

 

在上图中,主机A需要使用TCP作为其传输协议从主机B下载数据。TCP协议需要进行三次握手,以便双方建立虚拟连接才能交换数据。在三次握手期间,我们可以统计每个主机发送的SYN标志总数为2个。随着文件的交换和新连接的创建,我们将看到更多的SYN标志被发送和接收。

在发送SYN数据包时,并且创建一个随机的序列号

第六个标志位 - FIN 标志

可用的最后一个标志是FIN标志,代表“完成”一词。该标志用于拆除使用上一个标志(SYN)创建的虚拟连接,因此,由于这个原因,当连接之间交换最后一个数据包时,总是出现FIN标志。

重要的是要注意,当主机发送FIN标志来关闭连接时,它可能会继续接收数据,直到远程主机也关闭了连接,尽管这仅在特定情况下发生。一旦连接被双方削减,则释放用于连接的每端的缓冲区。

正常的拆卸过程如下所示:

 

上图显示了主机A和B之间的现有连接,两个主机正在交换数据。一旦数据传输完成,主机A发送一个包含FIN,ACK标志的数据包(步骤1)。使用此数据包,主机A确认先前的流,同时启动TCP关闭过程以终止此连接。此时,Host A的应用程序将停止接收任何数据,并将关闭此端的连接。为了响应主机A关闭连接的请求,主机B将发送确认(STEP 2),并通知其应用程序连接不再可用。一旦完成,主机(B)将发送自己的FIN,ACK标志(步骤3)以关闭其部分连接。

如果您想知道为什么需要执行此过程,则可能需要记住,TCP是一个全双工连接,这意味着有两个方向的数据流。在我们的示例中,这是从主机A到主机B的连接流,反之亦然。另外,它需要两个主机关闭它们的连接,因此两个主机必须发送一个FIN标志并且另一个主机必须确认它的原因。

最后,在步骤4,主机A将确认在步骤3发送的主机B的请求,并且双方的已关闭程序现在已经完成。

然后详细回顾一下TCP三次握手和四次挥手,此过程涉及到标志位的变化。

TCP三次握手

三次握手Three-way Handshake 

一个虚拟连接的建立是通过三次握手来实现的 

1. (A) --> SYN --> (B) 

2. (A) <--SYN/ACK <-- (B) 

3. (A) --> ACK --> (B) 

第一次握手:主机A发送位码为SYN=1,随机产生seq number=1234567(随机数)的数据包到服务器,主机B由SYN=1知道,A要求建立联机;

一个 SYN包就是仅SYN标记设为1的TCP包

第二次握手:主机B收到请求后要确认联机信息,向A发送ack number=(主机A的seq number+1),SYN=1,ACK=1,随机产生seq number=7654321(随机数)的包;

SYN/ACK包是仅SYN 和 ACK 标记为1的包. 

第三次握手:主机A收到后检查ack number是否正确,即第一次发送的seq number+1,以及位码ACK是否为1,若正确,主机A会再发送ack number=(主机B的seq number+1),ACK=1,主机B收到后确认seq值正确,并且ACK=1则连接建立成功。

ACK包就是仅ACK 标记设为1的TCP包

完成三次握手,主机A与主机B开始传送数据。并且每一个数据包中都带有ACK标志位。

这就是为何连接状态跟踪很重要的原因了。 没有连接跟踪,防火墙将无法判断收到的ACK包是否属于一个已经建立的连接.一般的包过滤(Ipchains)收到ACK包时,会让它通过(这绝对不是个 好主意). 而当状态型防火墙收到此种包时,它会先在连接表中查找是否属于哪个已建连接,否则丢弃该包。 

未连接队列

在三次握手协议中,服务器维护一个未连接队列,该队列为每个客户端的SYN包(syn=j)开设一个条目,该条目表明服务器已收到SYN包,并向客户发出确认,正在等待客户的确认包时,删除该条目,服务器进入ESTAB_LISHED状态。常用于产生SYN flooding攻击,所以缩短SYN超时时间,减小SYN队列数是抵抗SYN dos的基本手段。

TCP四次挥手

四次握手Four-way Handshake 

四次握手用来关闭已建立的TCP连接 

1. (B) --> ACK/FIN --> (A) 

2. (B) <-- ACK <-- (A) 

3. (B) <-- ACK/FIN <-- (A) 

4. (B) --> ACK --> (A) 

四次挥手过程说明: 
1、客户端发送断开TCP连接请求的报文,其中报文中包含seq序列号,并且还将报文中的FIN字段置为1,表示需要断开TCP连接。(FIN=1,seq=x,x序列号为客户端发送的上一个数据包中的确认号值。ACK=1, ack确认号为服务器发送的上一个数据包中的序列号+该数据包所带的数据的大小

2、服务端会回复客户端发送的TCP断开请求报文,其包含seq序列号,而且会产生ACK字段,ACK字段数值是在客户端发过来的seq序列号基础上加1进行回复,以便客户端收到信息时,知晓自己的TCP断开请求已经得到验证。(ACK=1,ack=x+1,seq=第1步中的确认号值)

3、服务端在回复完客户端的TCP断开请求后,不会马上进行TCP连接的断开,服务端会先确保断开前,所有传输到A的数据是否已经传输完毕,一旦确认传输数据完毕,就会将回复报文的FIN字段置1,ACK为1。并且产生seq序列号。(FIN=1,ACK=1,ack确认号为客户端发送的上一个数据包中的序列号+该数据包所带数据的大小,seq=z,z序列号为服务器发送的上一个数据包中的确认号值

4、客户端收到服务端的TCP断开请求后,会回复服务端的断开请求,包含seq字段和ack字段,ack字段会在服务端的TCP断开请求的seq基础上加1,第3步数据包中的序列号+1,从而完成服务端请求的验证回复。(FIN=1,ACK=z+1,seq=h,h为第3步中的确认号值) 
至此TCP断开的4次挥手过程完毕。

注意: 由于TCP连接是双向连接, 因此关闭连接需要在两个方向上做。ACK/FIN 包(ACK 和FIN 标记设为1)通常被认为是FIN(终结)包。然而,由于连接还没有关闭, FIN包总是打上ACK标记。没有ACK标记而仅有FIN标记的包不是合法的包,并且通常被认为是恶意的。 

注意 : 因为FIN和SYN一样,也要占一个序号。理论上服务器在TCP连接关闭时发送的终止数据包中,只有终止位是置1,然后客户端进行确认。但是在实际的 TCP实现中,在终止数据包中,确认位和终止位是同时置为1的,确认位置为1表示对最后一次传输的数据进行确认,终止位置为1表示关闭该方向的TCP连 接。

TCP状态转移

è¿éåå¾çæè¿°

建立连接

1、一开始,建立连接之前服务器和客户端的状态都为CLOSED; 
2、服务器创建socket后开始监听,变为LISTEN状态; 
3、客户端请求建立连接,向服务器发送SYN报文,客户端的状态变味SYN_SENT; 
4、服务器收到客户端的报文后向客户端发送ACK和SYN报文,此时服务器的状态变为SYN_RCVD; 
5、然后,客户端收到ACK、SYN,就向服务器发送ACK,客户端状态变为ESTABLISHED; 
6、服务器端收到客户端的ACK后变为ESTABLISHED。此时3次握手完成,连接建立!

由于TCP连接是全双工的,断开连接会比建立连接麻烦一点点。 
1、客户端先向服务器发送FIN报文,请求断开连接,其状态变为FIN_WAIT1; 
2、服务器收到FIN后向客户端发送ACK,服务器的状态围边CLOSE_WAIT; 
3、客户端收到ACK后就进入FIN_WAIT2状态,此时连接已经断开了一半了。如果服务器还有数据要发送给客户端,就会继续发送; 
4、直到发完数据,就会发送FIN报文,此时服务器进入LAST_ACK状态; 
5、客户端收到服务器的FIN后,马上发送ACK给服务器,此时客户端进入TIME_WAIT状态; 
6、再过了2MSL长的时间后进入CLOSED状态。服务器收到客户端的ACK就进入CLOSED状态。 
至此,还有一个状态没有出来:CLOSING状态。 
CLOSING状态表示: 
客户端发送了FIN,但是没有收到服务器的ACK,却收到了服务器的FIN,这种情况发生在服务器发送的ACK丢包的时候,因为网络传输有时会有意外。

状态详解:

LISTEN:等待从任何远端TCP 和端口的连接请求。

SYN_SENT:发送完一个连接请求后等待一个匹配的连接请求。

SYN_RECEIVED:发送连接请求并且接收到匹配的连接请求以后等待连接请求确认。

ESTABLISHED:表示一个打开的连接,接收到的数据可以被投递给用户。连接的数据传输阶段的正常状态。

FIN_WAIT_1:等待远端TCP 的连接终止请求,或者等待之前发送的连接终止请求的确认。

FIN_WAIT_2:等待远端TCP 的连接终止请求。

CLOSE_WAIT:等待本地用户的连接终止请求。

CLOSING:等待远端TCP 的连接终止请求确认。

LAST_ACK:等待先前发送给远端TCP 的连接终止请求的确认(包括它字节的连接终止请求的确认)

TIME_WAIT:等待足够的时间过去以确保远端TCP 接收到它的连接终止请求的确认。
TIME_WAIT 两个存在的理由:
          1.可靠的实现tcp全双工连接的终止;
          2.允许老的重复分节在网络中消逝。

CLOSED:不在连接状态(这是为方便描述假想的状态,实际不存在)

【问题1】为什么连接的时候是三次握手,关闭的时候却是四次握手?
答:因为当Server端收到Client端的SYN连接请求报文后,可以直接发送SYN+ACK报文。其中ACK报文是用来应答的,SYN报文是用来同步的。但是关闭连接时,当Server端收到FIN报文时,很可能并不会立即关闭SOCKET,所以只能先回复一个ACK报文,告诉Client端,"你发的FIN报文我收到了"。只有等到我Server端所有的报文都发送完了,我才能发送FIN报文,因此不能一起发送。故需要四步握手。

【问题2】为什么TIME_WAIT状态需要经过2MSL(最大报文段生存时间)才能返回到CLOSE状态?

答:虽然按道理,四个报文都发送完毕,我们可以直接进入CLOSE状态了,但是我们必须认为网络是不可靠的,有可以最后一个ACK丢失。所以TIME_WAIT状态就是用来重发可能丢失的ACK报文。

TCP会话开始和结束

(1)如上三次挥手和四次握手所示,开始都是以SYN开始,结束以FIN结束。

(2)发生异常情况下都是以RST结束。

在TCP协议中,RST标识复位,用来异常的关闭链接。在TCP的设计中它是不可或缺的,发送RST包关闭链接时,不必等缓冲区的数据都发送出去。直接丢弃缓冲区中的数据,发送RST包。而接受段收到RST包后,也不必发送ACK包来确认。

1. 请求的目标端口未打开,服务端发送RST包(用于端口扫描)。

2. 请求超时。

3. Socket内核接收缓冲区Recv-Q中的数据未完全被应用程序读取,而关闭该Socket。

4. 向已关闭的Socket中发送数据。(send与close调用)如果调用close()方法,关闭的Socket代表的链接依然处于FIN_WAIT2状态,则正常返回ACK确认包。如果状态FIN_WAIT2的超时,则close()调用后【向FIN_WAIT2超时状态的连接发送FIN】,依然会返回RST包。

连接复位 Resetting a connection 

四次握手不是关闭TCP连接的唯一方法。有时,如果主机需要尽快关闭连接(或连接超时,端口或主机不可达),RST (Reset)包将被发送。注意与FIN不同,由于RST包不是TCP连接中的必须部分, 可以只发送RST包(即不带ACK标记)。 但在正常的TCP连接中RST包也可以带ACK确认标记 。请注意RST包是可以不要收到方确认的。

无效的TCP标记 Invalid TCP Flags 

(1)ACK是可能与SYN,FIN等同时使用的,比如SYN和ACK可能同时为1,它表示的就是建立连接之后的响应,如果只是单个的一个SYN,它表示的只是建立连接。TCP的几次握手就是通过这样的ACK表现出来的。但SYN与FIN是不会同时为1的,因为前者表示的是建立连接,而后者表示的是断开连接。

(2)RST一般是在FIN之后才会出现为1的情况,表示的是连接重置。一般地,当出现FIN包或RST包时,我们便认为客户端与服务器端断开了连接;而当出现SYN和SYN+ACK包时,我们认为客户端与服务器建立了一个连接

(3)PSH为1的情况,一般只出现在 DATA内容不为0的包中,也就是说PSH为1表示的是有真正的TCP数据包内容被传递。 

(4)TCP的连接建立和连接关闭,都是通过请求-相应的模式完成的。最常见的非法组合是SYN/FIN 包。注意:由于 SYN包是用来初始化连接的,它不可能和 FIN和RST标记一起出现,这是一个恶意攻击。 由 SYN/FIN 包,我们知道别的一些组合,例如SYN/FIN/PSH, SYN/FIN/RST, SYN/FIN/RST/PSH。很明显,这些也都是恶意的攻击。 

(5)别的已知的非法包有FIN (无ACK标记)和"NULL"包。由于ACK/FIN包的出现是为了关闭一个TCP连接,那么正常的FIN包总是带有 ACK 标记。"NULL"包就是没有任何TCP标记的包(URG,ACK,PSH,RST,SYN,FIN都为0)。 

我们自己实现的socket应怎么确定传输结束那?

一种办法是服务器发送结束就直接断开连接,但是如果还有其他业务需要通信这种办法不太靠谱;
另一种办法是,服务器先给客户端发送文件的大小,然后客户端通过这个大小自己决定何时结束,例如FTP协议,客户端再请求文件可以再 connect 重新建立一条链接来,把传控制协议的通道和传数据的通道分开。.可以提前定义好文件名、文件大小和文件md5(用来检验),发送给客户端,然后让客户端去断开连接。

  • 调用socket的 shutdownOutput 方法(Java)关闭输出流,该方法的文档说明为,将此套接字的输出流置于“流的末尾”,这样另一端的输入流上的read操作就会返回-1。
  • 约定结束标志,当读到该结束标志时退出不再read。 (Http 的 Transfer-Encoding: Chunked 首部,表示将以一个 length 为 0 的 chunk 做结束标志)
  • 设置超时(timeout),会在设置的超时时间到达后抛出SocketTimeoutException异常而不再阻塞。
  • 双方定义好通信协议,在协议头部约定好数据的长度。当读取到的长度等于这个长度时就不再继续调用read方法。(Http 的 content-length 首部,会给出主体的长度)

 

 

<think>我们重点解析TCP协议。根据引用[1]提供的字段信息,TCP在Scapy中的关键字段包括: - sport: 源端口 - dport: 目的端口 - seq: 序列号 - ack: 确认号 - flags: 标志位(如SYN, ACK等) - chksum: 校验(注意:引用[1]中TCP的校验字段为chksum,但实际Scapy中TCP的校验字段是`chksum`) 此外,TCP头部还有窗口大小、紧急指针、选项等字段。 我们将详细展开: 1. TCP头部结构 2. Scapy中TCP层的访问方法 3. 实例解析 注意:引用[3]中展示了UDP报文的解析,我们可以借鉴其方式,但针对TCP。 结构如下: ### Python Scapy解析传输层TCP协议详解 #### 1. TCP协议头部结构 TCP头部通常为20字节(不含选项),结构如下: $$ \text{源端口(16 bits)} \parallel \text{目的端口(16 bits)} \parallel \text{序列号(32 bits)} \parallel \text{确认号(32 bits)} \parallel \text{数据偏移(4 bits)} \parallel \text{保留(4 bits)} \parallel \text{标志位(8 bits)} \parallel \text{窗口大小(16 bits)} \parallel \text{校验(16 bits)} \parallel \text{紧急指针(16 bits)} \parallel \text{选项(可变长度)} \parallel \text{填充} $$ 在Scapy中,TCP头部字段对应关系如下[^1]: | 字段名 | Scapy属性 | 长度 | 说明 | |---------------|----------------|--------|--------------------------| | 源端口 | `sport` | 16 bits | 取值范围0~65535 | | 目的端口 | `dport` | 16 bits | 取值范围0~65535 | | 序列号 | `seq` | 32 bits | 当前数据包的第一个字节的序列号 | | 确认号 | `ack` | 32 bits | 期望收到的下一个字节的序列号 | | 数据偏移 | `dataofs` | 4 bits | 指示TCP头部长度(以4字节为单位)| | 保留 | `reserved` | 4 bits | 必须为0 | | 标志位 | `flags` | 8 bits | 控制位(如SYN, ACK等) | | 窗口大小 | `window` | 16 bits | 接收窗口大小 | | 校验 | `chksum` | 16 bits | 头部数据的校验 | | 紧急指针 | `urgptr` | 16 bits | 紧急数据位置(当URG标志置位时有效)| | 选项 | `options` | 可变 | 可选字段 | > 注意:标志位`flags`在Scapy中以单个字符表示,组合成字符串,如`SA`表示SYN+ACK。 #### 2. 核心解析方法 以下函数演示如何解析TCP层: ```python from scapy.all import * def parse_tcp_packet(packet): # 确保是TCP数据包 if TCP in packet: tcp_layer = packet[TCP] # 提取关键字段 print(f"TCP源端口: {tcp_layer.sport}") print(f"TCP目的端口: {tcp_layer.dport}") print(f"序列号 (seq): {tcp_layer.seq}") print(f"确认号 (ack): {tcp_layer.ack}") print(f"数据偏移 (头部长度): {tcp_layer.dataofs} 字(即{tcp_layer.dataofs * 4}字节)") print(f"标志位: {tcp_layer.flags} -> 含义: {parse_tcp_flags(tcp_layer.flags)}") print(f"窗口大小: {tcp_layer.window}") print(f"校验: {hex(tcp_layer.chksum)}") print(f"紧急指针: {tcp_layer.urgptr}") # 解析选项字段(如果有) if tcp_layer.options: print(f"TCP选项: {tcp_layer.options}") else: print("非TCP数据包") def parse_tcp_flags(flags_str): """解析TCP标志位字符串""" flag_meanings = { 'F': 'FIN', 'S': 'SYN', 'R': 'RST', 'P': 'PSH', 'A': 'ACK', 'U': 'URG', 'E': 'ECE', 'C': 'CWR' } # 将标志位字符串拆分为单个字符,然后映射为含义 return [flag_meanings.get(flag, flag) for flag in flags_str] ``` #### 3. 实际应用示例 **示例1:解析单个数据包** ```python # 构造一个简单的TCP数据包(SYN包) ip = IP(src="192.168.1.100", dst="10.0.0.1") tcp = TCP(sport=12345, dport=80, flags="S", seq=1000) packet = ip / tcp # 调用解析函数 parse_tcp_packet(packet) ``` 输出: ``` TCP源端口: 12345 TCP目的端口: 80 序列号 (seq): 1000 确认号 (ack): 0 数据偏移 (头部长度): 5 字(即20字节) 标志位: S -> 含义: ['SYN'] 窗口大小: 8192 校验: 0x0 紧急指针: 0 ``` **示例2:解析pcap文件中的TCP数据包** ```python packets = rdpcap("tcp_traffic.pcap") for pkt in packets: if TCP in pkt and IP in pkt: print(f"\n源IP: {pkt[IP].src}:{pkt[TCP].sport} -> 目的IP: {pkt[IP].dst}:{pkt[TCP].dport}") parse_tcp_packet(pkt) ``` #### 4. 高级应用:TCP三次握手分析 通过实时捕获分析TCP三次握手: ```python # 定义回调函数 def tcp_handshake_analyzer(pkt): if TCP in pkt: flags = pkt[TCP].flags if flags == "S": # SYN print(f"[SYN] 源端口: {pkt[TCP].sport} -> 目的端口: {pkt[TCP].dport}, seq={pkt[TCP].seq}") elif flags == "SA": # SYN-ACK print(f"[SYN-ACK] 源端口: {pkt[TCP].sport} -> 目的端口: {pkt[TCP].dport}, seq={pkt[TCP].seq}, ack={pkt[TCP].ack}") elif flags == "A": # ACK print(f"[ACK] 源端口: {pkt[TCP].sport} -> 目的端口: {pkt[TCP].dport}, ack={pkt[TCP].ack}") # 实时捕获三次握手(过滤目标端口80的TCP包) sniff(filter="tcp and port 80", prn=tcp_handshake_analyzer, count=3) ``` #### 5. 常见问题处理 - **获取载荷数据**:使用`Raw`层 ```python if Raw in pkt: payload = pkt[Raw].load print(f"载荷长度: {len(payload)} 字节") ``` - **处理分片数据包**:Scapy默认会自动重组(使用`sniff(defrag=1)`) - **校验验证**:Scapy在构造包时会自动计算校验,但解析时若需验证可使用`pkt[TCP].chksum == checksum_func(pkt)`(需自行实现校验函数) > 注意:引用[1]指出Scapy需要系统安装Wireshark,建议先安装Wireshark/tshark[^1]。 §§相关问题§§ 1. 如何用Scapy构造一个带TCP选项的数据包? 2. Scapy如何解析TCP流重组后的完整数据? 3. TCP校验计算的具体方法是什么? 4. 如何识别TCP连接中的异常断开(如RST包)? </think>### Python Scapy解析传输层TCP协议详解 Scapy是一个强大的网络数据包操作库,能深入解析TCP协议的各个字段。以下是详细解析过程: #### 1. TCP头部结构 TCP头部通常包含20字节固定部分(不含选项),结构如下: $$ \text{源端口(16 bits)} \parallel \text{目的端口(16 bits)} \parallel \text{序列号(32 bits)} \parallel \text{确认号(32 bits)} \parallel \text{数据偏移(4 bits)} \parallel \text{保留(6 bits)} \parallel \text{标志位(6 bits)} \parallel \text{窗口大小(16 bits)} \parallel \text{校验(16 bits)} \parallel \text{紧急指针(16 bits)} $$ 在Scapy中对应字段(参考引用[1]): ```python TCP(sport=8080, dport=80, seq=1000, ack=2000, flags="SA", window=8192, chksum=0x1234, options=[('MSS',1460)]) ``` #### 2. 核心解析方法 ```python from scapy.all import * def parse_tcp_packet(pkt): if TCP in pkt: tcp = pkt[TCP] print(f"\n=== TCP头部解析 ===") print(f"源端口: {tcp.sport} → 目的端口: {tcp.dport}") print(f"序列号: {tcp.seq} | 确认号: {tcp.ack}") print(f"标志位: {parse_tcp_flags(tcp.flags)}") print(f"窗口大小: {tcp.window} 字节 | 校验: 0x{tcp.chksum:04x}") # 解析选项字段 if tcp.options: print(f"TCP选项: {decode_tcp_options(tcp.options)}") # 获取载荷数据 if Raw in pkt: payload = pkt[Raw].load print(f"载荷长度: {len(payload)} 字节 | HEX: {payload[:16].hex()}...") def parse_tcp_flags(flags): """解析TCP标志位""" flag_dict = { 'F': 'FIN', 'S': 'SYN', 'R': 'RST', 'P': 'PSH', 'A': 'ACK', 'U': 'URG', 'E': 'ECE', 'C': 'CWR' } return [flag_dict.get(f, f) for f in str(flags)] def decode_tcp_options(options): """解析TCP选项字段""" opt_map = {0: "EOL", 1: "NOP", 2: "MSS", 3: "WS", 4: "SACK"} return [(opt_map.get(k, k), v) for k,v in options] ``` #### 3. 实际应用示例 **示例1:解析实时TCP流量** ```python # 捕获HTTP流量并解析 sniff(filter="tcp port 80", prn=parse_tcp_packet, count=5) ``` 输出示例: ``` === TCP头部解析 === 源端口: 54321 → 目的端口: 80 序列号: 18999241 | 确认号: 0 标志位: ['SYN'] 窗口大小: 64240 字节 | 校验: 0x7e7c 载荷长度: 0 字节 ``` **示例2:解析pcap文件** ```python packets = rdpcap("http_traffic.pcap") for pkt in packets: if TCP in pkt: parse_tcp_packet(pkt) ``` #### 4. TCP标志位详解 Scapy中标志位通过单字母组合表示(参考引用[1]): - `S` = SYN (同步) - `A` = ACK (确认) - `F` = FIN (结束) - `R` = RST (重置) - `P` = PSH (推送) - `U` = URG (紧急) 例如三次握手过程: 1. SYN包:`flags='S'` 2. SYN-ACK包:`flags='SA'` 3. ACK包:`flags='A'` #### 5. 高级功能 **TCP流重组**(参考引用[3]): ```python sessions = packets.sessions() for session in sessions.values(): # 提取完整HTTP会 http_payload = b"".join(pkt[Raw].load for pkt in session if Raw in pkt) print(f"HTTP会长度: {len(http_payload)}字节") ``` **构造TCP响应包**: ```python # 响应SYN包 syn_ack = IP(src=pkt[IP].dst, dst=pkt[IP].src) / \ TCP(sport=pkt[TCP].dport, dport=pkt[TCP].sport, flags="SA", seq=1000, ack=pkt[TCP].seq+1) send(syn_ack) ``` #### 6. 常见问题处理 - **校验验证**:Scapy默认自动计算校验,手动验证: ```python if tcp.chksum != checksum(bytes(pkt[TCP])): print("校验无效!") ``` - **选项解析**:常见选项类型: - MSS: 最大分段大小 - WS: 窗口缩放因子 - SACK: 选择性确认 > 注意:引用[1]指出Scapy需要系统安装Wireshark,建议先安装Wireshark/tshark[^1]
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值