1 概述
2 doc/README.dissector
此文档给出了开发协议解析器的有用信息. :::warning
- 我不想只是翻译这个文件的内容, 会加入自己的注解
此文件有很长篇幅在介绍解析器可以调用的 Wireshark 函数, 这部分我会挑一些重点的介绍, 其余从略 :::
2.0 准备
开发新的解析器之前, 需要准备好 Wireshark 编译环境, 因为并不存在”解析器编译工具”这样的东西.
如何设置编译环境视操作系统而定, 详见开发手册, 以及源码主目录内的INSTALL
,README.md
等文件.2.0.1 解析器相关README文件
下面这些README文件也提供了有关解析器开发的信息:
README.heuristic - what are heuristic dissectors and how to write them
- README.plugins - how to “pluginize” a dissector
- README.request_response_tracking - how to track req./resp. times and such
- README.wmem - how to obtain “memory leak free” memory
2.1 配置解析器代码
2.1.1 骨架代码
// TODO2.1.2 骨架代码中需要替换的部分
// TODO
2.1.3 解析器及其收到的数据
头文件
只有当不使用一般注册方式, 或者需要给其他解析器暴露接口的解析器, 才需要头文件. 解析器必须在头文件 packet-PROTOABBREV.h
中声明:
int
dissect_PROTOABBREV(tvbuff_t *tvb, packet_info *pinfo, proto_tree *tree);
从报文中获取数据
注意: 可参考 epan/tvbuff.h 来理解.
tvb 指向包含将被解析器分析的原始数据的 buffer; 比如, 对于 UDP 上层协议, 它包含 UDP payload (而非 UDP 首部, 或者之上任何协议首部). tvbuffer 实现为不透明数据结构, 其内部实现是隐藏的, 必须通过 tvbuffer 函数接口来访问 (如 tvb_get_XXX()
系列函数).
README.dissector 文件对于这些函数接口给出了长篇幅的详细讲解, 这里从略.
2.1.4 处理流量概要窗口中的列
// TODO
col_set_str函数
col_add_str函数
col_add_fstr函数
col_clear函数
col_append_str函数
col_append_fstr函数
col_append_sep_str和col_append_sep_fstr函数
col_set_fence和col_prepend_fence_fstr函数
col_set_time函数
2.1.5 构造协议树
协议树是通过 GLib 的 GNode 实现的, 是一种 N 叉树. 协议解析器并不关心协议树的实际实现, 它只是作为一个参数传入解析函数, 用于添加新的结点或分支.
当用户在报文列表中点击某个报文, 或报文弹出窗口创建时, 就会创建新的协议树. 之后, 协议树传递给最上层解析器, 然后再传递给后续解析器. 最后调用 proto_tree_draw() 绘制 GUI 树.
// TODO
字段注册
将项和值添加到协议树
2.1.6 实用例程
val_to_str, val_to_str_const, try_val_to_str and try_val_to_str_idx
// TODO
rval_to_str, try_rval_to_str and try_rval_to_str_idx
// TODO
2.1.7 调用其他解析器
每个解析器在做完自己的解析工作后, 将创建 TVBUFF_SUBSET
类型的新 tvbuff, 它包含后续解析器要处理的 payload. 创建的接口有:
// next_tvb包含的数据从offset开始直到结尾
next_tvb = tvb_new_subset_remaining(tvb, offset);
// 当前解析器报告payload长度为reported_length
next_tvb = tvb_new_subset_length(tvb, offset, reported_length);
// length显式指定next_tvb的数据长度, -1表示尽可能多
// reported_length为-1表示当前解析器不知道payload长度
next_tvb = tvb_new_subset_length_caplen(tvb, offset, length, reported_length);
要调用解析器之前, 需要通过 find_dissector()
接口获取解析器句柄, 参数为解析器名称. 这一工作通常是在初始化阶段, 在解析器的 proto_reg_handoff_XXX()
函数中完成的. 示例代码(epan/dissectors/packet-tcp.c):
static dissector_handle_t data_handle;
static dissector_handle_t sport_handle;
void
proto_reg_handoff_tcp(void)
{
...
data_handle = find_dissector("data");
sport_handle = find_dissector("sport");
...
}
解析器表
调用其他解析器的另一个方式是设置解析器表. 解析器表由根据通用标识符(整数或字符串)分组的多个解析器构成. 后续解析器通过以下接口将自己注册到上游解析器的解析器表中:
void dissector_add_uint(const char *abbrev, const guint32 pattern,
dissector_handle_t handle);
void dissector_add_uint_range(const char *abbrev, struct epan_range *range,
dissector_handle_t handle);
void dissector_add_string(const char *name, const gchar *pattern,
dissector_handle_t handle);
void dissector_add_for_decode_as(const char *name,
dissector_handle_t handle);
当解析器命中通用标识符时(比如 TCP 端口 == 80), 就会调用下列接口来调用下游解析器(如 HTTP):
int dissector_try_uint(dissector_table_t sub_dissectors,
const guint32 uint_val, tvbuff_t *tvb, packet_info *pinfo,
proto_tree *tree);
int dissector_try_uint_new(dissector_table_t sub_dissectors,
const guint32 uint_val, tvbuff_t *tvb, packet_info *pinfo,
proto_tree *tree, const gboolean add_proto_name, void *data);
int dissector_try_string(dissector_table_t sub_dissectors, const gchar *string,
tvbuff_t *tvb, packet_info *pinfo, proto_tree *tree, void *data);
2.1.8 编辑CMakeLists.txt,添加新解析器
修改 epan/dissectors/CMakeLists.txt 文件中的 DISSECTOR_SRC
, 加入新解析器的源码文件名.
2.1.9 使用git源码树
见 https://www.wireshark.org/develop.html
2.1.10 提交新解析器代码
// TODO
2.2 高级解析器主题
2.2.1 介绍
某些高级特性一直在不断变化, 用到它们的时候, 明智的做法是查看其相关源代码.
2.2.2 追踪会话
// TODO
会话例程
conversation_init
conversation_new
find_conversation
find_conversation_pinfo
find_or_create_conversation
conversation_add_proto_data
给会话关联协议数据:
void conversation_add_proto_data(conversation_t *conv, int proto,
void *proto_data);
proto_data
一般是指向 wmem_alloc’d 内存, 这种内存会在每次新的解析开始时(// TODO 例如每次打开文件时?)自动释放, 并不需要解析器操心. proto
是注册解析时生成的唯一 ID, 也代表多个解析器都可以向同一会话添加协议数据.
conversation_get_proto_data
取得会话中保存的协议数据.
conversation_delete_proto_data
删除会话中保存的协议数据.
:::warning
调用此接口只会删除会话表项, 如果用非 wmem_alloc 的方式分配了协议数据内存, 需要自己释放
:::
conversation_set_dissector
设置会话参数(地址, 端口类型, 端口等)满足某个条件时, 调用某个解析器.
void conversation_set_dissector(conversation_t *conversation, const dissector_handle_t handle);
时间戳
// TODO
使用wmem_file_scope的会话示例代码
对于 IP/Port 四元组会话, 可以使用此示例代码. 此代码使用文件作用域内存池 (wmem_file_scope()
)来分配协议数据内存, 并关联到会话:
/************************ Global values ************************/
/* define your structure here */
typedef struct {
...
} my_entry_t;
/* Registered protocol number */
static int my_proto = -1;
/********************* in the dissector routine *********************/
/* the local variables in the dissector */
conversation_t *conversation;
my_entry_t *data_ptr;
/* look up the conversation */
conversation = find_conversation(pinfo->num, &pinfo->src, &pinfo->dst,
pinfo->ptype, pinfo->srcport, pinfo->destport, 0);
/* if conversation found get the data pointer that you stored */
if (conversation)
data_ptr = (my_entry_t*)conversation_get_proto_data(conversation, my_proto);
else {
/* new conversation create local data structure */
data_ptr = wmem_alloc(wmem_file_scope(), sizeof(my_entry_t));
/*** add your code here to setup the new data structure ***/
/* create the conversation with your data pointer */
conversation = conversation_new(pinfo->num, &pinfo->src, &pinfo->dst, pinfo->ptype,
pinfo->srcport, pinfo->destport, 0);
conversation_add_proto_data(conversation, my_proto, (void *)data_ptr);
}
/* at this point the conversation data is ready */
/***************** in the protocol register routine *****************/
my_proto = proto_register_protocol("My Protocol", "My Protocol", "my_proto");
开始于特定报文序号的会话示例代码
当全局有多个源/目的 ip/port 对相同的会话时, 有时解析器需要确定会话是否从某个报文开始. 可通过比较 conversation->setup_frame
和 pinfo->num
来实现:
/* in the dissector routine */
conversation = find_conversation(pinfo->num, &pinfo->src, &pinfo->dst,
pinfo->ptype, pinfo->srcport, pinfo->destport, 0);
if (conversation == NULL || (conversation->setup_frame != pinfo->num)) {
/* It's not part of any conversation or the returned
* conversation->setup_frame doesn't match the current frame
* create a new one.
*/
conversation = conversation_new(pinfo->num, &pinfo->src,
&pinfo->dst, pinfo->ptype, pinfo->srcport, pinfo->destport,
NULL, 0);
}
使用会话索引字段的会话示例代码
有时会话不足以为网络流量定义唯一的数据存储值. 比如在会话中存储了 request 信息, 而 request 带有标识符来定义自己. 此时就需要会话和标识符两个变量去查找数据存储指针. 可使用 conversation 的 index 成员来唯一定义会话.
示例代码可见 packet-afs.c. 此解析器中同一会话会发送多个 request. 此解析器有一个内部哈希表来保存每个 request 的信息, 哈希表的 key 就包含了会话的 index.
/* in the dissector routine */
/* to find a request value, first lookup conversation to get index */
/* then used the conversation index, and request data to find data */
/* in the local hash table */
conversation = find_or_create_conversation(pinfo);
request_key.conversation = conversation->index;
request_key.service = pntoh16(&rxh->serviceId);
request_key.callnumber = pntoh32(&rxh->callNumber);
request_val = (struct afs_request_val *)g_hash_table_lookup(
afs_request_hash, &request_key);
/* only allocate a new hash element when it's a request */
opcode = 0;
if (!request_val && !reply)
{
new_request_key = wmem_alloc(wmem_file_scope(), sizeof(struct afs_request_key));
*new_request_key = request_key;
request_val = wmem_alloc(wmem_file_scope(), sizeof(struct afs_request_val));
request_val -> opcode = pntoh32(&afsh->opcode);
opcode = request_val->opcode;
g_hash_table_insert(afs_request_hash, new_request_key,
request_val);
}
2.2.3 动态会话解析器注册
注意这里假定源端口/地址, 目的端口/地址这些创建会话的所有信息都是知道的, 如果只知道目的端口或地址, 见 2.2.4 节.
某些协议(如 FTP)会在一条连接里协商后续连接的端口, 对此场景, 可以(在当前连接的解析器代码中)给会话设置一个解析器以处理后续连接.
在为后续连接创建会话, 或对此会话设置解析前, 应先检查会话是否已经存在, 如果存在, 还要检查是否已经设置了一样的解析器. 之所以要检查, 是因为在应用生命周期的不同阶段, 多个不同协议可能会使用相同的 socketpair. 可通过跟踪会话起始的报文序号来区分.
可通过 conversation_set_dissector()
接口来设置动态解析器. 解析器可通过调用 create_dissector_handle()
或 register_dissector()
创建.
示例代码:
/* the handle for the dynamic dissector */
static dissector_handle_t sub_dissector_handle;
/* prototype for the dynamic dissector */
static void sub_dissector(tvbuff_t *tvb, packet_info *pinfo,
proto_tree *tree);
/* in the main protocol dissector, where the next dissector is setup */
/* if conversation has a data field, create it and load structure */
/* First check if a conversation already exists for this socketpair */
conversation = find_conversation(pinfo->num,
&pinfo->src, &pinfo->dst, protocol,
src_port, dst_port, 0);
/* If there is no such conversation, or if there is one but for
someone else's protocol then we just create a new conversation
and assign our protocol to it.
*/
if ( (conversation == NULL) ||
(conversation->dissector_handle != sub_dissector_handle) ) {
new_conv_info = wmem_alloc(wmem_file_scope(), sizeof(struct _new_conv_info));
new_conv_info->data1 = value1;
/* create the conversation for the dynamic port */
conversation = conversation_new(pinfo->num,
&pinfo->src, &pinfo->dst, protocol,
src_port, dst_port, new_conv_info, 0);
/* set the dissector for the new conversation */
conversation_set_dissector(conversation, sub_dissector_handle);
}
...
void
proto_register_PROTOABBREV(void)
{
...
sub_dissector_handle = create_dissector_handle(sub_dissector,
proto);
...
}
FTP 解析器就使用了这一特性(epan/dissectors/packet-ftp.c):
// 用于解析FTP数据的动态解析器
static dissector_handle_t ftpdata_handle;
// FTP数据解析函数
static int
dissect_ftpdata(tvbuff_t *tvb, packet_info *pinfo, proto_tree *tree, void* data _U_)
{ ... }
void
proto_register_ftp(void)
{
...
// 注册FTP数据协议, 及相应解析器
proto_ftp_data = proto_register_protocol("FTP Data", "FTP-DATA", "ftp-data");
ftpdata_handle = register_dissector("ftp-data", dissect_ftpdata, proto_ftp_data);
...
}
// FTP解析器代码
static int
dissect_ftp(tvbuff_t *tvb, packet_info *pinfo, proto_tree *tree, void* data _U_)
{
...
// 如果是PORT请求或PASV响应等情况, 给出了数据连接的端口号,
// 调用create_and_link_data_conversation()
if (is_port_request) {
if (parse_port_pasv(line, linelen, &ftp_ip, &ftp_port, &pasv_offset, &ftp_ip_len, &ftp_port_len)) {
...
/* Set up data conversation */
create_and_link_data_conversation(pinfo,
&pinfo->dst, 20,
&ftp_ip_address, ftp_port,
"PORT");
}
}
...
}
static void create_and_link_data_conversation(...)
{
...
ftp_conversation_t *p_ftp_conv = find_or_create_ftp_conversation(pinfo);
// 为后续数据连接创建新会话, 并设置解析器为FTP数据解析器
ftp_data_conversation_t *p_ftp_data_conv;
conversation_t *data_conversation = conversation_new(pinfo->num,
addr_a, addr_b,
ENDPOINT_TCP,
port_a, port_b,
NO_PORT2);
conversation_set_dissector(data_conversation, ftpdata_handle);
...
}
2.2.4 动态 server port 解析器注册
某些协议会为后续协议定义服务器地址和端口, 对此场景, 可使用会话把服务器地址端口和解析器关联起来. 关键点在于创建会话时第2个地址和端口应设置为”accept any”.
某些服务器应用会在事务的不同阶段, 把相同的端口用于不同的协议. 比如可能在一开始使用 SNMP 进行信息获取, 之后使用相同端口使用 TFTP. 为正确处理这种情况, 必须进行必要的检查.
有两个函数可用于在之后设置第2个端口和地址:
conversation_set_port2( conversation_t *conv, guint32 port);
conversation_set_addr2( conversation_t *conv, address addr);
这些函数会把 server port 会话转换为更完整的会话, 如果想保持server port 会话类型的话, 就不能调用这些函数, 而是应该创建一个新的会话.
示例代码:
/* the handle for the dynamic dissector *
static dissector_handle_t sub_dissector_handle;
...
/* in the main protocol dissector, where the next dissector is setup */
/* if conversation has a data field, create it and load structure */
new_conv_info = wmem_alloc(wmem_file_scope(), sizeof(struct _new_conv_info));
new_conv_info->data1 = value1;
// 为动态server地址和端口创建会话
// 注意: 这里使用了NO_ADDR_B | NO_PORT_B 表示不关心server地址和端口
/* First check if a conversation already exists for this IP/protocol/port */
conversation = find_conversation(pinfo->num,
&server_src_addr, 0, protocol,
server_src_port, 0, NO_ADDR_B | NO_PORT_B);
/* If there is no such conversation, or if there is one but for
someone else's protocol then we just create a new conversation
and assign our protocol to it.
*/
if ( (conversation == NULL) ||
(conversation->dissector_handle != sub_dissector_handle) ) {
conversation = conversation_new(pinfo->num,
&server_src_addr, 0, protocol,
server_src_port, 0, new_conv_info, NO_ADDR2 | NO_PORT2);
/* set the dissector for the new conversation */
conversation_set_dissector(conversation, sub_dissector_handle);
}
2.2.5 单包信息
可以调用以下接口对单个报文添加/读取自定义信息(声明于 epan/proto_data.h):
void
p_add_proto_data(wmem_allocator_t *scope, packet_info *pinfo, int proto, guint32 key, void *proto_data);
void *
p_get_proto_data(wmem_allocator_t *scope, packet_info *pinfo, int proto, guint32 key);
proto_data 的内存作用域(scope)必须与 scope 参数一致. 使用此特性的场景有:
- 为抓包文件中的每个报文保存信息 (file scope)
- 在单个报文的每次解析之间传递信息 (packet scope)
- 为”Decode As”对话框提供报文数据 (packet scope)
2.2.6 用户偏好设置
如果解析器支持用户选项, 可将此添加到配置对话框. 首先, 需要调用以下两个接口(任一)注册 module:
其中module_t *prefs_register_protocol(proto_id, void (*apply_cb)(void));
module_t *prefs_register_protocol_subtree(const char *subtree, int id,
void (*apply_cb)(void));
apply_cb
是配置应用后执行的回调函数.
然后可以调用以下接口注册允许用户配置的字段: ```c / Register a preference with an unsigned integral value. / void prefs_register_uint_preference(module_t module, const char name, const char title, const char description, guint base, guint *var);
/ Register a preference with an Boolean value. / void prefs_register_bool_preference(module_t module, const char name, const char title, const char description, gboolean *var);
/ Register a preference with an enumerated value. / void prefs_register_enum_preference(module_t module, const char name, const char title, const char description, gint var, const enum_val_t enumvals, gboolean radio_buttons)
/ Register a preference with a character-string value. / void prefs_register_string_preference(module_t module, const char name, const char title, const char description, char **var)
/* Register a preference with a file name (string) value.
- File name preferences are basically like string preferences
- except that the GUI gives the user the ability to browse for the
- file. Set for_writing TRUE to show a Save dialog instead of normal Open. / void prefs_register_filename_preference(module_t module, const char name, const char title, const char description, char *var, gboolean for_writing)
/* Register a preference with a range of unsigned integers (e.g.,
“1-20,30-40”). / void prefs_register_range_preference(module_t module, const char name, const char title, const char description, range_t var, guint32 max_value)
示例代码 (packet-rtpproxy.c):
c proto_rtpproxy = proto_register_protocol (“Sippy RTPproxy Protocol”,"RTPproxy", "rtpproxy");
…
rtpproxy_module = prefs_register_protocol(proto_rtpproxy, proto_reg_handoff_rtpproxy);
prefs_register_bool_preference(rtpproxy_module, “establish_conversation”, “Establish Media Conversation”, “Specifies that RTP/RTCP/T.38/MSRP/etc streams are decoded based “ “upon port numbers found in RTPproxy answers”, &rtpproxy_establish_conversation);
prefs_register_uint_preference(rtpproxy_module, “reply.timeout”, “RTPproxy reply timeout”, “Maximum timeout value in waiting for reply from RTPProxy (in milliseconds).”, 10, &rtpproxy_timeout);
<a name="Rofij"></a>
### 2.2.7 TCP上层协议的数据重组
重组跨越多个 TCP segments 的协议数据单元(Protocol Data Unit: PDU)主要有两种方法. 第一种方法比较简单 , 但需要假定你的解析器位于 TCP 之上(但可能它也会位于 UDP 之上), 而且你的 PDU 由定长数据组成, 其中包含用于确定 PDU 长度的足够信息, 后面还可以带有其他数据. 第二种方法更通用, 但要编写更多代码并且不够高效.
**(一) 使用tcp_disssect_pdus()**<br />第一种方法, 需要注册两个不同的解析方法, 一个用于 TCP, 另一个用于其他. 一个好的做法是再实现一个 dissect_PROTO_common() 函数用于解析所有 PDU 中的通用内容, 这个函数可由 dissect_PROTO_tcp() 调用, 也可由 dissect_PROTO_udp() , dissect_PROTO_other() 等调用.
为注册不同的解析器函数, 可参考以下代码(来自 packet-hartip.c):
```c
#include "packet-tcp.h"
dissector_handle_t hartip_tcp_handle;
dissector_handle_t hartip_udp_handle;
hartip_tcp_handle = create_dissector_handle(dissect_hartip_tcp, proto_hartip);
hartip_udp_handle = create_dissector_handle(dissect_hartip_udp, proto_hartip);
dissector_add_uint("udp.port", HARTIP_PORT, hartip_udp_handle);
dissector_add_uint_with_preference("tcp.port", HARTIP_PORT, hartip_tcp_handle);
dissect_hartip_udp()
很简单, 并调用 dissect_hartip_common()
, 而 dissect_hartip_tcp()
调用 tcp_dissect_pdus()
, 并传入一个回调函数 dissect_hartippdu, 它将在重组后被调用:
static int
dissect_hartip_tcp(tvbuff_t *tvb, packet_info *pinfo, proto_tree *tree,
void *data)
{
if (!tvb_bytes_exist(tvb, 0, HARTIP_HEADER_LENGTH))
return 0;
tcp_dissect_pdus(tvb, pinfo, tree, hartip_desegment, HARTIP_HEADER_LENGTH,
get_dissect_hartip_len, dissect_hartip_pdu, data);
return tvb_reported_length(tvb);
}
(二) 修改 pinfo
第二种重组方法适合用于解析器不知道需要读取多少数据才能确定 PDU 长度的场景, 也可用于本身需要支持重组的解析器.
首先, dissect_PROTO() 传入包含第一个报文 payload 的 tvbuff. 它应解析尽可能多的数据, 注意此报文可能包含多个完整 PDU. 如果 tvbuff 的结尾与某 PDU 的结尾对应, 那么是简单情况, 解析器正常返回即可. (如果是新式解析器, 应返回成功处理的字节数).
如果解析器发现 tvbuff 的结尾与 PDU 结尾不对应(即有 PDU 在 tvbuff 的结尾处截断了), 它可以通过修改 pinfo 结构体向父解析器报告此情况. pinfo->desegment_offset 是在 tvbuff 中的偏移, 解析器下次被调用时将从此位置继续处理. pinfo->desegment_len 表示构造完整 PDU 所需的额外长度. dissect_FOO() 下次被调用时, 传入的 tvbuff 将由上一次 tvbuff 的数据结尾和 desegment_len 个字节构成.
如果解析器无法确定需要多少字节, 应将 pinfo->desegment_len 置为 DESEGMENT_ONE_MORE_SEGMENT; 这后只要有更多数据可用, 就会立即调用解析器函数. 解析器应该在可能的时候把 pinfo->desegment_len 置为一个合理的值, 而不是一直置为 DESEGMENT_ONE_MORE_SEGMENT, 因为这样通常更高效. 另外, 在这种情况绝不能把 desegment_len 置为 1, 并希望之后再修改: 一旦从 desegment_len 返回正值, PDU 的长度就定死了.
示例代码:
static hf_register_info hf[] = {
{&hf_cstring,
{"C String", "c.string", FT_STRING, BASE_NONE, NULL, 0x0,
NULL, HFILL}
}
};
/**
* Dissect a buffer containing ASCII C strings.
*
* @param tvb The buffer to dissect.
* @param pinfo Packet Info.
* @param tree The protocol tree.
* @param data Optional data parameter given by parent dissector.
**/
static int dissect_cstr(tvbuff_t * tvb, packet_info * pinfo, proto_tree * tree, void *data _U_)
{
guint offset = 0;
while(offset < tvb_reported_length(tvb)) {
gint available = tvb_reported_length_remaining(tvb, offset);
gint len = tvb_strnlen(tvb, offset, available);
if( -1 == len ) {
/* we ran out of data: ask for more */
pinfo->desegment_offset = offset;
pinfo->desegment_len = DESEGMENT_ONE_MORE_SEGMENT;
return (offset + available);
}
col_set_str(pinfo->cinfo, COL_INFO, "C String");
len += 1; /* Add one for the '\0' */
if (tree) {
proto_tree_add_item(tree, hf_cstring, tvb, offset, len,
ENC_ASCII|ENC_NA);
}
offset += (guint)len;
}
/* if we get here, then the end of the tvb coincided with the end of a
string. Happy days. */
return tvb_captured_length(tvb);
}
这个简单的解析器不断将 desegment_len 置为 DESEGMENT_ONE_MORE_SEGMENT 以获取更多数据, 直到 tvbuff 包含了完整的 C 字符串. C 字符串之后被添加到协议树. 注意 tvbuff 中可能包含多个 C 字符串, 所以这里用了循环.
2.2.8 使用udp_dissect_pdus()
上文提到的 TCP 的 tcp_dissect_pdus() 接口可处理跨越多个报文的 PDU, 也可处理单个报文中的多个 PDU. 此小节描述 UDP 一个类似的机制, 但只能用于单包内的一个或多个 PDU. 如果协议可能在 TCP 和 UDP 两者之上, 则实现一个通用的 PDU 解析函数.
示例代码如下(packet-dnp.c), 首先为 TCP 和 UDP 注册不同的解析器函数:
#include "packet-tcp.h"
#include "packet-udp.h"
dissector_handle_t dnp3_tcp_handle;
dissector_handle_t dnp3_udp_handle;
dnp3_tcp_handle = create_dissector_handle(dissect_dnp3_tcp, proto_dnp3);
dnp3_udp_handle = create_dissector_handle(dissect_dnp3_udp, proto_dnp3);
dissector_add_uint("tcp.port", TCP_PORT_DNP, dnp3_tcp_handle);
dissector_add_uint("udp.port", UDP_PORT_DNP, dnp3_udp_handle);
两个解析器函数分别调用相应的 PDU 接口, 处理重组问题, 但重组完成后的 PDU 都会被同一函数 dissect_dnp3_message() 解析:
static int
dissect_dnp3_udp(tvbuff_t *tvb, packet_info *pinfo, proto_tree *tree, void *data)
{
return udp_dissect_pdus(tvb, pinfo, tree, DNP_HDR_LEN, dnp3_udp_check_header,
get_dnp3_message_len, dissect_dnp3_message, data);
}
static int
dissect_dnp3_tcp(tvbuff_t *tvb, packet_info *pinfo, proto_tree *tree, void *data)
{
if (!check_dnp3_header(tvb, FALSE)) {
return 0;
}
tcp_dissect_pdus(tvb, pinfo, tree, TRUE, DNP_HDR_LEN,
get_dnp3_message_len, dissect_dnp3_message, data);
return tvb_captured_length(tvb);
}
2.2.9 PINO (Protocols In Name Only)
一般情况下, 协议和解析器是 1:1 对应的, 但一个协议有时候也需要多个名字, 因为它有使用同一解析器表的多个解析器函数. 这种特性消除了”Decode As”功能带来的混乱.
通过 proto_register_protocol()
创建”主”协议名后, 可通过 proto_register_protocol_in_name_only()
创建 “pinos”. pinos 有同样的协议命名规范, 但会分开存储以避免和真实协议混淆. 当主协议被禁用后, 它的所有 pinos 也会被禁用. pinos 不应带有任何注册字段和启发式表(heuristic table).
使用 pinos 的另一个场景是, 协议包括 TLV 设计, 可创建解析器表用于解析 “V”. 另外, 由于创建了解析器表, TLV 中 “V” 的解析可以在原解析器代码之外进行(只能能访问到解析器表就行).
示例代码 (epan/dissectors/packet-tcp.c). TCP的选项字段就是通过 pinos 实现的:
void
proto_register_tcp(void)
{
....
// 创建TCP选项字段相关解析器表
tcp_option_table = register_dissector_table("tcp.option",
"TCP Options", proto_tcp, FT_UINT8, BASE_DEC);
// 注册TCP选项字段pinos, 主协议都是proto_tcp
proto_tcp_option_nop = proto_register_protocol_in_name_only("TCP Option - No-Operation (NOP)", "No-Operation (NOP)", "tcp.options.nop", proto_tcp, FT_BYTES);
proto_tcp_option_eol = proto_register_protocol_in_name_only("TCP Option - End of Option List (EOL)", "End of Option List (EOL)", "tcp.options.eol", proto_tcp, FT_BYTES);
proto_tcp_option_timestamp = proto_register_protocol_in_name_only("TCP Option - Timestamps", "Timestamps", "tcp.options.timestamp", proto_tcp, FT_BYTES);
proto_tcp_option_mss = proto_register_protocol_in_name_only("TCP Option - Maximum segment size", "Maximum segment size", "tcp.options.mss", proto_tcp, FT_BYTES);
...
}
void
proto_reg_handoff_tcp(void)
{
...
/* Create dissection function handles for all TCP options */
// 创建TCP选项字段对应的解析器, 并添加到解析器表
dissector_add_uint("tcp.option", TCPOPT_TIMESTAMP, create_dissector_handle( dissect_tcpopt_timestamp, proto_tcp_option_timestamp ));
dissector_add_uint("tcp.option", TCPOPT_MSS, create_dissector_handle( dissect_tcpopt_mss, proto_tcp_option_mss ));
dissector_add_uint("tcp.option", TCPOPT_WINDOW, create_dissector_handle( dissect_tcpopt_wscale, proto_tcp_option_wscale ));
dissector_add_uint("tcp.option", TCPOPT_SACK_PERM, create_dissector_handle( dissect_tcpopt_sack_perm, proto_tcp_option_sack_perm ));
...
}
// 最终由TCP选项相关解析函数调用
static void
tcp_dissect_options(tvbuff_t *tvb, int offset, ...)
{
...
opt = tvb_get_guint8(tvb, offset);
...
option_dissector = dissector_get_uint_handle(tcp_option_table, opt);
...
next_tvb = tvb_new_subset_length(tvb, offset, optlen);
call_dissector_with_data(option_dissector, next_tvb, pinfo, opt_tree/* tree */, data);
...
}
2.2.10 “Decode As”功能
2.2.11 ptvcursor
2.2.12 优化
协议解析器的调用根据协议树参数(proto_tree*
)是否为 null, 可分为两种方式 .
如果协议树为空, 则解析器应避免构造协议树, 并不需要处理构造协议树才需要用到的报文数据. 不过, 仍要填充列信息, 创建会话, 重组报文, 也没提 expert 相关函数, 构造其他解析所需的持久化状态, 调用后续解析器等.
注意在解析器第一次被调用时, 并不能保证 协议树为 null; 解析器在两种情况(协议树为 null 与否), 都应正确构造和更新所需的状态信息.
3 注册
4 报文解析过程
5 数据重组
// TODO
参考
- Wireshark源码: doc/README.dissector