Suricata源码阅读笔记:StreamTCP

简介

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还会作如下事:

  1. 调用StreamTcpReassembleInit对重组功能进行初始化。
  2. 调用FlowSetProtoFreeFunc和FlowSetFlowStateFunc,设置Flow engine对TCP的自定义状态处理函数和清理函数。

模块初始化

这个初始化指的是StreamTCP作为一个Thread Module,在其所嵌入的线程初始化阶段所被调用的该模块的ThreadInit函数。

TmModuleStreamTcpRegister注册时,将ThreadInit注册为了StreamTcpThreadInit,该函数流程为:

  1. 创建一个StreamTcpThread类型结构体stt作为该模块的在本线程的context。
  2. 设置stt->ssn_pool_id为-1。
  3. 注册一些性能计数器,例如TCP会话数、无效checksum包数、syn包数、synack包数、rst包数等。
  4. 调用StreamTcpReassembleInitThreadCtx初始化重组的context,保存在stt->ra_ctx。
  5. 若ssn_pool为NULL,则调用PoolThreadGrow为它创建一个新的PoolThread,预分配大小即为stream_config.prealloc_sessions,而创建的对象为TcpSession。
  6. 若不为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,其流程如下:

  1. 获取之前已经初始化好的StreamTcpThread结构体。
  2. 若不是TCP包,则直接返回。
  3. 若p->flow为空,也直接返回。
  4. 若stream_config的CHECKSUM_VALIDATION打开了,则调用StreamTcpValidateChecksum对数据包进行校验和检查,不通过就返回。
  5. 将p->flow加上写锁,然后调用StreamTcpPacket,最后解锁并返回。

StreamTcpPacket完成实际的工作,流程为:

  1. 获取存储在flow->protoctx中的TcpSession指针,变量名为ssn。
  2. 如果包的ACK字段不为空,然而ACK位却没设置,则设置STREAM_PKT_BROKEN_ACK事件。
  3. 调用StreamTcpCheckFlowDrops检查这个流是否已经被设置了drop action,若是则给该包也打上drop标记,然后返回。
  4. 若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标志。
  5. 否则,说明这个流已经建立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。
  6. 处理pseudo packet。目前只有在收到RST包时会产生,用来强制另一方向进行重组。
  7. 正常情况下,到这里就返回了。但如果之前有发现出错的数据包,则跳转到最后的error标号后执行:
    • 若有pseudo packet,就把它们enqueue到post_queue中去,防止丢失,下次就会处理。
    • 若包有PKT_STREAM_MODIFIED标志,说明被自身模块修改过,就重新计算校验和(为什么需要?)。
    • 若stream_inline打开了,就给数据包打上DROP标记,后续将会丢弃这个数据包。

Stream Tracking

那么,根据不同session状态处理数据包的那一簇函数StreamTcpPacketState*,做了什么事情呢?

简要来说,这些函数对不同session当前所属的状态进行了跟踪,并相应地进行错误检测事件记录以及状态更新

StreamTcpPacketStateSynSent为例,处理流程为:

  1. 若为RST包:
    • 调用StreamTcpValidateRst进行验证,不合法则返回。
    • 将session状态设置为CLOSED。
  2. 若为FIN包:
    • 代码中写着todo…
  3. 若为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的另一个重要功能 —— 重组,是如何实现的。

你可能感兴趣的:(Suricata)