suricata中receive和decode两个模块总是在一个线程中,而且每种receive模块都对应一个自己的decode模块,因此这里记录在一起。这两个模块的主要目标是根据收取的数据包填充合适的Packet结构,交由flowworker进行后续处理。decode阶段同时做了分片数据包重组、隧道数据包解封装等处理。

Packet

整个数据包的处理流程都是围绕Packet结构体来操作的,先看一下这个结构体,其他关联的结构比较简单,不做更多记录。

Packet结构体
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
/* sizes of the members:
* src: 17 bytes
* dst: 17 bytes
* sp/type: 1 byte
* dp/code: 1 byte
* proto: 1 byte
* recurs: 1 byte
*
* sum of above: 38 bytes
*
* flow ptr: 4/8 bytes
* flags: 1 byte
* flowflags: 1 byte
*
* sum of above 44/48 bytes
*/
typedef struct Packet_
{
/* Addresses, Ports and protocol
* these are on top so we can use
* the Packet as a hash key */
Address src;
Address dst;
union {
Port sp;
uint8_t type;
};
union {
Port dp;
uint8_t code;
};
uint8_t proto;
/* make sure we can't be attacked on when the tunneled packet
* has the exact same tuple as the lower levels */
uint8_t recursion_level;

uint16_t vlan_id[2];
uint8_t vlan_idx;

/* flow */
uint8_t flowflags;
/* coccinelle: Packet:flowflags:FLOW_PKT_ */

/* Pkt Flags */
uint32_t flags;

struct Flow_ *flow;

/* raw hash value for looking up the flow, will need to modulated to the
* hash size still */
uint32_t flow_hash;

struct timeval ts;

union {
/* nfq stuff */
#ifdef HAVE_NFLOG
NFLOGPacketVars nflog_v;
#endif /* HAVE_NFLOG */
#ifdef NFQ
NFQPacketVars nfq_v;
#endif /* NFQ */
#ifdef IPFW
IPFWPacketVars ipfw_v;
#endif /* IPFW */
#ifdef AF_PACKET
AFPPacketVars afp_v;
#endif
#ifdef HAVE_MPIPE
/* tilegx mpipe stuff */
MpipePacketVars mpipe_v;
#endif
#ifdef HAVE_NETMAP
NetmapPacketVars netmap_v;
#endif

/** libpcap vars: shared by Pcap Live mode and Pcap File mode */
PcapPacketVars pcap_v;
};

/** The release function for packet structure and data */
void (*ReleasePacket)(struct Packet_ *);
/** The function triggering bypass the flow in the capture method.
* Return 1 for success and 0 on error */
int (*BypassPacketsFlow)(struct Packet_ *);

/* pkt vars */
PktVar *pktvar;

/* header pointers */
EthernetHdr *ethh;

/* Checksum for IP packets. */
int32_t level3_comp_csum;
/* Check sum for TCP, UDP or ICMP packets */
int32_t level4_comp_csum;

IPV4Hdr *ip4h;

IPV6Hdr *ip6h;

/* IPv4 and IPv6 are mutually exclusive */
union {
IPV4Vars ip4vars;
struct {
IPV6Vars ip6vars;
IPV6ExtHdrs ip6eh;
};
};
/* Can only be one of TCP, UDP, ICMP at any given time */
union {
TCPVars tcpvars;
ICMPV4Vars icmpv4vars;
ICMPV6Vars icmpv6vars;
} l4vars;
#define tcpvars l4vars.tcpvars
#define icmpv4vars l4vars.icmpv4vars
#define icmpv6vars l4vars.icmpv6vars

TCPHdr *tcph;

UDPHdr *udph;

SCTPHdr *sctph;

ICMPV4Hdr *icmpv4h;

ICMPV6Hdr *icmpv6h;

PPPHdr *ppph;
PPPOESessionHdr *pppoesh;
PPPOEDiscoveryHdr *pppoedh;

GREHdr *greh;

VLANHdr *vlanh[2];

/* ptr to the payload of the packet
* with it's length. */
uint8_t *payload;
uint16_t payload_len;

/* IPS action to take */
uint8_t action;

uint8_t pkt_src;

/* storage: set to pointer to heap and extended via allocation if necessary */
uint32_t pktlen;
uint8_t *ext_pkt;

/* Incoming interface */
struct LiveDevice_ *livedev;

PacketAlerts alerts;

struct Host_ *host_src;
struct Host_ *host_dst;

/** packet number in the pcap file, matches wireshark */
uint64_t pcap_cnt;


/* engine events */
PacketEngineEvents events;

AppLayerDecoderEvents *app_layer_events;

/* double linked list ptrs */
struct Packet_ *next;
struct Packet_ *prev;

/** data linktype in host order */
int datalink;

/* tunnel/encapsulation handling */
struct Packet_ *root; /* in case of tunnel this is a ptr
* to the 'real' packet, the one we
* need to set the verdict on --
* It should always point to the lowest
* packet in a encapsulated packet */

/** mutex to protect access to:
* - tunnel_rtv_cnt
* - tunnel_tpr_cnt
*/
SCMutex tunnel_mutex;
/* ready to set verdict counter, only set in root */
uint16_t tunnel_rtv_cnt;
/* tunnel packet ref count */
uint16_t tunnel_tpr_cnt;

/** tenant id for this packet, if any. If 0 then no tenant was assigned. */
uint32_t tenant_id;

/* The Packet pool from which this packet was allocated. Used when returning
* the packet to its owner's stack. If NULL, then allocated with malloc.
*/
struct PktPool_ *pool;

#ifdef PROFILING
PktProfiling *profile;
#endif
#ifdef __SC_CUDA_SUPPORT__
CudaPacketVars cuda_pkt_vars;
#endif
#ifdef HAVE_NAPATECH
NapatechPacketVars ntpv;
#endif
}
#ifdef HAVE_MPIPE
/* mPIPE requires packet buffers to be aligned to 128 byte boundaries. */
__attribute__((aligned(128)))
#endif
Packet;
  • src
  • dst
    src和dst保存了三层地址的地址族类型和地址数据。
  • sp、dp
    sp和dp保存了四层端口号。
  • type、code
    type和code保存了icmpv4协议的type和code。其中type和sp共用一个联合体,code和dp共用一个联合体。
  • proto
    三层数据包载荷的协议类型。一般来说是一个四层协议类型,但如果是一个隧道包那这里会是隧道内层数据包的类型,就不是四层协议类型了。
  • recursion_level
    指示了当前Packet经历了几次隧道封装。普通数据包这个值是0.
  • vlan_id
    长度为2的数组,存储了vlan的id,允许vlan嵌套,数组中的两项分别代表该嵌套层的vlan id。
  • vlan_idx
    标记了当前的vlan嵌套层数。最多2层。
  • flowflags
    标记了当前packet与flow相关的一些标识。
    • FLOW_PKT_TOSERVER
    • FLOW_PKT_TOCLIENT
    • FLOW_PKT_ESTABLISHED
    • FLOW_PKT_TOSERVER_IPONLY_SET
    • FLOW_PKT_TOCLIENT_IPONLY_SET
    • FLOW_PKT_TOSERVER_FIRST
    • FLOW_PKT_TOCLIENT_FIRST
  • flags
    标识了当前Packet的flag,后面列举了各个flag。
  • flow
    关联的flow。
  • flow_hash
    数据包匹配相应的flow时使用的hash值,tcp/udp/sctp/icmp会设定该项。使用源目的地址、源目的端口、协议号、隧道递归值、vlan_id数组、系统初始化随机数计算得来。
  • ts
    记录了数据包的时间。
  • nflog_v
  • nfq_v
  • ipfw_v
  • afp_v
  • mpipe_v
  • netmap_v
  • pcap_v
    上面几个用于记录收包阶段时各个收包模块特有的数据。
  • ReleasePacket
    函数指针,当数据包需要被释放时调用。可能是释放回原线程的PktPool中,也可能是直接释放内存。
  • BypassPacketsFlow
    TODO
  • pktvar
    TODO
  • ethh
    二层以太网包头指针。
  • level3_comp_csum
  • level4_comp_csum
    TODO
  • ip4h
    三层ipv4包头指针。
  • ip6h
    三层ipv6包头指针。
  • ip4vars
    保存了ipv4的可选项信息,如果存在可选项则写入该成员。
    • opt_cnt
      保存了可选项的数量。
    • opts_set
      保存了有哪些可选项的标记。以IPV4_OPT_FLAG_开头的一组枚举型标识。
  • ip6vars
  • ip6eh
    这两个成员组成了一个结构体,这个结构体与ip4vars组成联合体,可以看出是ipv6的扩展选项,目前不关注。
  • tcpvars
  • icmpv4vars
  • icmpv6vars
    四层协议的扩展项。
  • tcph
    四层tcp协议头。
  • udph
    四层udp协议头。
  • sctph
    四层sctp协议头。
  • icmpv4h
    四层icmpV4协议头。
  • icmpv6h
    四层icmpV6协议头。
  • ppph
    二层ppp协议头。上层支持ipv4与ipv6协议。
  • pppoesh
    pppoe会话协议头。位于二层以太网之上,内部封装支持ipv4和ipv6。
  • pppoedh
    pppoe发现协议头。位于二层以太网之上。
  • greh
    gre封装协议头。位于三层ipv4之上,内部封装支持的比较多。
  • vlanh
    长度为2的数组,vlan头指针,与上面的vlan_idx组合,与vlan_id数组对应。
  • payload
    四层数据包的载荷,比如tcp或udp协议内的载荷。
  • payload_len
    四层数据包的载荷长度。
  • action
    IPS模式下,标识了要对这个数据包做的处理动作。动作定义在action-globals.h中。
  • pkt_src
    标识数据包的来源。比如PKT_SRC_WIRE表示直接收到的包,PKT_SRC_DECODER_GRE表示是由GRE解封装得到的伪造数据包。
  • pktlen
    收取的数据包字节长度
  • ext_pkt
    这个成员有一点特殊。Packet结构体在创建并分配内存时预分配了更多的default_packet_size字节的内存,也就是说当收取的数据包长度不大于default_packet_size时可以使用Packet结构体后的内存空间用以拷贝存储数据包数据。但是当数据包长度超出了default_packet_size时,将分配一块MAX_PAYLOAD_SIZE大小的内存块,地址赋值给ext_pkt成员,用以拷贝存储数据包数据,当数据包内存释放或归还到池子后重用时ext_pkt指向的内存将释放。另外如果数据包是使用零拷贝映射的,那么ext_pkt成员用于指向数据包内存地址,这时该指向的内存不能由用户释放。
  • livedev
    指向数据包来源的一个LiveDevice结构。
  • alerts
    TODO
  • host_src
  • host_dst
    TODO
  • pcap_cnt
    pcap文件中的数据包序号。
  • events
    记录解码、分片重组、和stream时的event。event类型记录在decode-events.h中。
    • cnt
      当前记录了的event的数量。
    • events
      PACKET_ENGINE_EVENT_MAX长度的数组,顺序记录event
  • app_layer_events
    TODO
  • next
  • prev
    next和prev两个成员可以将Packet链接起来,PktPool和PacketQueue结构都有使用。
  • datalink
    标识了收包设备的二层类型。决定了收取数据包首部的header结构类型。
  • root
    如果一个数据包是隧道数据包或封装数据包,解封装后会生成一个新的伪造数据包进入后续模块处理,这时新的伪造数据包的root成员会设置为指向最原始的那个数据包,也就是说如果是多层封装,那么后续的解封装后的伪造数据包root成员们都会指向最底层的那一个原始数据包。
  • tunnel_mutex
    锁,用于保护另外两个成员tunnel_rtv_cnt和tunnel_tpr_cnt的并发访问。
  • tunnel_rtv_cnt
    标识这个数据包做为其他数据包的root时,生成的新伪造数据包处理完成被回收(retrieve)的个数。只增不减。
  • tunnel_tpr_cnt
    标识这个数据包作为其他数据包的root被引用的次数,与root和tunnel_rtv_cnt配置实用。只增不减。
  • tenant_id
    TODO
  • pool
    这个数据包所属的PktPool,在释放时会返回挂载到这个池子中。
  • profile
  • cuda_pkt_vars
  • ntpv
    TODO
Packet中flags成员可能的选项
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
/*Packet Flags*/
#define PKT_NOPACKET_INSPECTION (1) /**< Flag to indicate that packet header or contents should not be inspected*/
#define PKT_NOPAYLOAD_INSPECTION (1<<2) /**< Flag to indicate that packet contents should not be inspected*/
#define PKT_ALLOC (1<<3) /**< Packet was alloc'd this run, needs to be freed */
#define PKT_HAS_TAG (1<<4) /**< Packet has matched a tag */
#define PKT_STREAM_ADD (1<<5) /**< Packet payload was added to reassembled stream */
#define PKT_STREAM_EST (1<<6) /**< Packet is part of establised stream */
#define PKT_STREAM_EOF (1<<7) /**< Stream is in eof state */
#define PKT_HAS_FLOW (1<<8)
#define PKT_PSEUDO_STREAM_END (1<<9) /**< Pseudo packet to end the stream */
#define PKT_STREAM_MODIFIED (1<<10) /**< Packet is modified by the stream engine, we need to recalc the csum and reinject/replace */
#define PKT_MARK_MODIFIED (1<<11) /**< Packet mark is modified */
#define PKT_STREAM_NOPCAPLOG (1<<12) /**< Exclude packet from pcap logging as it's part of a stream that has reassembly depth reached. */

#define PKT_TUNNEL (1<<13)
#define PKT_TUNNEL_VERDICTED (1<<14)

#define PKT_IGNORE_CHECKSUM (1<<15) /**< Packet checksum is not computed (TX packet for example) */
#define PKT_ZERO_COPY (1<<16) /**< Packet comes from zero copy (ext_pkt must not be freed) */

#define PKT_HOST_SRC_LOOKED_UP (1<<17)
#define PKT_HOST_DST_LOOKED_UP (1<<18)

#define PKT_IS_FRAGMENT (1<<19) /**< Packet is a fragment */
#define PKT_IS_INVALID (1<<20)
#define PKT_PROFILE (1<<21)

/** indication by decoder that it feels the packet should be handled by
* flow engine: Packet::flow_hash will be set */
#define PKT_WANTS_FLOW (1<<22)

/** protocol detection done */
#define PKT_PROTO_DETECT_TS_DONE (1<<23)
#define PKT_PROTO_DETECT_TC_DONE (1<<24)

#define PKT_REBUILT_FRAGMENT (1<<25) /**< Packet is rebuilt from
* fragments. */
#define PKT_DETECT_HAS_STREAMDATA (1<<26) /**< Set by Detect() if raw stream data is available. */

#define PKT_PSEUDO_DETECTLOG_FLUSH (1<<27) /**< Detect/log flush for protocol upgrade */

收包

以pcap和af-packet实时嗅探为例,收包过程对Packet的填充如下。

  • 取得一个空的Packet实例。从线程专有的PktPool中取得,池子空时新分配内存,这个新分配的Packet内存在使用结束后将直接释放。
  • 设置Packet的pkt_src成员为PKT_SRC_WIRE,表示由设备线路获取。
  • 设置Packet的ts成员记录时间。
  • 设置Packet的datalink成员记录二层链路层类型。
  • 设置Packet的livedev成员,指向收包设备LiveDevice实例。
  • 如果使用的收包设备接口在接收到的数据包头部支持提取vlan信息,则设置Packet的vlan_id、vlan_idx成员。
  • 设置Packet的pktlen成员,记录收取数据包长度。
  • 设置Packet的数据包原始数据。
    • 如果是零拷贝方式,设置Packet的ext_pkt成员指向映射内存的数据包头地址,同时设置flags成员增加标识PKT_ZERO_COPY。
    • 如果需要拷贝数据,根据情况选择是将数据拷贝到Packet内存尾部还是分配内存给ext_pkt成员用以拷贝数据。
  • 根据情况选择是否为Packet的flags成员设定标识PKT_IGNORE_CHECKSUM。如果符合忽略数据包校验的条件,会置PKT_IGNORE_CHECKSUM标记。比如配置文件中的checksum-checks设置为no,或设置auto的同时统计了足够比例的校验失败数据包。
  • 如果收包模块一段时间没有收到数据包,并且detect engine需要reload时,收包模块会调用TmThreadsCaptureInjectPacket函数,创建一个伪造数据包并将flasg成员增加PKT_PSEUDO_STREAM_END标记,数据包将交付给后续模块处理,这个数据包会流经整个引擎并使detect engine完成reload过程。
  • 收包模块生成的Packet由TmThreadsSlotProcessPkt函数交由后续模块处理。参考之前的线程模型介绍

解码

以pcap和af-packet实时嗅探为例,解码过程对Packet的填充如下。

  • 检查Packet中PKT_PSEUDO_STREAM_END标记,如果存在则直接完成decode过程进入下一阶段。
  • 判断Packet中datalink,选择不同的解码函数,各个链路类型的解码函数对于不同的decode模块是通用的,也就是说各个receive模块对应了自己的decode模块,但是这个decode模块是很轻的,因为主要的解码函数是通用的。
    pcap和af-packet支持的二层链路类型相同,以下。
    • LINKTYPE_LINUX_SLL
    • LINKTYPE_ETHERNET
    • LINKTYPE_PPP
    • LINKTYPE_RAW
    • LINKTYPE_NULL

下面记录部分常用协议的解码。

链路层解码

DecodeEthernet

  • 设置Packet成员ethh,类型为EthernetHdr指针。
  • 判断以太网头中的类型字段,选择上层数据解码函数,支持以下。
    • ETHERNET_TYPE_IP
    • ETHERNET_TYPE_IPV6
    • ETHERNET_TYPE_PPPOE_SESS
    • ETHERNET_TYPE_PPPOE_DISC
    • ETHERNET_TYPE_VLAN
    • ETHERNET_TYPE_8021QINQ
    • ETHERNET_TYPE_MPLS_UNICAST
    • ETHERNET_TYPE_MPLS_MULTICAST
    • ETHERNET_TYPE_DCE
以太网包头结构
1
2
3
4
5
typedef struct EthernetHdr_ {
uint8_t eth_dst[6];
uint8_t eth_src[6];
uint16_t eth_type;
} __attribute__((__packed__)) EthernetHdr;

以太网帧格式

DecodeSll

  • 通过头部内存SllHdr结构选择上层数据解码函数,支持以下。
    • ETHERNET_TYPE_IP
    • ETHERNET_TYPE_IPV6
    • ETHERNET_TYPE_VLAN
libpcap可能收取到此种链路层头的数据包,应该是cooked数据包。
1
2
3
4
5
6
7
typedef struct SllHdr_ {
uint16_t sll_pkttype; /* packet type */
uint16_t sll_hatype; /* link-layer address type */
uint16_t sll_halen; /* link-layer address length */
uint8_t sll_addr[8]; /* link-layer address */
uint16_t sll_protocol; /* protocol */
} __attribute__((__packed__)) SllHdr;

DecodePPP

  • 设置Packet成员ppph,类型为PPPHdr指针。
  • 判断头中协议字段,选择上层解码函数,支持以下。
    • PPP_VJ_UCOMP
      ipv4解码
    • PPP_IP
      ipv4解码
    • PPP_IPV6
      ipv6解码
PPP包头结构
1
2
3
4
5
6
/** PPP Packet header */
typedef struct PPPHdr_ {
uint8_t address;
uint8_t control;
uint16_t protocol;
} __attribute__((__packed__)) PPPHdr;

DecodeRaw

这里数据包直接就是三层数据包,该函数只支持ipv4与ipv6,通过判断头部版本号区分。

DecodeNull

二层链路类型为LINKTYPE_NULL时使用此解码函数,数据包首部4字节代表了上层数据的协议族,比如PF_INET、PF_INET6,这里字节序使用的是主机序,比如x86为小端序。这里只支持ipv4与ipv6。

DecodeVLAN

  • 设置Packet成员vlanh,根据vlan_idx选择vlanh数组中的项。
  • 设置Packet成员vlan_id,根据vlan_idx选择vlan_id数组中的项。
  • 加一Packet成员vlan_idx,标记当前vlan层数。suricata中只允许两层。
  • 判断vlan头中协议类型字段,选择上层解码函数,支持以下。
    • ETHERNET_TYPE_IP
    • ETHERNET_TYPE_IPV6
    • ETHERNET_TYPE_PPPOE_SESS
    • ETHERNET_TYPE_PPPOE_DISC
    • ETHERNET_TYPE_VLAN
    • ETHERNET_TYPE_8021AD
    • ETHERNET_TYPE_8021AH

vlan帧格式

网络层解码

DecodeIPV4

  • 设置Packet成员ip4h。类型为IPV4Hdr指针。
  • 设置Packet成员src、dst,写入地址族类型及地址。
  • 设置Packet成员ip4vars(如果存在ipv4协议可选项的话)。
  • 设置Packet成员proto,记录ipv4载荷协议类型。可能是tcp、udp这类传输层协议,也可能是GRE这类隧道协议。
  • 如果这是一个IP分片,进入分片重组流程,增加flags标记PKT_IS_FRAGMENT,直接返回不再继续进行载荷解码。(如果这里重组完成了一个新的伪造数据包,则将数据包置入模块的slot_pre_pq队列中。)
  • 如果不是一个IP分片,判断载荷协议类型,选择上层解码函数,支持以下。
    • IPPROTO_TCP
    • IPPROTO_UDP
    • IPPROTO_ICMP
    • IPPROTO_GRE
    • IPPROTO_SCTP
    • IPPROTO_IPV6
    • IPPROTO_IP
      看注释这里与ppp协议有关,载荷按照tcp解码。不了解ppp协议,不关注这里。
ipv4头结构体
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
typedef struct IPV4Hdr_
{
uint8_t ip_verhl; /**< version & header length */
uint8_t ip_tos; /**< type of service */
uint16_t ip_len; /**< length */
uint16_t ip_id; /**< id */
uint16_t ip_off; /**< frag offset */
uint8_t ip_ttl; /**< time to live */
uint8_t ip_proto; /**< protocol (tcp, udp, etc) */
uint16_t ip_csum; /**< checksum */
union {
struct {
struct in_addr ip_src;/**< source address */
struct in_addr ip_dst;/**< destination address */
} ip4_un1;
uint16_t ip_addrs[4];
} ip4_hdrun1;
} __attribute__((__packed__)) IPV4Hdr;

ipv4头格式

DecodeIPV6

IPv6协议不够熟悉,而且对这方面需求不大,暂时不做分析。

传输层解码

DecodeTCP

  • 设置Packet成员tcph。类型为TCPHdr指针。
  • 设置Packet成员tcpvars(如果存在tcp可选项的话)。
  • 设置Packet成员sp、dp,tcp源端口目的端口。
  • 再次设置Packet成员proto为IPPROTO_TCP。
  • 设置Packet成员payload,指向tcp载荷数据。
  • 设置Packet成员payload_len,tcp载荷数据长度。
  • 设置Packet成员flags增加标记PKT_WANTS_FLOW。
  • 设置Packet成员flow_hash。
tcp头结构体
1
2
3
4
5
6
7
8
9
10
11
12
typedef struct TCPHdr_
{
uint16_t th_sport; /**< source port */
uint16_t th_dport; /**< destination port */
uint32_t th_seq; /**< sequence number */
uint32_t th_ack; /**< acknowledgement number */
uint8_t th_offx2; /**< offset and reserved */
uint8_t th_flags; /**< pkt flags */
uint16_t th_win; /**< pkt window */
uint16_t th_sum; /**< checksum */
uint16_t th_urp; /**< urgent pointer */
} __attribute__((__packed__)) TCPHdr;

tcp头格式

DecodeUDP

  • 设置Packet成员udph,类型为UDPHdr指针。
  • 设置Packet成员sp、dp,udp源端口目的端口。
  • 设置Packet成员payload,指向udp载荷数据。
  • 设置Packet成员payload_len,udp载荷数据长度。
  • 再次设置Packet成员proto为IPPROTO_UDP。
  • 这里会判断是否是Teredo隧道包,我暂时不关注Teredo包的处理。
  • 如果非Teredo协议数据包
    • 设置Packet成员flags增加标记PKT_WANTS_FLOW。
    • 设置Packet成员flow_hash。
udp头结构体
1
2
3
4
5
6
7
8
/* UDP header structure */
typedef struct UDPHdr_
{
uint16_t uh_sport; /* source port */
uint16_t uh_dport; /* destination port */
uint16_t uh_len; /* length */
uint16_t uh_sum; /* checksum */
} __attribute__((__packed__)) UDPHdr;

udp头格式

DecodeICMPV4

  • 设置Packet成员icmpv4h。类型为ICMPV4Hdr指针。
  • 再次设置Packet成员proto为IPPROTO_ICMP。
  • 设置Packet成员type为icmpv4h中的type。
  • 设置Packet成员code为icmpv4h中的code。
  • 设置Packet成员payload,指向icmpv4载荷。
  • 设置Packet成员payload_len,icmpv4载荷长度。
  • 根据type类型设置Packet成员icmpv4vars。
icmpv4结构体
1
2
3
4
5
6
7
/* ICMPv4 header structure */
typedef struct ICMPV4Hdr_
{
uint8_t type;
uint8_t code;
uint16_t checksum;
} __attribute__((__packed__)) ICMPV4Hdr;

icmp头格式

隧道数据包

隧道数据包处理的核心逻辑就是,对封装的载荷创建一个新的Packet结构并合理填充,将其放入模块的slot_pre_pq队列。原始数据包标记为不需要继续分析,然后结束原始数据包的解码。下面记录一些填充细节。

GRE

通用路由封装协议是一种隧道协议,可以用于将网络层数据报封装起来,使这些被封装的数据报能在IPv4网络上传输。

DecodeGRE

  • 设置Packet成员greh,类型为GREHdr指针。
  • 判断GRE版本号,做一系列校验。同时根据各个选项,确定gre头占用的确定长度。
  • 判断GRE内部承载的协议类型,调用PacketTunnelPktSetup函数,这个函数将返回一个新的Packet结构,我们可以命名为tp(tunnel packet)。
  • 如果返回的tp不为NULL,设置tp的成员pkt_src为PKT_SRC_DECODER_GRE,表示这个数据包是由gre隧道解封装得来的,是一个新建的伪造数据包。
  • 将tp放入模块的slot_pre_pq队列,入队列后的处理逻辑参考之前的线程模型介绍

针对隧道数据包的处理都经由PacketTunnelPktSetup函数,这里记录该函数的主要逻辑。原数据包我们命名为parent,新数据包延续前文命名为tp。

  • 取得一个新的空Packet,tp。
  • 将parent中的载荷数据拷贝到tp中,同时设置tp的成员pktlen。
  • 设置tp成员recursion_level为parent中的同名成员加一。
  • 设置tp成员ts与parent中的值一致。
  • 设置tp成员datalink为DLT_RAW,这表示载荷中没有链路层头,直接就是网络层头。
  • 设置tp成员tenant_id与parent中的值一致。
  • 设置tp成员root,设置为最底层的root。也就是说如果父数据包root不为NULL则设置为父的root,否则设置为父数据包。这里另外说一句,root指向的Packet的tunnel_tpr_cnt将加一,用以标识有多少个数据包引用了它。
  • 设置tp成员flags增加标记PKT_TUNNEL。
  • 对tp调用函数DecodeTunnel。
  • 设置parent成员flags增加标记PKT_TUNNEL。
  • 设置root中的tunnel_tpr_cnt加一,用以记录root数据包被引用的次数。
  • 设置parent成员flags增加标记PKT_NOPAYLOAD_INSPECTION。
  • 返回tp。

函数DecodeTunnel很简单,根据传入的DecodeTunnelProto类型的参数proto,选择具体的解码函数继续填充新建的伪造数据包tp。比如参数值DECODE_TUNNEL_IPV4对应的解码函数为DecodeIPV4。这里支持的参数如下。

  • DECODE_TUNNEL_ETHERNET,
  • DECODE_TUNNEL_ERSPAN
  • DECODE_TUNNEL_VLAN
  • DECODE_TUNNEL_IPV4
  • DECODE_TUNNEL_IPV6
  • DECODE_TUNNEL_PPP
GREHdr结构体
1
2
3
4
5
6
7
typedef struct GREHdr_
{
uint8_t flags; /**< GRE packet flags */
uint8_t version; /**< GRE version */
uint16_t ether_type; /**< ether type of the encapsulated traffic */

} __attribute__((__packed__)) GREHdr;

gre头格式

IPv6 over IPv4

IPv6 over IPv4的解码逻辑与gre解码逻辑几乎一样,同样都由调用PacketTunnelPktSetup生成解封装后的数据包然后放入模块的slot_pre_pq队列,区别只在于两层间少了解码gre头这一个步骤。

其他隧道模式同理。

分片数据包

这里仅关注IPv4的分片处理。IPv6不做关注。

分片数据包处理围绕tracker进行,一个tracker保存了该ip数据包的所有分片(frag),使用中的tracker保存在一个hash table中,hash冲突使用链表解决,在这里链表被称为行,空闲的tracker都保存在一个队列中。frag拷贝了Packet中的数据,空闲的frag保存在一个池子中,这个池子也是全局的。tracker和frag的操作都可能多线程并发,因此都有锁保护。多线程锁的使用和frag的数据拷贝可能影响suricata性能。

分片相关初始化

main -> PostConfLoadedSetup -> PreRunInit -> DefragInit。这里是分片相关的初始化运行位置。

  • 依据配置文件中defrag.host-config段,以ip网段为key,以timeout为value,插入一个基数树种,树根为静态全局变量defrag_tree。
  • defrag_context静态全局变量初始化。
    • frag_pool。存储了空闲frag的池子,池子数量上限为配置文件项defrag.max-frags(默认65534),池子预先创建了数量上限一半的frag内存。
    • frag_pool_lock。并发访问用的保护锁。
    • timeout。tracker的过期时间,配置文件项defrag.timeout(默认60秒)。
  • default_timeout静态环境变量值设置为上面的timeout,用于host的默认timeout。
  • defrag_config静态全局变量初始化。
    • hash_rand。随机数。
    • hash_size。tracker使用的hash table的行数。配置文件defrag.hash-size,默认4096。
    • memcap。tracker使用的内存上限。配置文件defrag.memcap,默认16M。
    • prealloc。tracker预分配数量。配置文件defrag.trackers,默认1000。
  • defragtracker_hash。tracker的全局hash table。分配内存并置0,初始化每行的锁。
  • defragtracker_counter。使用中的tracker计数。
  • defrag_memuse。tracker和其hash table所使用的内存总量。跟随hash table及tracker的预创建而变动。
  • defragtracker_prune_idx。从使用中的tracker中选出一个清空并再次使用时,使用该值选择遍历开始的hash table行号。
  • defragtracker_spare_q。空闲tracker队列。配置文件项defrag.prealloc为真时将预分配defrag_config.prealloc个数的tracker到这个空闲队列。

分片数据包处理

  • 在DecodeIPV4解码中,会检查ipv4头的offset和mf(more fragment)标记
  • 如果是分片数据包,调用函数Defrag,这个函数将返回一个新的Packet结构,我们可以命名为rp(reassembly packet)。
  • 如果rp不为NULL,则放入模块的slot_pre_pq队列。
  • 原始数据包增加标记PKT_IS_FRAGMENT。

分片数据包的处理逻辑集中在函数Defrag内,这个函数是ipv4与ipv6通用的。

  • 调用DefragGetTracker获得数据包的tracker,如果tracker为NULL则返回NULL。
  • 调用DefragInsertFrag插入数据包到tracker中,如果在加入此数据包后分片重组成功则函数返回rp,既重组完成的Packet。如果返回NULL则说明重组没有完成。
  • 调用DefragTrackerRelease释放tracker。

DefragGetTracker

tracker有一个空闲链表,在使用的tracker放在一个hash table中,table中每一行对应一个hash值,Packet依次比较这一行中的tracker,匹配的tracker移动到队列头。行有锁,tracker有锁。

查找一个合适的tracker时,先从hash table中取得行,对该行上锁。hash key使用Packet的源目的地址、ip包id、vlan_id、系统初始化的随机数,这5项计算得到。

  • 如果该行空,则尝试获取一个新tracker。
    • 如果没能拿到新tracker,则释放行锁,返回NULL。
    • 如果拿到新tracker,则将该tracker插入行,对tracker调用DefragTrackerInit初始化数据,释放行锁,返回tracker。
  • 如果该行不空,则顺序比较该行的每个tracker
    • 如果匹配到tracker,将该tracker移动到行首,锁定该tracker,对tracker成员use_cnt加一,释放行锁,返回tracker。
    • 如果到末尾仍没有匹配到,则尝试获取新tracker。
      • 如果没能拿到新tracker,则释放行锁,返回NULL。
      • 如果拿到新tracker,将该tracker挂载到行尾部,对tracker调用DefragTrackerInit进行初始化数据,释放行锁,返回tracker。(这里很奇怪,匹配到的活跃tracker移动到头部,新的活跃tracker却挂载到尾部,我估计是忘改了)

尝试获取新tracker的流程如下:

  • 从空闲链表中尝试获取tracker。
    • 如果没取到,检查当前全局tracker内存用量是否达到配置限额。
      • 内存使用未达到限额,尝试分配内存。
        • 再次检查,如果仍未达到限额则分配内存,增加全局tracker内存用量,对tracker内存置零,初始化锁,返回tracker。如果检查达到限额则返回NULL(这里完全是吃饱了撑的)
      • 尝试从在使用中的tracker中清理出一个。
        • 从defragtracker_prune_idx所以开始遍历tracker的hash table的每一行。
        • 尝试对行上锁,如果上锁失败则尝试下一行。
        • 上锁成功后,检查是不是空行,如果是空行则释放行锁尝试下一行。
        • 尝试上锁行尾的tracker,如果上锁失败则释放行锁尝试下一行。
        • 上锁tracker成功后,检查track成员use_cnt,如果不大于零则将tracker从行上移除,释放行锁,将该tracker上的分片frag释放回池子,释放tracker锁,增加defragtracker_prune_idx值,增加的值为尝试的行数,返回tracker。
        • 如果尝试了所有行仍未成功取到tracker,则返回NULL。
    • 如果前面的操作有取到tracker,增加全局的tracker使用计数,上锁并返回该tracker。

DefragTrackerInit初始化tracker数据:

  • 设置tracker成员源地址目的地址。
  • 设置tracker成员id为ip的id。
  • 设置tracker成员af为地址族。
  • 设置tracker成员proto为ip数据包载荷的协议。
  • 设置tracker成员vlan_id数组。
  • 设置tracker成员policy,该tracker所匹配上的host-os,也就是认为这个tracker追踪的数据包归属于哪种操作系统。匹配规则来源于配置文件项host-os-policy。
  • 设置tracker成员host_timeout,该tracker的timeout值,从前文提过的defrag_tree得到,有默认值。
  • 设置tracker成员remove和seen_last都置0.
  • 初始化tracker成员frags队列。
  • 设置tracker成员use_cnt加一。

DefragInsertFrag

  • 检查分片重组后长度如果超过允许的IP包最大长度,则返回NULL。
  • 更新tracker成员timeout,每次有新分片到来都会更新此项。
  • 根据tracker成员policy,检查该Packet与已经保存的前后分片的数据是否有相互覆盖,更新相关frag成员的skip项,如果本数据包数据有需要做ltrim(左端一定长度的数据无用)的,记录需要ltrim的数据。
  • 从frag池子中取出一个新的frag,如果取出NULL,则返回NULL。
  • 设置frag成员pkt,分配当前Packet数据长度的内存,如果失败返还frag并返回NULL。
  • 将Packet中去掉ltrim部分的数据拷贝到frag成员pkt中。
  • 设置frag成员len。
  • 设置frag成员hlen、offset、data_offset、data_len、ip_hdr_offset、frag_hdr_offset、more_frags。
  • 将该frag插入tracker成员frags上,这里根据frag的offset顺序排列。
  • 如果more_frag为假,设置tracker成员seen_last为一。
  • 如果tracker的seen_last是一,IPv4协议的话将调用Defrag4Reassemble尝试重组分片,这个函数返回Packet类型指针。
  • 如果上一步返回非NULL,则调用DecodeIPV4对重组数据包做解码。解码成功后对原数据包调用函数PacketDefragPktSetupParent,增加Packet成员flags标记PKT_TUNNEL和PKT_NOPAYLOAD_INSPECTION,增加成员tunnel_tpr_cnt计数。
  • 如果之前重组分片返回了Packet指针,则将其返回,否则返回的将是NULL。

分片重组工作在函数Defrag4Reassemble中进行。

  • 检查有没有seen_last,既有没有接收到最后一个分片。
  • 检查分片见有没有空洞。
  • 取得一个新的Packet,这里命名为rp。以原始数据包为参考,设置rp成员,包括:
    • root,这个参考隧道数据包的root,逻辑一致。
    • recursion_level,与原始数据包一致,不做增加。
    • ts,与原始数据包一致。
    • datalink,设置为DLT_RAW。
    • tenant_id,与原始数据包一致。
    • flags,增加PKT_TUNNEL标记。
    • vlan_id,vlan_idx,与原始数据包一致。
  • 如果取得rp失败,进入清空tracker错误处理流程。
  • 设置rp成员pkt_src为PKT_SRC_DEFRAG,记录其来源是分片重组。
  • 设置rp成员flags,增加PKT_REBUILT_FRAGMENT标记。
  • 按顺序将tracker上挂载的frag中的数据拷贝到rp成员ext_pkt合适的位置上。
  • 设置rp成员ip4h。ip4h中的ip_len、ip_off、ip_csum需要重新计算。
  • 设置rp成员pktlen。
  • 设置tracker成员remove置1。
  • 调用DefragTrackerFreeFrags释放tracker及其中的frag。
  • 返回rp。

函数Defrag4Reassemble的清空tracker错误处理流程比较简单:

  • tracker成员remove置1。
  • 调用函数DefragTrackerFreeFrags清空tracker中的所有frag。顺序移除所有的frag,frag的成员pkt释放内存,frag置0,将frag放回池子。
  • 如果之前已经取得了rp,将rp释放,具体是释放内存还是放回池子取决于rp的flags与释放函数。

DefragTrackerRelease

这里只做了两步操作。

  • 对tracer的use_cnt项减一。
  • 释放tracker的锁。