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 骨架代码

    // TODO

    2.1.2 骨架代码中需要替换的部分

    // TODO

自动替换工具
// TODO

2.1.3 解析器及其收到的数据

头文件
只有当不使用一般注册方式, 或者需要给其他解析器暴露接口的解析器, 才需要头文件. 解析器必须在头文件 packet-PROTOABBREV.h 中声明:

  1. int
  2. 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. 创建的接口有:

  1. // next_tvb包含的数据从offset开始直到结尾
  2. next_tvb = tvb_new_subset_remaining(tvb, offset);
  3. // 当前解析器报告payload长度为reported_length
  4. next_tvb = tvb_new_subset_length(tvb, offset, reported_length);
  5. // length显式指定next_tvb的数据长度, -1表示尽可能多
  6. // reported_length为-1表示当前解析器不知道payload长度
  7. next_tvb = tvb_new_subset_length_caplen(tvb, offset, length, reported_length);

要调用解析器之前, 需要通过 find_dissector() 接口获取解析器句柄, 参数为解析器名称. 这一工作通常是在初始化阶段, 在解析器的 proto_reg_handoff_XXX() 函数中完成的. 示例代码(epan/dissectors/packet-tcp.c):

  1. static dissector_handle_t data_handle;
  2. static dissector_handle_t sport_handle;
  3. void
  4. proto_reg_handoff_tcp(void)
  5. {
  6. ...
  7. data_handle = find_dissector("data");
  8. sport_handle = find_dissector("sport");
  9. ...
  10. }

解析器表
调用其他解析器的另一个方式是设置解析器表. 解析器表由根据通用标识符(整数或字符串)分组的多个解析器构成. 后续解析器通过以下接口将自己注册到上游解析器的解析器表中:

  1. void dissector_add_uint(const char *abbrev, const guint32 pattern,
  2. dissector_handle_t handle);
  3. void dissector_add_uint_range(const char *abbrev, struct epan_range *range,
  4. dissector_handle_t handle);
  5. void dissector_add_string(const char *name, const gchar *pattern,
  6. dissector_handle_t handle);
  7. void dissector_add_for_decode_as(const char *name,
  8. dissector_handle_t handle);

当解析器命中通用标识符时(比如 TCP 端口 == 80), 就会调用下列接口来调用下游解析器(如 HTTP):

  1. int dissector_try_uint(dissector_table_t sub_dissectors,
  2. const guint32 uint_val, tvbuff_t *tvb, packet_info *pinfo,
  3. proto_tree *tree);
  4. int dissector_try_uint_new(dissector_table_t sub_dissectors,
  5. const guint32 uint_val, tvbuff_t *tvb, packet_info *pinfo,
  6. proto_tree *tree, const gboolean add_proto_name, void *data);
  7. int dissector_try_string(dissector_table_t sub_dissectors, const gchar *string,
  8. 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
给会话关联协议数据:

  1. void conversation_add_proto_data(conversation_t *conv, int proto,
  2. 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
设置会话参数(地址, 端口类型, 端口等)满足某个条件时, 调用某个解析器.

  1. void conversation_set_dissector(conversation_t *conversation, const dissector_handle_t handle);

时间戳
// TODO

使用wmem_file_scope的会话示例代码
对于 IP/Port 四元组会话, 可以使用此示例代码. 此代码使用文件作用域内存池 (wmem_file_scope())来分配协议数据内存, 并关联到会话:

  1. /************************ Global values ************************/
  2. /* define your structure here */
  3. typedef struct {
  4. ...
  5. } my_entry_t;
  6. /* Registered protocol number */
  7. static int my_proto = -1;
  8. /********************* in the dissector routine *********************/
  9. /* the local variables in the dissector */
  10. conversation_t *conversation;
  11. my_entry_t *data_ptr;
  12. /* look up the conversation */
  13. conversation = find_conversation(pinfo->num, &pinfo->src, &pinfo->dst,
  14. pinfo->ptype, pinfo->srcport, pinfo->destport, 0);
  15. /* if conversation found get the data pointer that you stored */
  16. if (conversation)
  17. data_ptr = (my_entry_t*)conversation_get_proto_data(conversation, my_proto);
  18. else {
  19. /* new conversation create local data structure */
  20. data_ptr = wmem_alloc(wmem_file_scope(), sizeof(my_entry_t));
  21. /*** add your code here to setup the new data structure ***/
  22. /* create the conversation with your data pointer */
  23. conversation = conversation_new(pinfo->num, &pinfo->src, &pinfo->dst, pinfo->ptype,
  24. pinfo->srcport, pinfo->destport, 0);
  25. conversation_add_proto_data(conversation, my_proto, (void *)data_ptr);
  26. }
  27. /* at this point the conversation data is ready */
  28. /***************** in the protocol register routine *****************/
  29. my_proto = proto_register_protocol("My Protocol", "My Protocol", "my_proto");

开始于特定报文序号的会话示例代码
当全局有多个源/目的 ip/port 对相同的会话时, 有时解析器需要确定会话是否从某个报文开始. 可通过比较 conversation->setup_framepinfo->num 来实现:

  1. /* in the dissector routine */
  2. conversation = find_conversation(pinfo->num, &pinfo->src, &pinfo->dst,
  3. pinfo->ptype, pinfo->srcport, pinfo->destport, 0);
  4. if (conversation == NULL || (conversation->setup_frame != pinfo->num)) {
  5. /* It's not part of any conversation or the returned
  6. * conversation->setup_frame doesn't match the current frame
  7. * create a new one.
  8. */
  9. conversation = conversation_new(pinfo->num, &pinfo->src,
  10. &pinfo->dst, pinfo->ptype, pinfo->srcport, pinfo->destport,
  11. NULL, 0);
  12. }

使用会话索引字段的会话示例代码
有时会话不足以为网络流量定义唯一的数据存储值. 比如在会话中存储了 request 信息, 而 request 带有标识符来定义自己. 此时就需要会话和标识符两个变量去查找数据存储指针. 可使用 conversation 的 index 成员来唯一定义会话.

示例代码可见 packet-afs.c. 此解析器中同一会话会发送多个 request. 此解析器有一个内部哈希表来保存每个 request 的信息, 哈希表的 key 就包含了会话的 index.

  1. /* in the dissector routine */
  2. /* to find a request value, first lookup conversation to get index */
  3. /* then used the conversation index, and request data to find data */
  4. /* in the local hash table */
  5. conversation = find_or_create_conversation(pinfo);
  6. request_key.conversation = conversation->index;
  7. request_key.service = pntoh16(&rxh->serviceId);
  8. request_key.callnumber = pntoh32(&rxh->callNumber);
  9. request_val = (struct afs_request_val *)g_hash_table_lookup(
  10. afs_request_hash, &request_key);
  11. /* only allocate a new hash element when it's a request */
  12. opcode = 0;
  13. if (!request_val && !reply)
  14. {
  15. new_request_key = wmem_alloc(wmem_file_scope(), sizeof(struct afs_request_key));
  16. *new_request_key = request_key;
  17. request_val = wmem_alloc(wmem_file_scope(), sizeof(struct afs_request_val));
  18. request_val -> opcode = pntoh32(&afsh->opcode);
  19. opcode = request_val->opcode;
  20. g_hash_table_insert(afs_request_hash, new_request_key,
  21. request_val);
  22. }

2.2.3 动态会话解析器注册

注意这里假定源端口/地址, 目的端口/地址这些创建会话的所有信息都是知道的, 如果只知道目的端口或地址, 见 2.2.4 节.

某些协议(如 FTP)会在一条连接里协商后续连接的端口, 对此场景, 可以(在当前连接的解析器代码中)给会话设置一个解析器以处理后续连接.

在为后续连接创建会话, 或对此会话设置解析前, 应先检查会话是否已经存在, 如果存在, 还要检查是否已经设置了一样的解析器. 之所以要检查, 是因为在应用生命周期的不同阶段, 多个不同协议可能会使用相同的 socketpair. 可通过跟踪会话起始的报文序号来区分.

可通过 conversation_set_dissector() 接口来设置动态解析器. 解析器可通过调用 create_dissector_handle()register_dissector() 创建.

示例代码:

  1. /* the handle for the dynamic dissector */
  2. static dissector_handle_t sub_dissector_handle;
  3. /* prototype for the dynamic dissector */
  4. static void sub_dissector(tvbuff_t *tvb, packet_info *pinfo,
  5. proto_tree *tree);
  6. /* in the main protocol dissector, where the next dissector is setup */
  7. /* if conversation has a data field, create it and load structure */
  8. /* First check if a conversation already exists for this socketpair */
  9. conversation = find_conversation(pinfo->num,
  10. &pinfo->src, &pinfo->dst, protocol,
  11. src_port, dst_port, 0);
  12. /* If there is no such conversation, or if there is one but for
  13. someone else's protocol then we just create a new conversation
  14. and assign our protocol to it.
  15. */
  16. if ( (conversation == NULL) ||
  17. (conversation->dissector_handle != sub_dissector_handle) ) {
  18. new_conv_info = wmem_alloc(wmem_file_scope(), sizeof(struct _new_conv_info));
  19. new_conv_info->data1 = value1;
  20. /* create the conversation for the dynamic port */
  21. conversation = conversation_new(pinfo->num,
  22. &pinfo->src, &pinfo->dst, protocol,
  23. src_port, dst_port, new_conv_info, 0);
  24. /* set the dissector for the new conversation */
  25. conversation_set_dissector(conversation, sub_dissector_handle);
  26. }
  27. ...
  28. void
  29. proto_register_PROTOABBREV(void)
  30. {
  31. ...
  32. sub_dissector_handle = create_dissector_handle(sub_dissector,
  33. proto);
  34. ...
  35. }

FTP 解析器就使用了这一特性(epan/dissectors/packet-ftp.c):

  1. // 用于解析FTP数据的动态解析器
  2. static dissector_handle_t ftpdata_handle;
  3. // FTP数据解析函数
  4. static int
  5. dissect_ftpdata(tvbuff_t *tvb, packet_info *pinfo, proto_tree *tree, void* data _U_)
  6. { ... }
  7. void
  8. proto_register_ftp(void)
  9. {
  10. ...
  11. // 注册FTP数据协议, 及相应解析器
  12. proto_ftp_data = proto_register_protocol("FTP Data", "FTP-DATA", "ftp-data");
  13. ftpdata_handle = register_dissector("ftp-data", dissect_ftpdata, proto_ftp_data);
  14. ...
  15. }
  16. // FTP解析器代码
  17. static int
  18. dissect_ftp(tvbuff_t *tvb, packet_info *pinfo, proto_tree *tree, void* data _U_)
  19. {
  20. ...
  21. // 如果是PORT请求或PASV响应等情况, 给出了数据连接的端口号,
  22. // 调用create_and_link_data_conversation()
  23. if (is_port_request) {
  24. if (parse_port_pasv(line, linelen, &ftp_ip, &ftp_port, &pasv_offset, &ftp_ip_len, &ftp_port_len)) {
  25. ...
  26. /* Set up data conversation */
  27. create_and_link_data_conversation(pinfo,
  28. &pinfo->dst, 20,
  29. &ftp_ip_address, ftp_port,
  30. "PORT");
  31. }
  32. }
  33. ...
  34. }
  35. static void create_and_link_data_conversation(...)
  36. {
  37. ...
  38. ftp_conversation_t *p_ftp_conv = find_or_create_ftp_conversation(pinfo);
  39. // 为后续数据连接创建新会话, 并设置解析器为FTP数据解析器
  40. ftp_data_conversation_t *p_ftp_data_conv;
  41. conversation_t *data_conversation = conversation_new(pinfo->num,
  42. addr_a, addr_b,
  43. ENDPOINT_TCP,
  44. port_a, port_b,
  45. NO_PORT2);
  46. conversation_set_dissector(data_conversation, ftpdata_handle);
  47. ...
  48. }

2.2.4 动态 server port 解析器注册

某些协议会为后续协议定义服务器地址和端口, 对此场景, 可使用会话把服务器地址端口和解析器关联起来. 关键点在于创建会话时第2个地址和端口应设置为”accept any”.

某些服务器应用会在事务的不同阶段, 把相同的端口用于不同的协议. 比如可能在一开始使用 SNMP 进行信息获取, 之后使用相同端口使用 TFTP. 为正确处理这种情况, 必须进行必要的检查.

有两个函数可用于在之后设置第2个端口和地址:

  1. conversation_set_port2( conversation_t *conv, guint32 port);
  2. conversation_set_addr2( conversation_t *conv, address addr);

这些函数会把 server port 会话转换为更完整的会话, 如果想保持server port 会话类型的话, 就不能调用这些函数, 而是应该创建一个新的会话.

示例代码:

  1. /* the handle for the dynamic dissector *
  2. static dissector_handle_t sub_dissector_handle;
  3. ...
  4. /* in the main protocol dissector, where the next dissector is setup */
  5. /* if conversation has a data field, create it and load structure */
  6. new_conv_info = wmem_alloc(wmem_file_scope(), sizeof(struct _new_conv_info));
  7. new_conv_info->data1 = value1;
  8. // 为动态server地址和端口创建会话
  9. // 注意: 这里使用了NO_ADDR_B | NO_PORT_B 表示不关心server地址和端口
  10. /* First check if a conversation already exists for this IP/protocol/port */
  11. conversation = find_conversation(pinfo->num,
  12. &server_src_addr, 0, protocol,
  13. server_src_port, 0, NO_ADDR_B | NO_PORT_B);
  14. /* If there is no such conversation, or if there is one but for
  15. someone else's protocol then we just create a new conversation
  16. and assign our protocol to it.
  17. */
  18. if ( (conversation == NULL) ||
  19. (conversation->dissector_handle != sub_dissector_handle) ) {
  20. conversation = conversation_new(pinfo->num,
  21. &server_src_addr, 0, protocol,
  22. server_src_port, 0, new_conv_info, NO_ADDR2 | NO_PORT2);
  23. /* set the dissector for the new conversation */
  24. conversation_set_dissector(conversation, sub_dissector_handle);
  25. }

2.2.5 单包信息

可以调用以下接口对单个报文添加/读取自定义信息(声明于 epan/proto_data.h):

  1. void
  2. p_add_proto_data(wmem_allocator_t *scope, packet_info *pinfo, int proto, guint32 key, void *proto_data);
  3. void *
  4. 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:
    1. module_t *prefs_register_protocol(proto_id, void (*apply_cb)(void));
    2. module_t *prefs_register_protocol_subtree(const char *subtree, int id,
    3. 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”,

    1. "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);

  1. <a name="Rofij"></a>
  2. ### 2.2.7 TCP上层协议的数据重组
  3. 重组跨越多个 TCP segments 的协议数据单元(Protocol Data Unit: PDU)主要有两种方法. 第一种方法比较简单 , 但需要假定你的解析器位于 TCP 之上(但可能它也会位于 UDP 之上), 而且你的 PDU 由定长数据组成, 其中包含用于确定 PDU 长度的足够信息, 后面还可以带有其他数据. 第二种方法更通用, 但要编写更多代码并且不够高效.
  4. **(一) 使用tcp_disssect_pdus()**<br />第一种方法, 需要注册两个不同的解析方法, 一个用于 TCP, 另一个用于其他. 一个好的做法是再实现一个 dissect_PROTO_common() 函数用于解析所有 PDU 中的通用内容, 这个函数可由 dissect_PROTO_tcp() 调用, 也可由 dissect_PROTO_udp() , dissect_PROTO_other() 等调用.
  5. 为注册不同的解析器函数, 可参考以下代码(来自 packet-hartip.c):
  6. ```c
  7. #include "packet-tcp.h"
  8. dissector_handle_t hartip_tcp_handle;
  9. dissector_handle_t hartip_udp_handle;
  10. hartip_tcp_handle = create_dissector_handle(dissect_hartip_tcp, proto_hartip);
  11. hartip_udp_handle = create_dissector_handle(dissect_hartip_udp, proto_hartip);
  12. dissector_add_uint("udp.port", HARTIP_PORT, hartip_udp_handle);
  13. 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, 它将在重组后被调用:

  1. static int
  2. dissect_hartip_tcp(tvbuff_t *tvb, packet_info *pinfo, proto_tree *tree,
  3. void *data)
  4. {
  5. if (!tvb_bytes_exist(tvb, 0, HARTIP_HEADER_LENGTH))
  6. return 0;
  7. tcp_dissect_pdus(tvb, pinfo, tree, hartip_desegment, HARTIP_HEADER_LENGTH,
  8. get_dissect_hartip_len, dissect_hartip_pdu, data);
  9. return tvb_reported_length(tvb);
  10. }

(二) 修改 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 的长度就定死了.

示例代码:

  1. static hf_register_info hf[] = {
  2. {&hf_cstring,
  3. {"C String", "c.string", FT_STRING, BASE_NONE, NULL, 0x0,
  4. NULL, HFILL}
  5. }
  6. };
  7. /**
  8. * Dissect a buffer containing ASCII C strings.
  9. *
  10. * @param tvb The buffer to dissect.
  11. * @param pinfo Packet Info.
  12. * @param tree The protocol tree.
  13. * @param data Optional data parameter given by parent dissector.
  14. **/
  15. static int dissect_cstr(tvbuff_t * tvb, packet_info * pinfo, proto_tree * tree, void *data _U_)
  16. {
  17. guint offset = 0;
  18. while(offset < tvb_reported_length(tvb)) {
  19. gint available = tvb_reported_length_remaining(tvb, offset);
  20. gint len = tvb_strnlen(tvb, offset, available);
  21. if( -1 == len ) {
  22. /* we ran out of data: ask for more */
  23. pinfo->desegment_offset = offset;
  24. pinfo->desegment_len = DESEGMENT_ONE_MORE_SEGMENT;
  25. return (offset + available);
  26. }
  27. col_set_str(pinfo->cinfo, COL_INFO, "C String");
  28. len += 1; /* Add one for the '\0' */
  29. if (tree) {
  30. proto_tree_add_item(tree, hf_cstring, tvb, offset, len,
  31. ENC_ASCII|ENC_NA);
  32. }
  33. offset += (guint)len;
  34. }
  35. /* if we get here, then the end of the tvb coincided with the end of a
  36. string. Happy days. */
  37. return tvb_captured_length(tvb);
  38. }

这个简单的解析器不断将 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 注册不同的解析器函数:

  1. #include "packet-tcp.h"
  2. #include "packet-udp.h"
  3. dissector_handle_t dnp3_tcp_handle;
  4. dissector_handle_t dnp3_udp_handle;
  5. dnp3_tcp_handle = create_dissector_handle(dissect_dnp3_tcp, proto_dnp3);
  6. dnp3_udp_handle = create_dissector_handle(dissect_dnp3_udp, proto_dnp3);
  7. dissector_add_uint("tcp.port", TCP_PORT_DNP, dnp3_tcp_handle);
  8. dissector_add_uint("udp.port", UDP_PORT_DNP, dnp3_udp_handle);

两个解析器函数分别调用相应的 PDU 接口, 处理重组问题, 但重组完成后的 PDU 都会被同一函数 dissect_dnp3_message() 解析:

  1. static int
  2. dissect_dnp3_udp(tvbuff_t *tvb, packet_info *pinfo, proto_tree *tree, void *data)
  3. {
  4. return udp_dissect_pdus(tvb, pinfo, tree, DNP_HDR_LEN, dnp3_udp_check_header,
  5. get_dnp3_message_len, dissect_dnp3_message, data);
  6. }
  7. static int
  8. dissect_dnp3_tcp(tvbuff_t *tvb, packet_info *pinfo, proto_tree *tree, void *data)
  9. {
  10. if (!check_dnp3_header(tvb, FALSE)) {
  11. return 0;
  12. }
  13. tcp_dissect_pdus(tvb, pinfo, tree, TRUE, DNP_HDR_LEN,
  14. get_dnp3_message_len, dissect_dnp3_message, data);
  15. return tvb_captured_length(tvb);
  16. }

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 实现的:

  1. void
  2. proto_register_tcp(void)
  3. {
  4. ....
  5. // 创建TCP选项字段相关解析器表
  6. tcp_option_table = register_dissector_table("tcp.option",
  7. "TCP Options", proto_tcp, FT_UINT8, BASE_DEC);
  8. // 注册TCP选项字段pinos, 主协议都是proto_tcp
  9. 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);
  10. 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);
  11. proto_tcp_option_timestamp = proto_register_protocol_in_name_only("TCP Option - Timestamps", "Timestamps", "tcp.options.timestamp", proto_tcp, FT_BYTES);
  12. 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);
  13. ...
  14. }
  15. void
  16. proto_reg_handoff_tcp(void)
  17. {
  18. ...
  19. /* Create dissection function handles for all TCP options */
  20. // 创建TCP选项字段对应的解析器, 并添加到解析器表
  21. dissector_add_uint("tcp.option", TCPOPT_TIMESTAMP, create_dissector_handle( dissect_tcpopt_timestamp, proto_tcp_option_timestamp ));
  22. dissector_add_uint("tcp.option", TCPOPT_MSS, create_dissector_handle( dissect_tcpopt_mss, proto_tcp_option_mss ));
  23. dissector_add_uint("tcp.option", TCPOPT_WINDOW, create_dissector_handle( dissect_tcpopt_wscale, proto_tcp_option_wscale ));
  24. dissector_add_uint("tcp.option", TCPOPT_SACK_PERM, create_dissector_handle( dissect_tcpopt_sack_perm, proto_tcp_option_sack_perm ));
  25. ...
  26. }
  27. // 最终由TCP选项相关解析函数调用
  28. static void
  29. tcp_dissect_options(tvbuff_t *tvb, int offset, ...)
  30. {
  31. ...
  32. opt = tvb_get_guint8(tvb, offset);
  33. ...
  34. option_dissector = dissector_get_uint_handle(tcp_option_table, opt);
  35. ...
  36. next_tvb = tvb_new_subset_length(tvb, offset, optlen);
  37. call_dissector_with_data(option_dissector, next_tvb, pinfo, opt_tree/* tree */, data);
  38. ...
  39. }

2.2.10 “Decode As”功能

// TODO

2.2.11 ptvcursor

// TODO

2.2.12 优化

协议解析器的调用根据协议树参数(proto_tree*)是否为 null, 可分为两种方式 .

如果协议树为空, 则解析器应避免构造协议树, 并不需要处理构造协议树才需要用到的报文数据. 不过, 仍要填充列信息, 创建会话, 重组报文, 也没提 expert 相关函数, 构造其他解析所需的持久化状态, 调用后续解析器等.

注意在解析器第一次被调用时, 并不能保证 协议树为 null; 解析器在两种情况(协议树为 null 与否), 都应正确构造和更新所需的状态信息.

3 注册

// TODO

4 报文解析过程

// TODO

5 数据重组

// TODO

参考

  • Wireshark源码: doc/README.dissector