wireshark中capture_session结构代表了一个捕捉过程。默认情况下,当用户选择网卡启动wireshark的捕获数据帧功能后会启动一个dumpcap子进程,这个子进程在管道上透过一个自定义的协议传递消息给wireshark主程序,这些消息包括了告知主进程捕获文件所在的路径的通知、捕获工作状态变化的通知(例如暂停、停止捕获、发生错误)、新捕获到数据包的通知等等。
(一)初始化捕获会话在程序启动时调用capture_session_init()函数以初始化其内部基本的成员函数,调用栈如下:
capture_session_init(...)
capture_input_init(capture_session *cap_session, capture_file *cf)
MainWindow::MainWindow(...)
这里传递给capture_input_init()函数的cap_session是一个MainWindow的私有成员,cf是一个全局对象。在capture_session_init()中主要做以下事情:
- 将cap_session->cf置为capture_input_init参数2传入的全局变量。
- 初始化捕获子进程的pid为-1
- 初始化和Pipe有关的fd为-1
- 当前sesseion状态置为CAPTURE_STOPPED
- 捕获封包计数设置为0
- session即将重启的标记设置为FALSE
- 设置几个回调函数,当对应事件发生的时候调用对应的函数。这几个函数都定义在capture.c中。
- capture_session.new_file=capture_input_new_file
- capture_session.new_packets=capture_input_new_packets
- capture_session.drops=capture_input_drops
- capture_session.error=capture_input_error
- capture_session.cfilter_error=capture_input_cfilter_error
- capture_session.closed=capture_input_closed
用户选择网卡接口启动捕获时的调用栈如下:
Wireshark.exe!capture_start(...)
Wireshark.exe!MainWindow::startCapture()
在capture_start函数中主要做以下事情:
- 设置会话状态为“捕获准备中”capture_session.state = CAPTURE_PREPARING;
- 重置封包计数capture_session.count = 0;
- 根据capture_options得到临时文件名,并赋予capture_session.cf.source
- 调用sync_pipe_start,在这个函数中:
- 以capture_options对象为选项设置dumpcap子进程的命令行参数。
- 创建和dumpcap子进程通信的pipe
- 在sync_pipe_start中创建dumpcap子进程。
- 保存子进程句柄到capture_session.fork_child
- 保存pipe写fd到capture_session.signal_pipe_write_fd
- 重置子进程exitcode。capture_session.fork_child_status = 0;
- 保存capture_options对象到capture_session.capture_opts
- 初始化capture_session.cap_data_info让它指向MainWindow::info_data_,这个数据结构应该是用于统计数据包相关功能的。
- 调用pipe_input_set_handler,设置管道读取事件的处理函数为sync_pipe_input_cb,以后当有消息从管道获取到的时候将调用它处理。
- 使用wtap_rec_init()函数初始化capture_session.rec,这个缓冲区会在读例程capture_session.new_packets中用到。(用户读取数据包的元数据,后续内容详细说明wtap_rec结构)
- 使用ws_buffer_init()初始化capture_session.buf,并为它申请1514个字节(数据链路帧MTU1500字节+14字节?),这个缓冲区用于存放数据包,会在读例程capture_session.new_packets中用到。
在执行启动捕获后(在dumpcap子进程启动后),子进程将通过管道发送一条SP_FILE消息(处理过程详情见sync_pipe_input_cb()函数),这条消息携带一个数据捕获文件名,dumpcap子进程会按照约定的格式往这个文件里写入数据包。在处理这个消息时会调用capture_session.new_file方法初始化capture_file结构。
在capture_session.new_file中(通过capture_input_new_file()实现)做以下这些事情:
- 检查session状态,确保为CAPTURE_PREPARING或CAPTURE_RUNNING
- 判断capture_opts确认是否为临时文件模式,wireshark程序如果没有设置保存到文件这里就是临时文件模式(通常为临时文件模式)。并根据它的值设置capture_session.cf.is_tempfile。
- 根据管道发来的文件名,设置capture_opts.save_file的值。
- 执行cf_open,打开临时文件并设置session从中读取。
- 执行wtap_open_offline()函数,打开文件,alloc和初始化一个 wtap 结构。在这个过程中wiretap库将确认文件的具体类型,并在wtap结构中设置对应的处理函数。(例如对于默认的pacpng捕获程序而言,这些值分别是pcapng_read、pcapng_seek_read、pcapng_close,见下方"关于wtap_open_offline"一段)
- 用cf_close()关闭capture_file,重置一堆文件有关的状态。
- 使用wtap_rec_init()初始化记录元数据(capture_file.rec, wtap_rec类型)。
- 使用ws_buffer_init()初始化capture_file.buf
- 将capture_file设置为FILE_READ_IN_PROGRESS状态,表示我们即将开始读取文件
- 将wtap_open_offline()返回的wtap结构指针赋予capture_file.provider.wth
- 初始化capture_file的f_datalen、filename、is_tempfile、unsaved_changes、computed_elapsed、cd_t、open_type等等成员
- 初始化capture_file.provider.frames,在这里会存放文件读取出来的所有数据帧(这是一棵四级radix tree,每隔节点都有1024个子节点,所以树的第一层、第二层、第三层分别可以放1024、1024*1024、1024*1024*1024个节点)
- 创建新的epan(数据包解析模块句柄),用capture_file作为参数。
- 通知UI的数据包列表重绘。
- 设置wtap_set_cb_new_ipv4、wtap_set_cb_new_ipv6、wtap_set_cb_new_secrets等名字解析的额外回调(仅pcapng适用)。
- 设置session状态为CAPTURE_RUNNING
- 调用capture_callback_invoke()执行UI设置的callback,以更新对应的UI。
关于wtap_open_offline
这个函数打开一个文件,alloc和准备一个 wtap 结构。如果“do_random”为TRUE,则打开文件两次; 第二个 open 允许应用程序执行随机访问 I/O而无需移动顺序 I/O 的查找偏移量,在选中数据包时用随机 I/O 可以显示数据包的协议树。Wireshark 使用该偏移量以便对作为新数据包写入的捕获文件执行顺序 I/O。
- alloc一个wtap
- 执行file_open,将结果赋予wtap.fh
- 如果允许随机读取(do_random=TRUE),再执行一次file_open,将结果赋予wtap.random_fh
- 初始化wtap的部分成员
- 设置时间戳精度file_tsprec=WTAP_TSPREC_USEC
- 初始化shb_hdrs链表,为它添加第一个wtap_block。
- 设置pathname为dumpcap创建的数据捕获文件路径。
- 初始化ispipe、file_encap、subtype_sequential_close、subtype_close、priv、wslua_data等的值为默认值。
- 初始化interface_data,它是包含接口列表的数组。pcapng_open 和 erf_open 需要这个(和 libpcap_open 用于ERF封装类型)。 总是在这里初始化它可以节省以后检查 NULL ptr 的时间。
- 初始化next_interface_data,它是wtap_get_next_interface_description() 将返回的下一个接口数据。
- 如果允许随机读取,则调用file_set_random_access()以设置文件随机读取的功能。
- 执行循环尝试遍历open_routines链表中所有的open_routine看看哪个函数可以打开这个文件,用这种方式确定捕获文件的类型。这里open_routines是从open_info_base[]数组初始化而来(见init_open_routines),在open_info_base定义了各种类型数据捕获文件的特征(典型如幻数、拓展名)。同时按照libwiretap库的约定,在open_routine中还将设置wtap.subtype_read(顺序读)、wtap.subtype_seek_read(随机读)、wtap.subtype_close等特定文件格式有关的处理函数(例如对于wireshark默认使用的pacpng捕获文件格式而言,这些值分别是pcapng_read()、pcapng_seek_read()、pcapng_close(),这样当执行wtap.subtype_read时实际执行的是pcapng文件格式对应的pcapng_read()例程)。
wireshark数据的收集默认是通过pcapng(pcap next gen)库,通过一个管道,当子进程dumpcap有新捕获带数据(并保存到临时文件)后,将通知wireshark主模块。最终主进程调用capture.c文件中的capture_input_new_packets()函数去读取和解析这些数据包,定义如下:
static void capture_input_new_packets(capture_session *cap_session, int to_read);
第一个参数是捕包会话,第二个参数是有多少个新的数据包到了需要读取。在整个函数中,主要起到读取和解析作用的是cf_continue_tail()函数:
if(capture_opts->real_time_mode)
{
switch (cf_continue_tail(
(capture_file *)cap_session->cf,
to_read,
&cap_session->rec,
&cap_session->buf, &err))
{
...
}
}
其声明如下:
cf_read_status_t cf_continue_tail(capture_file *cf, volatile int to_read,
wtap_rec *rec, Buffer *buf, int *err);
这个函数定义在file.c文件中。
这个函数关键的几个地方:
// 省略部分代码......
epan_dissect_t edt;
// 省略部分代码......
epan_dissect_init(&edt, cf->epan, create_proto_tree, FALSE);
TRY {
gint64 data_offset = 0;
column_info *cinfo;
cinfo = (tap_flags & TL_REQUIRES_COLUMNS) ? &cf->cinfo : NULL;
// 在下面这里循环读取to_read个数据包
while (to_read != 0) {
wtap_cleareof(cf->provider.wth);
// 从临时读取这个数据包到rec和buf
if (!wtap_read(cf->provider.wth, rec, buf, err, &err_info,
&data_offset)) {
break;
}
if (cf->state == FILE_READ_ABORTED) {
break;
}
// 读取封包内容,并使用epan_dissect_t解析(并通过dfcode过滤)
if (read_record(cf, rec, buf, dfcode, &edt, cinfo, data_offset)) {
newly_displayed_packets++;
}
to_read--;
}
}
CATCH(OutOfMemoryError) {
...... //错误处理,如严重就退出程序。
}
ENDTRY;
// ......省略部分代码
// 解析完成后
epan_dissect_cleanup(&edt);
这里有两个关键的调用wtap_read()和read_record()。
wtap_read()的作用如下:
- 初始化一个wtap_rec结构,然后从数据捕获文件中读取数据包的元数据到wtap_rec结构中,元数据指的是记录数据包在文件中的偏移,数据包的长度等信息。
- 将数据包从文件中读取到Buffer结构中。Buffer.data指向了实际的数据。
read_record()的作用:
- 如果程序设置了过滤器,则尝试过滤这个数据包,如果满足过滤条件直接pass并返回FALSE。
- 根据wtap_rec结构构建frame_data,然后添加到capture_file.provider.frames这颗基数树中(基数为1024),这里是唯一保存帧的地方。注意frame_data中不包含数据包,很明显如果每个包都保存下来很快内存就会耗尽,所以capture_file.provider.frames这颗树仅起到一个索引的作用。
- 将数据包Buffer和wtap_rec等传递给epan_dissect_run_with_taps执行,解析这个帧。
- 调用packet_list_append将这个帧添加到数据包列表中(添加到PacketListModel对象中,这样UI就可以将他绘制出来,见PacketListModel::appendPacket())。
关于元数据wtap_rec,定义如下:
typedef struct {
guint rec_type;
guint32 presence_flags;
nstime_t ts;
int tsprec;
union {
wtap_packet_header packet_header; // 对于REC_TYPE_PACKET有效
wtap_ft_specific_header ft_specific_header; //暂不用管
wtap_syscall_header syscall_header; //暂不用管
wtap_systemd_journal_export_header systemd_journal_export_header; //暂不用管
wtap_custom_block_header custom_block_header; //暂不用管
} rec_header;
wtap_block_t block ;
gboolean block_was_modified;
Buffer options_buf;
} wtap_rec
typedef struct {
guint32 caplen;
guint32 len;
int pkt_encap;
guint32 interface_id;
union wtap_pseudo_header pseudo_header;
} wtap_packet_header;
struct wtap_block
{
wtap_blocktype_t* info; // 见下面的描述
void* mandatory_data;
GArray* options;
gint ref_count;
#ifdef DEBUG_COUNT_REFS
guint id;
#endif
};
typedef struct {
wtap_block_type_t block_type;
const char *name;
const char *description;
wtap_block_create_func create;
wtap_mand_free_func free_mand;
wtap_mand_copy_func copy_mand;
GHashTable *options;
} wtap_blocktype_t;
关于frame_data定义如下:
typedef struct _frame_data {
guint32 num;
guint32 pkt_len;
guint32 cap_len;
guint32 cum_bytes;
gint64 file_off;
GSList *pfd;
const struct _color_filter *color_filter;
guint16 subnum;
unsigned int passed_dfilter : 1;
unsigned int dependent_of_displayed : 1;
unsigned int encoding : 1;
unsigned int visited : 1;
unsigned int marked : 1;
unsigned int ref_time : 1;
unsigned int ignored : 1;
unsigned int has_ts : 1;
unsigned int has_phdr_block : 1;
unsigned int has_modified_block : 1;
unsigned int need_colorize : 1;
unsigned int tsprec : 4;
nstime_t abs_ts;
nstime_t shift_offset;
guint32 frame_ref_num;
guint32 prev_dis_num;
} frame_data;
当一个frame_data需要展示到界面的时候,程序会先调用cf_read_record_no_alert()或者cf_read_record()这两个函数,它们将根据该frame_data取得元数据wtap_rec和数据包Buffer,然后执行epan_dissect_run()函数解析数据包协议树,这部分的逻辑在PacketListRecord::dissect()这个成员函数中。
默认情况下主界面列表展示【序号(No.)、时间(Time)、源地址(Source)、目标地址(Destination)、协议(Protocol)、长度(Length)、简要信息(Info)】。这几项中序号、时间、长度已经在frame_data中,其余几个项都需要执行epan_dissect_run()函数后才可以取到。
总结一下:
- wireshark通过子进程抓取数据包,并通过一个管道接收子进程的通知。
- 当收取到有新的数据包时,读取子进程写入数据包的那个临时文件。
- wireshark通过wiretap库读取数据包,wiretap库可以读取各种格式的数据捕获文件,wireshark默认使用的是pcapng格式。
- wiretap库通过wtap_read()读取一个数据包,同时返回元数据(wtap_rec)和数据包数据(Buffer)。
- 展示在界面数据包列表上的数据基础类型为frame_data结构。通过read_record()将一个wtap_rec重新封装成frame_data,过滤器在这里就已经生效,如果数据包为需要过滤的类型则frame_data在这里就被直接丢弃,否则将添加到capture_file.provider.frames这颗基数树中,并且插入到树控件的模型中。



