TCP会话交互用时详细分析(PCAP版)

版本:
    1.4  修正解析IP包总长度结果为0(IP_Total_Length == 0)的情况 wireshark 提示 (reported as 0, presumed to be because of "TCP segmentation offload" (TSO))
主要功能:
    用于分析TCP交互过程中的用时信息,找出网络卡慢原因
前置操作:
    安装 python3.x 环境
    通过tcpdump或wireshark等抓包软件抓包并另存为PCAP文件格式
    在程序末尾设置:
        日志文件存放目录位置
        PCAP文件或存放多个PCAP文件的目录位置
TCP用时分析包含:
    客户端发送请求的传输用时(有多个数据包时计算传输完成用时)
    服务端对客户端请求进行处理的计算用时(服务端处理速度)
    服务端处理完后返回响应数据的传输用时(有多个数据包时计算传输完成用时)
    客户端发起多个请求之间的间隔用时(如:等待用户输入、等待其他进程先完成)
    客户端原因长时间等待保持连接用时(Keep-Alive)
    服务端原因长时间等待保持连接用时(Keep-Alive)
    客户端接收服务器响应数据包时缓冲区满造成等待用时(Windows=0)
    服务端接收客户端请求数据包时缓冲区满造成等待用时(Windows=0)
    会话超时被强制终止前的用时(未完成)
UDP用时分析包含:
    (未完成)
数据解析功能:
    解析TCP数据
    解析HTTP数据
    解析HTTPS数据(未完成)
    解析MySQL数据(未完成)
    解析UDP数据(未完成)
其他功能:
    画流量图(精确到秒)需要先安装 matplotlib 模块
    从PCAP文件(或存放多个PCAP文件的目录中)中提取指定会话的数据另存为PCAP文件


import socket, struct, os, time
from binascii import b2a_hex, a2b_hex
import logging
import re
from io import BytesIO      ## 在内存中组装TCP数据用
import gzip
from urllib.parse import unquote



ReadMe = '''
版本:
    1.4  修正解析IP包总长度结果为0(IP_Total_Length == 0)的情况 wireshark 提示 (reported as 0, presumed to be because of "TCP segmentation offload" (TSO))
主要功能:
    用于分析TCP交互过程中的用时信息,找出网络卡慢原因
前置操作:
    安装 python3.x 环境
    通过tcpdump或wireshark等抓包软件抓包并另存为PCAP文件格式
    在程序末尾设置:
        日志文件存放目录位置
        PCAP文件或存放多个PCAP文件的目录位置
TCP用时分析包含:
    客户端发送请求的传输用时(有多个数据包时计算传输完成用时)
    服务端对客户端请求进行处理的计算用时(服务端处理速度)
    服务端处理完后返回响应数据的传输用时(有多个数据包时计算传输完成用时)
    客户端发起多个请求之间的间隔用时(如:等待用户输入、等待其他进程先完成)
    客户端原因长时间等待保持连接用时(Keep-Alive)
    服务端原因长时间等待保持连接用时(Keep-Alive)
    客户端接收服务器响应数据包时缓冲区满造成等待用时(Windows=0)
    服务端接收客户端请求数据包时缓冲区满造成等待用时(Windows=0)
    会话超时被强制终止前的用时(未完成)
UDP用时分析包含:
    (未完成)
数据解析功能:
    解析TCP数据
    解析HTTP数据
    解析HTTPS数据(未完成)
    解析MySQL数据(未完成)
    解析UDP数据(未完成)
其他功能:
    画流量图(精确到秒)需要先安装 matplotlib 模块
    从PCAP文件(或存放多个PCAP文件的目录中)中提取指定会话的数据另存为PCAP文件
'''




## 显示颜色
import ctypes

STD_INPUT_HANDLE = -10
STD_OUTPUT_HANDLE = -11
STD_ERROR_HANDLE = -12

绿色 = 0x0a # green.
红色 = 0x0c # red.
金色 = 0x0e # yellow.
白色 = 0x0f # white.
蓝色 = 0x09 # blue.
天蓝色 = 0x0b # skyblue.
深灰色 = 0x08 # dark gray.
暗天蓝色 = 0x03 # dark skyblue.

# get handle
std_out_handle = ctypes.windll.kernel32.GetStdHandle(STD_OUTPUT_HANDLE)

def 设置CMD颜色(color, handle=std_out_handle):
    Bool = ctypes.windll.kernel32.SetConsoleTextAttribute(handle, color)
    return Bool




####################
## 解析数据包函数 ##
####################

## TCP校验
def 计算校验和(DATA):
    LEN = len(DATA)
    if LEN % 2 == 0:
        FMT = '!' + str(LEN//2) + 'H'
    else:
        DATA += b'\x00'
        LEN = len(DATA)
        FMT = '!' + str(LEN//2) + 'H'
    X = struct.unpack(FMT, DATA)
    SUM = 0
    for i in X:
        SUM += i

    while(SUM > 65535):     ## 值大于 65535 说明二进制位数超过16bit
        H16 = SUM >> 16     ## 取高16位
        L16 = SUM & 0xffff  ## 取低16位
        SUM = H16 + L16
    校验和 = SUM ^ 0xffff
    #Log.debug(f"计算 TCP_Checksum {hex(校验和)}")
    return(校验和)

## 解析包头(16字节)
def Packet_Header(BytesData):
    PacketHeader = struct.unpack('IIII', BytesData)
    时间戳 = PacketHeader[0]
    微秒 = PacketHeader[1]
    抓取数据包长度 = PacketHeader[2]       # 所抓获的数据包保存在pcap文件中的实际长度,以字节为单位。
    实际数据包长度 = PacketHeader[3]       # 所抓获的数据包的真实长度,如果文件中保存不是完整的数据包,那么这个值可能要比前面的数据包长度的值大。
    #return(时间戳, 微秒, 抓取数据包长度, 实际数据包长度)
    时间戳_float = 时间戳 + 微秒/1000000
    return(时间戳_float, 抓取数据包长度, 实际数据包长度)

## 解析帧头(14字节)
def EthernetII_Header(BytesData):
    DstMAC = BytesData[0:6]             # 目的MAC地址
    SrcMAC = BytesData[6:12]            # 源MAC地址
    FrameType = BytesData[12:14]        # 帧类型
    return(DstMAC, SrcMAC, FrameType)

## 解析IP头(20字节)
def IPv4_Header(BytesData):
    IP_B, IP_TOS, IP_Total_Length, IP_Identification, IP_H, IP_TTL, IP_Protocol, IP_Header_checksum, IP_Source, IP_Destination = struct.unpack('!BBHHHBBHII', BytesData)                                     # B
    IP_Version = IP_B >> 4          # 取1字节的前4位
    IP_Header_Length = IP_B & 0xF   # 取1字节的后4位
    IP_Flags = IP_H >> 13
    IP_Fragment_offset = IP_H & 0b0001111111111111
    return(IP_Version, IP_Header_Length, IP_TOS, IP_Total_Length, IP_Identification, IP_Flags, IP_Fragment_offset, IP_TTL, IP_Protocol, IP_Header_checksum, IP_Source, IP_Destination)

## 解析TCP头(20字节)
def TCP_Header(BytesData):
    TCP_Source_Port, TCP_Destination_Port, TCP_Sequence_Number, TCP_Acknowledgment_Number, TCP_H, TCP_Window, TCP_Checksum, TCP_Urgent_Pointers = struct.unpack('!HHLLHHHH', BytesData)                                                                # H(2B)
    TCP_Data_Offset = TCP_H >> 12
    先去掉后6位 = TCP_H >> 6
    TCP_Reserved = 先去掉后6位 & 0b0000111111
    TCP_Flags = TCP_H & 0b0000000000111111
    return(TCP_Source_Port, TCP_Destination_Port, TCP_Sequence_Number, TCP_Acknowledgment_Number, TCP_Data_Offset, TCP_Reserved, TCP_Flags, TCP_Window, TCP_Checksum, TCP_Urgent_Pointers)

## 解析TCP头中Options(0字节或4字节的倍数)
def TCP_Options(BytesData):
    #print("TCP_Options", BytesData)
    ## 格式 Kind/Type(1 Byte) + Length(1 Byte) + Value(X Bytes)
    ## EOL 和 NOP Option 只有 Kind/Type(1 Byte)
    ## 准备需要return的变量
    MSS = 0             # 本端可以接受的最大实际数据长度(单位字节,不含TCP Header),默认值536,最大65535
    WSOPT = 0           # 窗口扩大系数(转成倍数是 2**WSOPT)
    #SACK_Premitted = 0  # 告知对方自己支持SACK(允许只重传丢失部分数据),自定义值,1表示启用,0表示不启用
    L_SACK_INFO = []    # 重传的数据信息
    
    N = 0                          # 记录已经读到的位置
    while N < len(BytesData):
        # 读取首字节判断 Kind/Type(1 Byte)
        Kind = BytesData[N:N+1]
        #print("  Kind/Type", Kind)
        if Kind == b'\x02':
            ## 最大Segment长度(MSS)
            #print("    Kind:   Maximum Segment Size (最大Segment长度)")
            Length = 4  ## 固定为4
            #print("    Length:", Length)
            Value = struct.unpack('!H', BytesData[N+2:N+Length])[0]
            #print("    Value: ", Value)
            N += Length     ## 更新N为实际已经读取了的字数
            MSS = Value     ## 赋值准备return的变量
        elif Kind == b'\x01':
            ## 补位填充
            #print("    Kind:   NOP Option (补位填充)")
            N +=1
        elif Kind == b'\x00':
            ## 选项列表结束
            #print("    Kind:   EOL (选项列表结束)")
            N +=1
        elif Kind == b'\x03':
            ## 窗口扩大系数
            #print("    Kind:   Window Scaling Factor (窗口扩大系数)")
            Length = 3      ## 固定为3
            #print("    Length:", Length)
            Value = struct.unpack('B', BytesData[N+2:N+Length])[0]
            #print("    Value: ", Value)
            N += Length
            WSOPT = Value
        elif Kind == b'\x04':
            ## 支持SACK
            #print("      Kind:   SACK-Premitted(支持SACK)")
            Length = 2  ## 固定为2
            #print("    Length:", Length)
            N += Length
            #SACK_Premitted = 1  ## 自定义,1表示启用,0表示不启用
        elif Kind == b'\x05':
            ## 乱序/丢包数据
            #print("    Kind:   SACK Block(乱序/丢包信息)")
            ## 长度不固定,存在后续1个字节中
            Length = struct.unpack('B', BytesData[N+1:N+2])[0]
            #print("    Length:", Length)
            Value = BytesData[N+2:N+Length]
            #print("    Value: ", b2a_hex(Value))
            #print("len(Value)", len(Value))
            for i in range(0, len(Value)-2, 8):
                T = struct.unpack('!II', Value[i:i+8])
                #print("T", T)
                L_SACK_INFO.append(T)
            N += Length
        elif Kind == b'\x08':
            ## Timestamps(随时间单调递增的值)
            #print("    Kind:   Timestamps(随时间单调递增的值)")
            Length = 10 ## 固定为10
            #print("    Length:", Length)
            Value = BytesData[N+2:N+Length]
            #print("    Value: ", b2a_hex(Value))
            N += Length
        elif Kind == b'\x13':   # 19
            ## MD5认证
            #print("    Kind:   TCP-MD5(MD5认证)")
            Length = 18 ## 固定为18
            #print("    Length:", Length)
            Value = BytesData[N+2:N+Length]
            #print("    Value: ", b2a_hex(Value))
            N += Length
        elif Kind == b'\x1c':   # 28
            ## 超过一定闲置时间后拆除连接
            #print("    Kind:   User Timeout(超过一定闲置时间后拆除连接)")
            Length = 4 ## 固定为4
            #print("    Length:", Length)
            Value = BytesData[N+2:N+Length]
            #print("    Value: ", b2a_hex(Value))
            N += Length
        elif Kind == b'\x1d':   # 29
            ## 认证(可选用各种算法)
            #print("    Kind:   TCP-AO(认证(可选用各种算法))")
            ## 长度不固定,存在后续1个字节中
            Length = struct.unpack('B', BytesData[N+1:N+2])[0]
            #print("    Length:", Length)
            Value = BytesData[N+2:N+Length]
            #print("    Value: ", b2a_hex(Value))
            N += Length
        elif Kind == b'\xfd':   # 253
            ## 科研实验保留
            #print("    Kind:   Experimental(科研实验保留/253)")
            ## 长度不固定,存在后续1个字节中
            Length = struct.unpack('B', BytesData[N+1:N+2])[0]
            #print("    Length:", Length)
            Value = BytesData[N+2:N+Length]
            #print("    Value: ", b2a_hex(Value))
            N += Length
        elif Kind == b'\xfe':   # 254
            ## 科研实验保留
            #print("    Kind:   Experimental(科研实验保留/254)")
            ## 长度不固定,存在后续1个字节中
            Length = struct.unpack('B', BytesData[N+1:N+2])[0]
            #print("    Length:", Length)
            Value = BytesData[N+2:N+Length]
            #print("    Value: ", b2a_hex(Value))
            N += Length
        else:
            print(f"【W】未知 TCP Option,终止继续解析TCP选项部分,Kind={Kind}")
            Value = BytesData[N:]
            print("    Value: ", b2a_hex(Value))
            break
    return(MSS, WSOPT, L_SACK_INFO)

## 粗略解析TCP实际数据(以UTF8编码解析,忽略解析不出的部分)
def TCP_DATA(BytesData):
    print(BytesData.decode('UTF8', errors='ignore'))



##################################################
## 解析PCAP文件中每个数据包,结果存入全局总字典 ##
##################################################
## 总字典                   # {(源地址,源端口,目的地址,目的端口):D_TCP, (目的地址,目的端口,源地址,源端口):D_TCP}
## 参数 D_TCP               # 子字典,存单个TCP会话交互中的包信息
## 参数 PacketHeader_Bytes  # PCAP文件存储每个数据包的包头(16字节)内容为:时间戳, 抓取数据包长度, 实际数据包长度
## 参数 PacketData          # 数据包实际内容
## 记录 D_TCP['L_DATA']     # TCP会话交互中的包简要信息列表 (编号,时间戳,SrcIP,DstIP,SrcPort,DstPort,Seq,Ack,FLAGE,TCP_DATA_LEN,TCP_req_or_ack,TCP_Window,hex(TCP_Checksum),PS)
## 前置条件:不能含VLAN信息,这些由前置拆分PCAP文件的函数解决
##################################################
def D_SAVE(D_TCP, PacketHeader_Bytes, PacketData, 编号):
    TCP校验 = 0   # 是否检查TCP校验和,0关闭 1开启
    PS = ''       # 初始化每个包的备注信息
    时间戳, 抓取数据包长度, 实际数据包长度 = Packet_Header(PacketHeader_Bytes)
    
    DstMAC, SrcMAC, FrameType = EthernetII_Header(PacketData[0:14])     # 以太帧头
    ###########【IPv4】###########
    if FrameType == b'\x08\x00':
        ## 解析IP头(20字节[14:34])
        (IP_Version, IP_Header_Length, IP_TOS, IP_Total_Length, IP_Identification, IP_Flags, IP_Fragment_offset, IP_TTL, IP_Protocol, IP_Header_checksum, IP_Source, IP_Destination) = IPv4_Header(PacketData[14:34])
        IP部首字节长度 = IP_Header_Length*4
        ###########【TCP】###########
        if IP_Protocol == 6:               # TCP b'\x06'
            TCP_Source_Port, TCP_Destination_Port, TCP_Sequence_Number, TCP_Acknowledgment_Number, TCP_Data_Offset, TCP_Reserved, TCP_Flags, TCP_Window, TCP_Checksum, TCP_Urgent_Pointers = TCP_Header(PacketData[34:54])
            if TCP_Reserved != 0:
                Log.debug("【注意,保留6bit被使用,支持(CWR/ECN)功能】TCP_Reserved(6bit)", TCP_Reserved)
            
            ## 根据TCP部首长度计算是否有TCP可选字段
            TCP部首实际长度 = TCP_Data_Offset*4
            TCP部首固定长度 = 20
            TCP选项长度 = TCP部首实际长度 - TCP部首固定长度
            ## 初始化TCP选项信息
            MSS = 0             # 本端可以接受的最大实际数据长度(单位字节,不含TCP Header),默认值536,最大65535
            WSOPT = 0           # 窗口扩大系数
            L_SACK_INFO = []    # 放丢包信息
            if TCP选项长度 > 0:
                MSS, WSOPT, L_SACK_INFO = TCP_Options(PacketData[54:54+TCP选项长度])
                if MSS == 0:
                    MSS = 536   # 使用默认值
            
            TCP_DATA_LEN = IP_Total_Length - IP部首字节长度 - TCP部首固定长度 - TCP选项长度
            
            ## 计算数据长度
            ## 最小60字节,不足会填充
            #剩余全部数据 = PacketData[54+TCP选项长度:]
            剩余有效数据 = PacketData[54+TCP选项长度:54+TCP选项长度+TCP_DATA_LEN]
            #print("剩余全部数据字节数(TCP选项后全部)", len(剩余全部数据))           ## 当不足最小长度会填充,这个会比下面的大
            #print("剩余有效数据字节数(计算得到)", len(剩余有效数据))
            
            ## 修正解析IP包总长度结果为0的情况 wireshark 提示 (reported as 0, presumed to be because of "TCP segmentation offload" (TSO))
            if IP_Total_Length == 0:
                剩余有效数据 = PacketData[54+TCP选项长度:]    # 修正内容数据值为剩余全部数据值
                TCP_DATA_LEN = len(剩余有效数据)              # 修正内容数据的长度值
            
            ## 是否进行TCP校验,校验错误的包可能可以正常使用
            if TCP校验 == 1:
                ## 校验TCP是否正确
                #print("提取 TCP_Checksum", hex(TCP_Checksum))
                TCP_Pseudo_Total_Length = TCP部首实际长度 + TCP_DATA_LEN
                #print("TCP_Pseudo_Total_Length", TCP_Pseudo_Total_Length)
                TCP_Pseudo_Header = PacketData[26:34] + b'\x00\x06' + struct.pack('!H', TCP_Pseudo_Total_Length)    # 构造TCP伪部首
                if TCP选项长度 == 0:
                    DATA = TCP_Pseudo_Header + PacketData[34:54] + 剩余有效数据
                else:
                    TCP选项_Bytes = PacketData[54:54+TCP选项长度]
                    DATA = TCP_Pseudo_Header + PacketData[34:54] + TCP选项_Bytes + 剩余有效数据
                校验结果 = 计算校验和(DATA)                      #校验通过返回为0
                if 校验结果 != 0:
                    PS = PS + f"TCP校验错误({hex(校验结果)})"
            
            ## 分析数据包中的TCP内容
            TCP_req_or_ack = ''                     # 标识一下是请求还是响应(一般都是客户端发起请求,服务端响应)
            FLAGE = ''
            SrcIP_Bytes = PacketData[26:30]         # 源IP地址
            DstIP_Bytes = PacketData[30:34]         # 目的IP地址
            SrcIP = socket.inet_ntoa(SrcIP_Bytes)
            DstIP = socket.inet_ntoa(DstIP_Bytes)
            SrcPort = TCP_Source_Port
            DstPort = TCP_Destination_Port
            Seq = TCP_Sequence_Number
            Ack = TCP_Acknowledgment_Number
            if TCP_Flags == 24:                     # 24 011000 URG ACK PSH RST SYN FIN
                FLAGE = 'ACK PSH'
            elif TCP_Flags == 16:                   # 16 010000 URG ACK PSH RST SYN FIN
                FLAGE = 'ACK'
            elif TCP_Flags == 17:                   # 17 010001 URG ACK PSH RST SYN FIN
                FLAGE = 'ACK FIN'
            elif TCP_Flags == 18:                   # 18 010010 URG ACK PSH RST SYN FIN
                FLAGE = 'ACK SYN'
            elif TCP_Flags == 2:                    #  2 000010 URG ACK PSH RST SYN FIN
                FLAGE = 'SYN'
            elif TCP_Flags == 20:                   # 20 010100 URG ACK PSH RST SYN FIN
                FLAGE = 'ACK RST'
            elif TCP_Flags == 4:                    #  4 000100 URG ACK PSH RST SYN FIN
                FLAGE = 'RST'
            elif TCP_Flags == 25:                   # 25 011001 URG ACK PSH RST SYN FIN
                FLAGE = 'A.P.F'
            elif TCP_Flags == 28:                   # 28 011100 URG ACK PSH RST SYN FIN
                FLAGE = 'A.P.R'
            else:
                FLAGE = 'NA'
                Log.error("【ERROR】TCP_Flags unknown %d URG ACK PSH RST SYN FIN 编号 %d" % (TCP_Flags, 编号))
            
            ## 先判断出客户端和服务端的IP及端口
            ## 如果前面还没有判断出客户端和服务端,且这个包不是'SYN'和'ACK SYN',就根据端口大小估计客户端和服务端,大的为客户端
            if D_TCP['CLIENT'] == ():
                if FLAGE not in ('SYN', 'ACK SYN'):
                    if SrcPort > DstPort:
                        D_TCP['CLIENT'] = (SrcIP, SrcPort)
                        D_TCP['SERVER'] = (DstIP, DstPort)
                        D_TCP['C_Tx_DATA_ALL'] = Seq
                        D_TCP['S_Tx_DATA_ALL'] = Ack
                    else:
                        D_TCP['CLIENT'] = (DstIP, DstPort)
                        D_TCP['SERVER'] = (SrcIP, SrcPort)
                        D_TCP['S_Tx_DATA_ALL'] = Seq
                        D_TCP['C_Tx_DATA_ALL'] = Ack
            
            ## 根据FLAGE分类存数据包
            if FLAGE in ('ACK', 'ACK PSH'):
                ## 先区分是哪端发的ACK
                if SrcPort == D_TCP['CLIENT'][1]:
                    ## 客户端发ACK
                    ## Seq = C发的这个包的数据编号(累计值,不含当次数据,不含重发)
                    ## Ack = C确认已经接到S数据的编号(丢包/乱序信息放在TCP选项SACK中发送)
                    
                    ## 判断是否有数据
                    if TCP_DATA_LEN == 0:
                        TCP_ACK_KeepAlive_Mark = 0  # 标记是否是Keep-Alive包,0表示不是。1表示是,初始化为0
                        if TCP_Window == 0:                                                                        # 记录C发的ACK且窗口为0的包信息,说明C接收缓存满,同一段接收满的包的(Seq,Ack)相同,就只记录一次Ack
                            PS = PS + '接收缓冲区已满(C)'
                            if Ack not in D_TCP['D_C_WIN_0']:
                                D_TCP['D_C_WIN_0'][Ack] = [(时间戳, Seq, Ack, TCP_DATA_LEN, TCP_Window, 编号)]     # 记录C接收缓存满的包信息
                                D_TCP['C_ACK_WIN_0'].append((Seq,Ack))                                             # 记录C接收缓存满的包信息(判断对方探测包使用)
                            else:
                                D_TCP['D_C_WIN_0'][Ack].append((时间戳, Seq, Ack, TCP_DATA_LEN, TCP_Window, 编号)) # 记录C接收缓存满的包信息
                        if L_SACK_INFO != []:
                            if L_SACK_INFO[0][0] +1 == L_SACK_INFO[0][1]:
                                PS = PS + f'【TCP Keep-Alive ACK】(C) 响应保持连接包,L_SACK_INFO={L_SACK_INFO}'
                                TCP_ACK_KeepAlive_Mark = 1  # 标记是Keep-Alive包
                                ## 保存C响应保持连接的包信息
                                if Ack in D_TCP['C_TCP_Keep_Alive_ACK']:
                                    D_TCP['C_TCP_Keep_Alive_ACK'][Ack].append((时间戳, Seq, Ack, TCP_DATA_LEN, TCP_Window, 编号))
                                else:
                                    D_TCP['C_TCP_Keep_Alive_ACK'][Ack] = [(时间戳, Seq, Ack, TCP_DATA_LEN, TCP_Window, 编号)]
                            else:
                                PS = PS + f'C丢包乱序通知={L_SACK_INFO}'
                        ## 保存纯确认消息包到 D_C_ACK (C确认收完S数据的纯回应包会在这里)
                        if TCP_ACK_KeepAlive_Mark == 0:     # 忽略响应保持连接包
                            if Ack in D_TCP['D_C_ACK']:
                                D_TCP['D_C_ACK'][Ack].append((时间戳, Seq, Ack, TCP_DATA_LEN, TCP_Window))
                            else:
                                D_TCP['D_C_ACK'][Ack] = [(时间戳, Seq, Ack, TCP_DATA_LEN, TCP_Window)]
                    ## 数据长度为1,Seq(自发送累计) 主动变小1的,应该就是C发的keep-alive包,不累计大小
                    elif TCP_DATA_LEN == 1 and 剩余有效数据 == b'\x00':
                        PS = PS + '【TCP Keep-Alive】    (C) 请求保持连接包(长度1,内容空)'
                        ## 保存C发起保持连接的包信息
                        if Ack in D_TCP['C_TCP_Keep_Alive']:
                            D_TCP['C_TCP_Keep_Alive'][Ack].append((时间戳, Seq, Ack, TCP_DATA_LEN, TCP_Window, 编号))
                        else:
                            D_TCP['C_TCP_Keep_Alive'][Ack] = [(时间戳, Seq, Ack, TCP_DATA_LEN, TCP_Window, 编号)]
                    elif TCP_DATA_LEN == 1 and (Ack,Seq) in D_TCP['S_ACK_WIN_0']:                   # TCP数据长度是1且能和S缓存满的消息能对应上,说明是C发的探测S窗口的包
                        PS = PS + '(C=>S Window=?)'
                    #elif TCP_DATA_LEN == 1:     # 查看长度为1情况
                    #    print(编号, 剩余有效数据, len(剩余有效数据))
                    else:
                        ## 已经避开了纯消息包和保持连接的包
                        ## 记录TCP有效数据的包信息(C)发起的(一般都是客户端发起请求,包信息保存到 C_TCP_REQ 字典 Key=Ack Value=[(包信息1),(包信息2)] 一个请求可能会分成多个包发送)
                        TCP_req_or_ack = 'C_req'            # 备注标识为:客户端发起请求
                        if Ack in D_TCP['D_C_ACK_DATA']:
                            D_TCP['D_C_ACK_DATA'][Ack].append((时间戳, Seq, Ack, TCP_DATA_LEN, TCP_Window, 编号))
                        else:
                            D_TCP['D_C_ACK_DATA'][Ack] = [(时间戳, Seq, Ack, TCP_DATA_LEN, TCP_Window, 编号)]
                        ## 重复的情况
                        if (Seq,Ack) not in D_TCP['P_C_Tx_Seq_Ack']:
                            D_TCP['P_C_Tx_Seq_Ack'].add((Seq,Ack))                                           # 加入C发送过的数据集合,查重复用
                            D_TCP['C_Tx_DATA_ALL'] += TCP_DATA_LEN                                            # 累计C发送数据(也可以用于识别C发的keep-alive)
                        else:
                            PS = PS + 'C重发数据(Seq,Ack)完全重复'
                else:
                    ## 服务端发ACK
                    ## Seq = S自己已经发数据的累计(不含本次的数据量,不累计重发数据)
                    ## Ack = S已经接到了C发的多少数据(S告诉C端,S是接到C多少数据后的回应,在HTTP里这个可以用于区别这个回应是针对哪个请求的)
                    if TCP_DATA_LEN == 0:
                        ## 保存到 D_S_ACK
                        if Ack in D_TCP['D_S_ACK']:
                            D_TCP['D_S_ACK'][Ack].append((时间戳, Seq, Ack, TCP_DATA_LEN, TCP_Window))
                        else:
                            D_TCP['D_S_ACK'][Ack] = [(时间戳, Seq, Ack, TCP_DATA_LEN, TCP_Window)]
                        if TCP_Window == 0:                                 # 记录S发的ACK且窗口为0的包信息(Seq,Ack)说明S接收缓存满
                            PS = '接收缓冲区已满(S)'
                            if Ack not in D_TCP['D_S_WIN_0']:
                                D_TCP['D_S_WIN_0'][Ack] = [(时间戳, Seq, Ack, TCP_DATA_LEN, TCP_Window, 编号)]
                                D_TCP['S_ACK_WIN_0'].append((Seq,Ack))      # 记录S接收缓存满的包信息
                            else:
                                D_TCP['D_S_WIN_0'][Ack].append((时间戳, Seq, Ack, TCP_DATA_LEN, TCP_Window, 编号))
                        if L_SACK_INFO != []:
                            if L_SACK_INFO[0][0] +1 == L_SACK_INFO[0][1]:
                                PS = PS + f'【TCP Keep-Alive ACK】(S) 响应保持连接包,L_SACK_INFO={L_SACK_INFO}'
                                ## 保存S响应保持连接的包信息
                                if Ack in D_TCP['S_TCP_Keep_Alive_ACK']:
                                    D_TCP['S_TCP_Keep_Alive_ACK'][Ack].append((时间戳, Seq, Ack, TCP_DATA_LEN, TCP_Window, 编号))
                                else:
                                    D_TCP['S_TCP_Keep_Alive_ACK'][Ack] = [(时间戳, Seq, Ack, TCP_DATA_LEN, TCP_Window, 编号)]
                            else:
                                PS = PS + f'S丢包乱序通知={L_SACK_INFO}'
                    ## 数据长度是1且能和C缓存满的消息能对应上(Seq Ack 交换位置),说明是S发的探测C窗口的包
                    elif TCP_DATA_LEN == 1 and (Ack,Seq) in D_TCP['C_ACK_WIN_0']:
                        PS = PS + '探测窗口(S=>C Window=?)'
                    ## (S) 请求保持连接包(长度1,内容空)
                    elif TCP_DATA_LEN == 1 and 剩余有效数据 == b'\x00':
                        PS = PS + '【TCP Keep-Alive】    (S) 请求保持连接包(长度1,内容空)'
                        ## 保存S发起保持连接的包信息
                        if Ack in D_TCP['S_TCP_Keep_Alive']:
                            D_TCP['S_TCP_Keep_Alive'][Ack].append((时间戳, Seq, Ack, TCP_DATA_LEN, TCP_Window, 编号))
                        else:
                            D_TCP['S_TCP_Keep_Alive'][Ack] = [(时间戳, Seq, Ack, TCP_DATA_LEN, TCP_Window, 编号)]
                    else:
                        ## 已经避开了纯消息包和保持连接的包
                        TCP_req_or_ack = '    S_ack'            # 备注标识为:服务端响应请求
                        ## 记录TCP有效数据的包信息(S)发起的
                        if Ack in D_TCP['D_S_ACK_DATA']:
                            D_TCP['D_S_ACK_DATA'][Ack].append((时间戳, Seq, Ack, TCP_DATA_LEN, TCP_Window, 编号))
                        else:
                            D_TCP['D_S_ACK_DATA'][Ack] = [(时间戳, Seq, Ack, TCP_DATA_LEN, TCP_Window, 编号)]
                        ## 重复的情况
                        if (Seq,Ack) not in D_TCP['P_S_Tx_Seq_Ack']:
                            D_TCP['P_S_Tx_Seq_Ack'].add((Seq,Ack))
                            D_TCP['S_Tx_DATA_ALL'] += TCP_DATA_LEN                                                        # 累计S发送数据(也可以用于识别S发的keep-alive)
                        else:
                            PS = PS + 'S重发数据(Seq,Ack)完全重复'
            elif FLAGE in ('ACK FIN', 'A.P.F'):             ## 终止连接
                if DstPort == D_TCP['CLIENT'][1]:           ## 是 S 发起的 ACK FIN
                    PS = PS + '(S)发起终止连接'
                    if Seq in D_TCP['S_ACK_FIN_SeqKey']:
                        D_TCP['S_ACK_FIN_SeqKey'][Seq].append((时间戳, Seq, Ack, TCP_DATA_LEN))
                    else:
                        D_TCP['S_ACK_FIN_SeqKey'][Seq] = [(时间戳, Seq, Ack, TCP_DATA_LEN)]
                else:                                       ## 是 C 发起的 ACK FIN
                    PS = PS + '(C)发起终止连接'
                    D_TCP['C_ACK_FIN'] = (时间戳, Seq, Ack, TCP_DATA_LEN)
            elif FLAGE in ('ACK RST', 'RST', 'A.P.R'):      ## 强制断开连接
                if DstPort == D_TCP['CLIENT'][1]:
                    if Ack in D_TCP['S_ACK_RST']:
                        PS = PS + 'S强制终止连接(重复)'
                    else:
                        D_TCP['S_ACK_RST'][Ack] = (时间戳, Seq, Ack, TCP_DATA_LEN)
                        PS = PS + 'S强制终止连接'
                else:
                    ## C终止自己的请求 C_ACK_RST_AckKey 以Ack为KEY,给没有数据回应情况用请求Ack匹配RST的ACK
                    if Ack in D_TCP['C_ACK_RST_AckKey']:
                        PS = PS + 'C强制终止连接(重复)'
                    else:
                        PS = PS + 'C强制终止连接'
                        D_TCP['C_ACK_RST_AckKey'][Ack] = (时间戳, Seq, Ack, TCP_DATA_LEN)  # 记录
                    ## C终止自己的请求 C_ACK_RST_SeqKey 以Seq为KEY,期望S响应的Ack=这个Seq,给有回应的匹配用
                    if Seq in D_TCP['C_ACK_RST_SeqKey']:
                        PS = PS + '(匹配有回应)'
                    else:
                        D_TCP['C_ACK_RST_SeqKey'][Seq] = (时间戳, Seq, Ack, TCP_DATA_LEN)  # 记录
                        PS = PS + '(记录)'
            elif FLAGE == 'SYN':                                        ## 发起连接方是客户端
                if D_TCP['CLIENT'] != ():
                    if D_TCP['CLIENT'] == (SrcIP, SrcPort):
                        PS = PS + f"重复SYN【C 新建TCP连接】初始Seq={D_TCP['C_Tx_DATA_ALL']} MSS={MSS} 窗口扩大系数={WSOPT}"
                    else:
                        D_TCP['CLIENT'] = (SrcIP, SrcPort)
                        D_TCP['SERVER'] = (DstIP, DstPort)
                        D_TCP['C_SYN'] = (时间戳, Seq, Ack, 0)
                        D_TCP['C_Tx_DATA_ALL'] = Seq+1                  ## 客户端初始数据编号
                        PS = PS + f"前面判断谁是客户端错误或端口被复用,重置【C 新建TCP连接】初始Seq={D_TCP['C_Tx_DATA_ALL']} MSS={MSS} 窗口扩大系数={WSOPT}"
                else:
                    D_TCP['CLIENT'] = (SrcIP, SrcPort)
                    D_TCP['SERVER'] = (DstIP, DstPort)
                    D_TCP['C_SYN'] = (时间戳, Seq, Ack, 0)
                    D_TCP['C_Tx_DATA_ALL'] = Seq+1                      ## 客户端初始数据编号
                    PS = PS + f"【C 新建TCP连接】初始Seq={D_TCP['C_Tx_DATA_ALL']} MSS={MSS} 窗口扩大系数={WSOPT}"
            elif FLAGE == 'ACK SYN':                                    ## 回应SYN请求,是本次连接的服务端,Seq随机,Ack为发起方Seq+1
                if D_TCP['CLIENT'] == ():
                    D_TCP['CLIENT'] = (DstIP, DstPort)
                    D_TCP['SERVER'] = (SrcIP, SrcPort)
                if D_TCP['C_SYN'] != ():
                    if Ack == D_TCP['C_SYN'][1] + 1:
                        D_TCP['S_ACK_SYN'] = (时间戳, Seq, Ack, 0)
                        D_TCP['S_Tx_DATA_ALL'] = Seq+1                  ## 服务端初始数据编号
                        PS = PS + f"【S 响应TCP连接】初始Seq={D_TCP['S_Tx_DATA_ALL']} MSS={MSS} 窗口扩大系数={WSOPT}"
                    else:
                        PS = PS + 'SYN_ACK错误 或 SYN重连 或 端口被复用'
                else:
                    DEBUG = f"【DEBUG】编号={编号} ACK SYN 前面没有SYN"
                    Log.debug(DEBUG)
            else:
                Log.error(f"【ERROR】未定义怎么处理的 FLAGE {FLAGE} 忽略")
                PS = PS + f"【ERROR】未定义怎么处理的 FLAGE {FLAGE} 忽略"
            
            ## 保存信息用于显示(TCP校验通过的包/忽略TCP校验的包)
            D_TCP['L_DATA'].append((编号,时间戳,SrcIP,DstIP,SrcPort,DstPort,Seq,Ack,FLAGE,TCP_DATA_LEN,TCP_req_or_ack,TCP_Window,hex(TCP_Checksum),PS))
            
        elif IP_Protocol == 17:
            Log.error("【ERROR】UDP PASS")
        else:
            Log.error("【ERROR】NOT TCP/UDP PASS")
    else:
        Log.error("【ERROR】NOT IPv4 Ethernet")



## 记录数据包信息
def SHOW_PACK_INFO(L_DATA):
    Log.info('%3s %-17s %-15s %-15s %5s %5s %10s %10s %-7s %4s %-5s %5s %s %s' % ('ID','TIME','SrcIP','DstIP','SPort','DPort','Seq','Ack','FLAGE','LEN','请求/响应','WIN','校验码','备注说明'))
    for i in L_DATA:
        Log.info('%3s %-17s %-15s %-15s %5s %5s %10s %10s %-7s %4s %-9s %5s %6s %s' % i)
    Log.info('')



#############################
## 分析请求/响应的交互时间 ##
#############################

## 以客户端视角(在客户端处抓包)或服务端视角(在服务端处抓包)分析每个请求/响应的交互时间,记录各过程耗时,返回列表
## 请求完成耗时   COMP
## 发完请求耗时   C_req
## 处理请求耗时   SYS
## 传回结果耗时   S_send
## 请求包数量     C_PKS
## 响应包数量     S_PKS
## 设置了一下可以反应问题的自定义响应码:
# 响应状态码 = 0    # 自定义初始值,如果后面解析不出,就响应码设置为0
# 响应状态码 = 1    # 自定义,两种方法都找不到响应信息,且发现了C发的RST信息
# 响应状态码 = 2    # 自定义,两种方法都找不到响应信息,且没有发现C发的RST信息,应该是真没有响应
# 响应状态码 = 3    # 自定义,两种方法都找不到响应信息,且找到服务端发起了FIN终止
# 响应状态码 = 4    # 自定义,有响应的情况下表示C发起了RST,强制断开连接
# 响应状态码 = 5    # 自定义,两种方法都找不到响应信息,且发现了S发的RST信息

## 分析请求的结果及用时信息(一次TCP会话中全部交互过程),返回分析结果 L_TCP_TIME
def TCP_SESSION_TIME(D_TCP):
    ## 客户端发起的请求都在 D_TCP['D_C_ACK_DATA'] 中,有数据,大请求分成多个包的话seq最小那个是第一个包(可在乱序中找到实际第一个包)
    ## 在 D_TCP['D_C_ACK_DATA'] 中找出起始请求包,生成包信息列表
    L_C_ACK = []
    for K in D_TCP['D_C_ACK_DATA']:     # {1653587983: [(1625798739.757637, 1741930757, 1653587983, 87, 8212, 5)], ...}
        L = D_TCP['D_C_ACK_DATA'][K]
        MIN_INFO = L[0]
        MIN_SEQ = L[0][1]
        for Ln in range(1, len(L)):
            if L[Ln][1] < MIN_SEQ:
                MIN_SEQ = L[Ln][1]
                MIN_INFO = L[Ln]
        L_C_ACK.append(MIN_INFO)
    
    ## 服务端响应生成一个以Seq为Key,Ack为值的字典,方便客户端以自己Ack去匹配服务端Seq然后找出相应服务端Ack
    D_S_ACK_DATA_SeqKey = {}
    for K in D_TCP['D_S_ACK_DATA']:
        L = D_TCP['D_S_ACK_DATA'][K]
        MIN_SEQ = L[0][1]
        ACK = L[0][2]
        for Ln in range(1, len(L)):
            if L[Ln][1] < MIN_SEQ:
                MIN_SEQ = L[Ln][1]
                ACK = L[Ln][2]
        D_S_ACK_DATA_SeqKey[MIN_SEQ] = ACK
    
    L_TCP_TIME = []                     # 记录用时分析结果
    for C_REQ in L_C_ACK:
        C发送请求时间戳 = C_REQ[0]
        REQ_seq = C_REQ[1]
        REQ_ack = C_REQ[2]
        REQ_len = C_REQ[3]
        
        ## 设置初始值
        响应状态码 = 0                  # 正常响应设置为0
        请求类型 = "TCP"
        
        ## 处理 C 发送的请求包
        Log.debug(f"C 发送 {请求类型} 请求 ACK={REQ_ack}")
        C发送请求包列表 = D_TCP['D_C_ACK_DATA'][REQ_ack]
        
        ## 检查一下有没有异常的数据包(乱序/编号有问题)
        if 检查数据包编号(C发送请求包列表) == 1:
            Log.debug("    【WARNING】数据包编号有乱序")
        else:
            Log.debug("    数据包编号正常(单包请求忽略检查)")

        ## 开始分析请求时间信息(没有处理重发/乱序问题)
        REQ_DATA_LEN = 0                                     # 累计请求数据总长度(字节)
        C发送请求尾包 = C发送请求包列表[-1]                  # 请求包按时间最后一个
        C发完请求耗时 = C发送请求尾包[0] - C发送请求时间戳   # 最后一个减去第一个包的时间,单包请求结果为0
        for i in C发送请求包列表:
            Log.debu
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值