2019独角兽企业重金招聘Python工程师标准>>>
简介
StreamTCP是Suricata中用于实现TCP流跟踪(Tracking)和重组(Reassembly)的模块。
这个模块在Suricata的数据包处理流水线中是处于哪个位置呢?继续以前面介绍的Pcap实时类型的autofp模式为例:
当数据包由抓包线程的ReceivePcap模块获取,再经过线程内下一个模块DecodePcap完成解码后,最终将在TmThreadsSlotProcessPkt中调用tmqh_out,也即上一节介绍的TmqhOutputFlowActivePackets函数,把数据包送完输出队列,让后续线程再去继续处理。
而在对main()的介绍中,我们知道当前模式下,下一级线程配置的模块依次为:StreamTCP、Detect、RespondReject和Output相关模块。因此,StreamTCP将是数据包解码完成后第一个流经的模块。
配置初始化
StreamTcpInitConfig在main()中被调用,完成StreamTCP的配置初始化。
该函数首先会填充模块的全局配置结构TcpStreamCnf,其重要字段包括:
字段 | 含义 |
memcap | stream跟踪功能所能使用的最大内存,默认为32MB。 |
prealloc_sessions | 为每个stream线程预分配的session数量。 |
flags | 标志,目前只用到CHECKSUM_VALIDATION。 |
midstream | 是否允许接手midstream session,默认为关闭。 |
async_oneside | 是否打开异步stream处理,默认为关闭。 |
max_synack_queued | 能存放在队列中的SYN/ACK的最大数量,默认为5. |
reassembly_memcap |
重组功能所能使用的最大内存,默认为64MB。 |
reassembly_depth | 重组深度?默认为1MB。 |
ssn_init_flags | session标志 |
segment_init_flags | segment标志 |
reassembly_toserver_chunk_size | 待填 |
reassembly_toclient_chunk_size | 待填 |
除了填充该结构体,StreamTcpInitConfig还会作如下事:
- 调用StreamTcpReassembleInit对重组功能进行初始化。
- 调用FlowSetProtoFreeFunc和FlowSetFlowStateFunc,设置Flow engine对TCP的自定义状态处理函数和清理函数。
模块初始化
这个初始化指的是StreamTCP作为一个Thread Module,在其所嵌入的线程初始化阶段所被调用的该模块的ThreadInit函数。
TmModuleStreamTcpRegister注册时,将ThreadInit注册为了StreamTcpThreadInit,该函数流程为:
- 创建一个StreamTcpThread类型结构体stt作为该模块的在本线程的context。
- 设置stt->ssn_pool_id为-1。
- 注册一些性能计数器,例如TCP会话数、无效checksum包数、syn包数、synack包数、rst包数等。
- 调用StreamTcpReassembleInitThreadCtx初始化重组的context,保存在stt->ra_ctx。
- 若ssn_pool为NULL,则调用PoolThreadGrow为它创建一个新的PoolThread,预分配大小即为stream_config.prealloc_sessions,而创建的对象为TcpSession。
- 若不为NULL,则调用PoolThreadGrow扩充原有的ssn_pool。
注:Pool是一个Suricata中实现的一个通用的池存储,可以避免不断malloc和free的开销,并且显著减少堆碎片。PoolThread只是在Pool上做了一层包装,允许多个同类线程共用一个Pool数组,每个线程对应其中一项。为什么要这么麻烦而不是每个线程自己创建自己的Pool呢?我也不太理解,有待进一步研究。
模块执行
"StreamTCP"所属的线程使用的slot类型为"var",其对应的线程执行函数为TmThreadsSlotVar。该函数与TmThreadsSlotPktAcqLoop大同小异,只不过数据包获取是通过tmqh_in,然后会直接调用TmThreadsSlotVarRun把数据包送往每一个slot去处理。这个函数之前已经介绍过,它会依次调用每个slot的SlotFunc,而包含"StreamTCP"的slot的SlotFunc,就是StreamTCP,其流程如下:
- 获取之前已经初始化好的StreamTcpThread结构体。
- 若不是TCP包,则直接返回。
- 若p->flow为空,也直接返回。
- 若stream_config的CHECKSUM_VALIDATION打开了,则调用StreamTcpValidateChecksum对数据包进行校验和检查,不通过就返回。
- 将p->flow加上写锁,然后调用StreamTcpPacket,最后解锁并返回。
StreamTcpPacket完成实际的工作,流程为:
- 获取存储在flow->protoctx中的TcpSession指针,变量名为ssn。
- 如果包的ACK字段不为空,然而ACK位却没设置,则设置STREAM_PKT_BROKEN_ACK事件。
- 调用StreamTcpCheckFlowDrops检查这个流是否已经被设置了drop action,若是则给该包也打上drop标记,然后返回。
- 若ssn为NULL,或者其state为TCP_NONE,说明这个流还没有session或者session刚建立,则调用StreamTcpPacketStateNone处理:
- 若为RST包,说明这个这个流还没建立session就已经要结束了,添加STREAM_RST_BUT_NO_SESSION事件并返回。
- 若为FIN包,与上面类似,添加STREAM_FIN_BUT_NO_SESSION事件并返回。
- 若为SYN/ACK包,说明之前的SYN包没收到,若midstream和async_oneside配置都没打开,就返回。否则:
- 若ssn为NULL,则先调用StreamTcpNewSession新建一个session。
- 给session添加TCP_SYN_RECV状态,并打上一些midestream标志。
- 设置session中的server和client相关数据(类型都为TcpStream),包括初始序列号、下一个序列号、窗口大小、时间戳等,另外还会设置是否支持SACK机制。
- 若为SYN包,这是最正常的情况,处理情况与上面类似,主要区别:
- 给session添加的是TCP_SYN_SENT状态。
- 若为ACK包,说明之前的SYN/ACK、ACK都没,若midstream没打开就返回,否则处理与上面类似,主要区别:
- 给session添加的是TCP_ESTABLISHED状态。
- 会调用StreamTcpReassembleHandleSegment进行重组,因为这个包可能包含协议数据。
- 会给client和server都默认打开SACK标志。
- 否则,说明这个流已经建立session了,流程如下:
- 如果这个流是syn/ack类型的midstream,则反转这个包的方向。因为流的初始方向应该为SYN包的方向,而不是SYN/ACK的方向。
- 判断这个包是否是窗口更新(window update)包。目前没有根据这个信息做任何动作。
- 若这个包是keep-alive包或这种包的ack,则跳过下面一步处理。
- 根据session的state进行不同操作:
- TCP_SYN_SENT -> StreamTcpPacketStateSynSent
- TCP_SYN_RECV -> StreamTcpPacketStateSynRecv
- TCP_ESTABLISHED -> StreamTcpPacketStateEstablished
- TCP_FIN_WAIT1 -> StreamTcpPacketStateFinWait
- TCP_FIN_WAIT2 -> StreamTcpPacketStateFinWait2
- TCP_CLOSING -> StreamTcpPacketStateClosing
- TCP_CLOSE_WAIT -> StreamTcpPacketStateCloseWait
- TCP_LAST_ACK -> StreamTcpPacketStateLastAck
- TCP_TIME_WAIT -> StreamTcpPacketStateTimeWait
- TCP_CLOSED:说明client在结束一个session后,又新建了一个session且端口重用了。若这是一个SYN包,则可以重用这个TCPSession,进行一些重初始化,并把stat设为TCP_NONE,然后调用StreamTcpPacketStateNone去处理。
- 若session状态为ESTABLISHED及以后的,则把包的状态设为PKT_STREAM_EST。
- 若包所在的这一端(client或server)发送过FIN或RST,则把包状态设置为PKT_STREAM_EOF。
- 处理pseudo packet。目前只有在收到RST包时会产生,用来强制另一方向进行重组。
- 正常情况下,到这里就返回了。但如果之前有发现出错的数据包,则跳转到最后的error标号后执行:
- 若有pseudo packet,就把它们enqueue到post_queue中去,防止丢失,下次就会处理。
- 若包有PKT_STREAM_MODIFIED标志,说明被自身模块修改过,就重新计算校验和(为什么需要?)。
- 若stream_inline打开了,就给数据包打上DROP标记,后续将会丢弃这个数据包。
Stream Tracking
那么,根据不同session状态处理数据包的那一簇函数StreamTcpPacketState*,做了什么事情呢?
简要来说,这些函数对不同session当前所属的状态进行了跟踪,并相应地进行错误检测、事件记录以及状态更新。
以StreamTcpPacketStateSynSent为例,处理流程为:
- 若为RST包:
- 调用StreamTcpValidateRst进行验证,不合法则返回。
- 将session状态设置为CLOSED。
- 若为FIN包:
- 代码中写着todo…
- 若为SYN/ACK包:
- 处理4WHS(SYN-SYN-SYN/ACK-ACK)的特殊情况。
- 若数据包方向为TO_SERVER,添加STREAM_3WHS_SYNACK_IN_WRONG_DIRECTION事件并返回。
- 若ACK值不等于client.isn(initial sequence number)+1,则添加STREAM_3WHS_SYNACK_WITH_WRONG_ACK事件并返回。
- 到这里,就说明是个正常的3WHS SYN/ACK包了,则调用StreamTcp3whsSynAckUpdate更新session的各个属性值,这个函数会:
- 将session状态更新为TCP_SYN_RECV。
- 将server.isn设为该包的seq,next_seq设为isn+1(SYN/FIN包都会使序列号+1,参见Understanding TCP Sequence and Acknowledgment Numbers)
- 将client.window设为该包的win值,从这可以看出TCPStream的window为流的目标端所宣传的窗口值。
- 更新时间戳选项相关的值和标志。
- 将client.last_ack设为该包的ack,而server.last_ack设为server.isn+1(这个不太理解,因为客户端的ACK还没发过来呢,为什么就设置了?)。
- 若支持,则设置client.wscale为该包的wscale,即窗口扩大因子。
- 更新SACK相关的标志。
- 将server.next_win(在窗口范围内能发送的下一个最大的seq)和client.next_win分别设为各自的last_ack+window。
- 若为SYN包:
- 若数据包方向不为TO_CLIENT,则不会做任何处理。
- 否则,说明这是一个4WHS,给session打上相应标记。
- 与SYN/ACK的处理类似,更新server的isn、next_seq、timestamp、window、wscale、sack。注意的是,并没有更新client相应数据。
- 若为ACK包:
- 异步(单边)流的情况:从同一个主机收到SYN后再收到ACK,而没有收到SYN/ACK。若async_oneside配置未打开,就返回。
- 检查p.seq是否等于client.next_seq,若不是则添加STREAM_3WHS_ASYNC_WRONG_SEQ事件并返回。
- 给session打上STREAMTCP_FLAG_ASYNC标记,并更新状态为ESTABLISHED。
- 与上面类似,更新client和server的相应属性值。
其他状态处理函数也类似,组合到一起,就构成了一个非常复杂的有限状态机,其中有多个错误终止状态(如错误序列号),但只有一个正常终止状态(TCP_CLOSED),并且还存在很多“捷径”(如从SYN_SENT直接到CLOSED)。状态的跳转并不只是依赖于当前的主状态(session->state),还包括很多附属状态(TCPSession、TCPStream的各种字段),这些附属状态在跳转过程中也会不断更新。这个理解不一定准确,但从源码中来看,TCP会话的跟踪和验证确实非常繁琐。
Stream Reassembly
现在,在看一下StreamTCP的另一个重要功能 —— 重组,是如何实现的。