ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

MATLAB解析pcap文件:从抓包数据到信号处理的完整指南

MATLAB解析pcap文件:从抓包数据到信号处理的完整指南 搞通信和嵌入式的人电脑里一定不缺各种抓包文件。Wireshark打开pcap看包很方便可是当你要对几十万个包的IP做统计、把UDP负载当成信号序列做FFT、或者分析雷达传感器dump出来的数据流时Wireshark的手工点选就完全不够用了。这时候把pcap文件读进MATLAB就成了最顺理成章的一步。这篇文章不是我抄来的教程而是我实际处理pcap抓包文件时整理的完整做法包含格式细节、MATLAB解析代码、踩坑记录和几个“读完之后怎么办”的落地思路适合正在做网络分析、USB抓包、雷达数据读取或者传感器数据解析的读者。1. 为什么非要在MATLAB里读pcap1.1 Wireshark能看但算不了Wireshark是目前最主流的抓包工具pcap/pcapng文件默认都可以打开还能按协议过滤、看十六进制内容、导出特定包。但它的强项是“人和包交互”不是批量计算。真正让我下决心写MATLAB解析脚本的场景是一个下午处理3个GB的抓包文件要统计某个IP段出现的次数、平均包长、TCP重传占比顺便把某些UDP报文里的负载转换成有符号数画波形。这种活如果靠Wireshark手动导出一次两次可以变成常态化需求就非常痛苦。MATLAB的优势在于数组操作、矩阵运算、信号处理和可视化。一旦把pcap里的数据读成结构体或者表格后面不管是做FFT、滤波、统计回归还是直接画时序图都顺手得多。对于做雷达比如awr2243这类毫米波雷达数据读取的人这个需求更直接雷达原始数据经常通过以太网抓包存成pcap负载里就是ADC采样点必须读到MATLAB里才能做距离多普勒处理Wireshark本身根本干不了这个。1.2 哪些项目会用到这一步从我接触过的需求看主要有这么几类网络流量分析从pcap里提取五元组、包长分布、时间戳间隔做异常检测或性能统计。车载雷达/激光雷达数据传感器通过以太网或者USB抓包输出原始回波数据pcap只是容器的壳真正有价值的是payload里的有符号整数数组。USB抓包USB协议分析仪导出的pcaplinktype不是Ethernet而是特殊的USB类型解析方式要和网络帧区分开。硬件调试FPGA、嵌入式设备把采集数据封装成UDP包发出来上位机用Wireshark抓包后存成pcap需要进一步在MATLAB里做算法验证。传感器数据回放用pcap文件作为离线数据源在MATLAB里模拟实时数据流开发解算算法。这些场景的共同点是文件本身不复杂但数据量不小手工处理不现实而且包结构可能包含私有协议Wireshark不认识只能在MATLAB里自己按字节解析。1.3 官方没现成函数为什么还要自己解析目前MATLAB官方并没有提供一个“一步到位”的pcap读取函数至少在常用版本里没有类似loadpcap()这种内置接口。虽然有File Exchange上的第三方工具但有的需要编译外部库有的只支持很老的MATLAB版本还有的只能读特定linktype。最稳妥、可控性最高的方案还是理解pcap格式后自己写解析器。pcap格式本身不复杂它就是一套“集装箱规则”文件开头放一个全局头后面每个包前面放一个包头包头后面跟原始报文数据。整个格式是公开的二十多年没怎么变过。只要知道字节顺序和字段长度用MATLAB的fread函数就能读出来。这篇文章给的代码就是按这个思路实现的。2. pcap文件结构抓包文件的“集装箱”2.1 全局头部与包头部的字节约定pcap文件是所有包共享一个全局文件头这个头固定24字节。字段如下字段名长度说明magic number4字节0xa1b2c3d4微秒时间戳0xa1b23c4d纳秒时间戳version_major2字节主版本号通常是2version_minor2字节次版本号通常是4thiszone4字节时区偏移一般填0sigfigs4字节时间戳有效位数一般填0snaplen4字节抓包最大长度network4字节链路层类型1表示Ethernet276表示Linux cooked147表示USB这里最关键的是magic number和network字段。magic number不只是用来识别文件还决定了整个文件的大小端network字段决定了后面每个包的数据区应该按什么链路层格式去解析。很多人后面解析出来的IP地址乱掉、端口对不上一半是因为大小端搞反了另一半是因为不知道链路层不是Ethernet。每个数据包前面都有一个16字节的包头字段名长度说明ts_sec4字节Unix时间戳秒ts_usec4字节微秒/纳秒部分取决于magic numberincl_len4字节当前包实际保存的字节数orig_len4字节原始包长度可能大于incl_len读的时候要特别注意incl_len告诉你后面必须读多少字节读完这些字节后再进入下一个包头。如果incl_len和orig_len不一致说明这个包在抓取时被截断了后面那段丢失的数据是补不回来的。2.2 用十六进制视角看一个包如果你用十六进制编辑器注意Windows自带的记事本不建议直接打开二进制文件最好用HxD、010 Editor或者Wireshark自带的“查看-数据包字节”功能打开pcap前面一串字节大概长这样d4 c3 b2 a1 02 00 04 00 00 00 00 00 00 00 00 00 ff ff 00 00 01 00 00 00d4 c3 b2 a1就是小端字节序的magic number真正的值是a1 b2 c3 d4但存的时候低位在前。02 00 04 00是版本号2.4ff ff 00 00是snaplen 65535最后01 00 00 00就是linktype 1以太网。第一个包从偏移24开始先是16字节包头然后跟着incl_len字节的原始以太网帧。看十六进制能建立字节敏感度后面写MATLAB解析时哪些字段在哪个偏移位置心里就有数了。2.3 大小端与时间戳精度pcap文件存在大端和小端两种可能不能假设所有文件都是小端。判断方法很简单如果你读到文件头4个字节是d4 c3 b2 a1那么按小端解析如果读到的是a1 b2 c3 d4那么按大端解析。还有一种magic是a1 b2 3c 4d这是纳秒时间戳版本的pcap文件前两个字节是小端或大端标志后面时间戳分数部分的单位不是微秒而是纳秒。在MATLAB里fread可以指定机器格式参数l和b读uint16、uint32、int32时都能自动处理字节序。但读uint8字节数组时不需要管大小端因为单字节没有顺序问题。整段代码里最容易出错的就是文件头判断一定先把magic number的字节顺序搞对再看后面的字段。3. MATLAB解析器的核心实现3.1 主函数骨架从文件头到数据包循环我建议把解析器拆成两层第一层负责把pcap文件读成MATLAB结构体第二层负责解析每个包的链路层和网络层。这样职责清晰也方便以后支持不同链路类型。先看第一层一个完整的read_pcap_file函数function pcap read_pcap_file(filename) fid fopen(filename, rb); if fid -1 error(无法打开文件: %s, filename); end % 读前4字节判断文件大小端和时间戳精度 magic_bytes fread(fid, 4, uint8); if isequal(magic_bytes, [hex2dec(d4), hex2dec(c3), hex2dec(b2), hex2dec(a1)]) endian l; is_nano false; elseif isequal(magic_bytes, [hex2dec(a1), hex2dec(b2), hex2dec(c3), hex2dec(d4)]) endian b; is_nano false; elseif isequal(magic_bytes, [hex2dec(4d), hex2dec(3c), hex2dec(b2), hex2dec(a1)]) endian l; is_nano true; elseif isequal(magic_bytes, [hex2dec(a1), hex2dec(b2), hex2dec(3c), hex2dec(4d)]) endian b; is_nano true; else fclose(fid); error(不是标准pcap文件); end % 读取全局头其余字段 version_major fread(fid, 1, uint16, 0, endian); version_minor fread(fid, 1, uint16, 0, endian); thiszone fread(fid, 1, int32, 0, endian); sigfigs fread(fid, 1, uint32, 0, endian); snaplen fread(fid, 1, uint32, 0, endian); network fread(fid, 1, uint32, 0, endian); pcap.header struct(... version_major, version_major, ... version_minor, version_minor, ... thiszone, thiszone, ... sigfigs, sigfigs, ... snaplen, snaplen, ... linktype, network, ... is_nanosecond, is_nano, ... endian, endian); % 循环读包 pcap.packets struct([]); pktIdx 0; while true hdr fread(fid, 4, uint32, 0, endian); if numel(hdr) 4 break; end ts_sec hdr(1); ts_frac hdr(2); incl_len hdr(3); orig_len hdr(4); % 读取包数据 pktData fread(fid, incl_len, uint8uint8); if numel(pktData) incl_len warning(文件在第%d个包附近意外结束, pktIdx 1); break; end pktIdx pktIdx 1; pcap.packets(pktIdx).sec ts_sec; pcap.packets(pktIdx).frac ts_frac; pcap.packets(pktIdx).incl_len incl_len; pcap.packets(pktIdx).orig_len orig_len; pcap.packets(pktIdx).data pktData; end fclose(fid); end这个函数有几个细节值得注意fread(fid, 4, uint32, 0, endian)一次读4个uint32正好是包头里的ts_sec、ts_frac、incl_len、orig_len。pktData用uint8uint8直接以uint8类型存避免默认double类型占8倍内存。大文件建议在循环外预分配结构体数组否则每加一个包都扩充一次结构体会很慢。可以用pcap.packets(1000000) struct(...)但这里为了代码简洁没做。3.2 链路层/IP层解析拿到IP、端口和协议第二层函数把每个包的数据区解析成我们关心的信息。我写的parse_frame默认处理Ethernet链路同时兼容Raw IP链路function info parse_frame(pktData, linktype) info struct(... src_ip, , ... dst_ip, , ... protocol, , ... src_port, [], ... dst_port, [], ... payload, []); if linktype 1 % Ethernet if numel(pktData) 14 return; end ethType pktData(13) * 256 pktData(14); offset 14; % 0-based偏移用于后续IP头 % 跳过VLAN tag if ethType 0x8100 if numel(pktData) 18 return; end ethType pktData(17) * 256 pktData(18); offset 18; end if ethType ~ 0x0800 % 只解析IPv4IPv6和ARP等暂时不处理 return; end elseif linktype 101 % Raw IP offset 0; else return; end if numel(pktData) offset 20 return; end % IPv4头 ihl bitand(pktData(offset 1), 15) * 4; protocol pktData(offset 10); src_ip sprintf(%d.%d.%d.%d, pktData(offset 13:offset 16)); dst_ip sprintf(%d.%d.%d.%d, pktData(offset 17:offset 20)); info.src_ip src_ip; info.dst_ip dst_ip; l4Start offset ihl; if protocol 6 numel(pktData) l4Start 4 info.protocol TCP; info.src_port pktData(l4Start 1) * 256 pktData(l4Start 2); info.dst_port pktData(l4Start 3) * 256 pktData(l4Start 4); % TCP头部长度在l4Start12字节的低4位 tcpHeaderLen bitand(pktData(l4Start 12), 15) * 4; if numel(pktData) l4Start tcpHeaderLen info.payload pktData(l4Start tcpHeaderLen 1:end); end elseif protocol 17 numel(pktData) l4Start 8 info.protocol UDP; info.src_port pktData(l4Start 1) * 256 pktData(l4Start 2); info.dst_port pktData(l4Start 3) * 256 pktData(l4Start 4); % UDP头固定8字节 info.payload pktData(l4Start 9:end); else info.protocol sprintf(IP(%d), protocol); end end这段代码里最容易忽略的是IPv4头长度ihl。虽然大部分IP头是20字节但有些包带了Options头长度会超过20字节。如果直接写死20后面解析端口和payload全会错位。bitand(pktData(offset1), 15)*4就是从第一个字节的低4位取出IHL再乘4得到头长度。TCP的payload起始位置也不能只按20字节算它有一个Data Offset字段在TCP头的第13个字节低4位乘4才是TCP头长度。UDP头则固定8字节直接跳过就行。3.3 解析结果长什么样把两个函数组合起来遍历所有包pcap read_pcap_file(capture.pcap); linktype pcap.header.linktype; n numel(pcap.packets); tableData cell(n, 7); for i 1:n info parse_frame(pcap.packets(i).data, linktype); tableData{i, 1} datestr(datenum(1970,1,1,0,0,0) pcap.packets(i).sec / 86400, yyyy-mm-dd HH:MM:SS); tableData{i, 2} pcap.packets(i).sec pcap.packets(i).frac / 1e6; tableData{i, 3} info.src_ip; tableData{i, 4} info.dst_ip; tableData{i, 5} info.protocol; tableData{i, 6} sprintf(%d-%d, info.src_port, info.dst_port); tableData{i, 7} pcap.packets(i).orig_len; end T cell2table(tableData, VariableNames, ... {TimeStr, TimeEpoch, SrcIP, DstIP, Protocol, Ports, Length}); disp(T(1:min(20, n), :));输出就是一个很直观的表格和Wireshark看到的会话列表对得上。这里面时间戳的转换只是可读化显示真正做时间差分析时直接用sec frac/1e6算成double更合适。4. 校验与排坑为什么你读出来的数据对不上4.1 用Wireshark做交叉验证写完解析器第一件事不是跑大数据而是拿一个几十个包的小文件和Wireshark逐个包对照。Wireshark里选中一个包能看到源IP、目的IP、端口、时间戳、包长度。对照时优先看第一包和最后一包再看几个中间包。我之前踩过一次很隐蔽的坑Wireshark显示的源端口是随机高位端口我解析出来是另一个数排查半天发现是VLAN标记问题。交换机抓包很多时候会在Ethernet头后面带一个4字节的802.1Q VLAN tagEtherType变成了0x8100如果代码不做处理会把VLAN tag的前两个字节当成IP头解析出来的IP和端口自然全乱。修复方式就是上面代码里判断ethType 0x8100并跳过18字节。4.2 caplen、origlen、snaplen的区别与截断incl_len和orig_len不一致的情况虽然不常见但一旦出现就要小心。snaplen是抓包时设置的最大长度比如有些抓包工具默认只抓每个包的前128字节。如果orig_len远大于incl_len只能拿到包开头的一部分后面的payload被截断了。这时解析IP头没问题但解析TCP/UDP payload就可能越界。orig_len可以看成网络上的真实包长incl_len是文件里实际保存的字节数。做流量统计时如果要用包长画分布我建议使用orig_len如果只是读取已保存的数据则用incl_len。两者差距大时说明抓包配置不完整最好的办法是重新抓包而不是硬解析。4.3 链路类型不只是Ethernetpcap全局头里network字段决定了数据区怎么解析。我这份代码目前支持1Ethernet和101Raw IP。如果直接从USB抓包工具或者Linux下tcpdump -i any抓包linktype可能会是147USB或276Linux SLL。对于Linux SLL头部是16字节在它之后才是IP头不能直接按Ethernet的14字节跳过。USB的pcap就更特殊了里面不是以太网帧而是USB请求和响应结构需要单独写解析逻辑。遇到这种情况先确认你的pcap.header.linktype再去查对应的协议文档不要拿解析Ethernet的代码硬套。4.4 16进制转有符号数雷达数据/传感器数据的常见落地做awr2243或者其他雷达数据读取时最常见的目标是把UDP payload里的字节还原成有符号16位整数。Wireshark里看到的是FF 7C 01 23这类十六进制字节但MATLAB里需要的是数值数组。如果数据是小端有符号16位直接用payload info.payload; % uint8行向量 samples typecast(payload(:), int16);注意typecast默认按当前机器字节序MATLAB在x86机器上通常是小端所以解析小端数据没问题。如果你遇到的是大端数据需要用swapbytessamples swapbytes(typecast(payload(:), int16));这里有个常见的思维坑你看到FF 7C觉得应该是0xFF7C -132但实际小端存储时低字节在前MATLAB会把payload(1)0xFF当成低字节payload(2)0x7C当成高字节组合出来是0x7CFF 31999。所以遇到FF 7C时先想清楚你的数据源是大端还是小端再决定要不要swapbytes。5. 从“读到”到“能用”pcap转txt/CSV与信号提取5.1 批量导出成CSV/txt很多时候不需要一个完整MATLAB结构体只是想把pcap里的关键字段导出来给Python或者其他工具用。这时可以直接用writetable生成CSVwritetable(T, capture_summary.csv);如果想把原始载荷以十六进制字符串的形式写入txt可以这样fid fopen(payload_hex.txt, w); for i 1:numel(pcap.packets) info parse_frame(pcap.packets(i).data, linktype); if ~isempty(info.payload) hexStr reshape(sprintf(%02X , info.payload), 3, []); fprintf(fid, %d %s\n, i, hexStr); end end fclose(fid);这样导出的txt文件可以直接用文本工具查看也可以再次读入MATLAB做二次处理。自己做pcap转txt最大的好处是字段顺序和格式完全由你控制不像某些第三方工具给一堆用不上的列。5.2 UDP payload当作信号波形画出来这个例子很能说明“为什么非要在MATLAB里读pcap”。假设有个系统每包发512字节UDP数据前256个int16是I路数据后256个int16是Q路数据。解析出payload后只要几行代码就能画频谱payload info.payload; raw typecast(payload(:), int16); I raw(1:256); Q raw(257:512); complexSignal I 1j * Q; Fs 2e6; nfft 512; spec fftshift(fft(complexSignal, nfft)); f (-nfft/2:nfft/2-1) * Fs / nfft; plot(f, 20*log10(abs(spec)));这种流程在Wireshark里是做不到的你最多看个十六进制列表还得自己手工去拼。而用MATLAB解析pcap从文件到频谱图全程自动化适合大量回放数据。5.3 大文件性能优化思路如果pcap文件有几百MB甚至几GB直接按包循环用fread读会比较慢。有两个改进方向一次性把整个文件读成uint8数组然后用偏移指针在内存里切片避免高频fread调用。只用fread读完所有包但是不要把原始包数据都存在结构体里而是每读一个包立刻解析出需要的字段并存入预分配的数组处理完就释放原始数据。另外MATLAB的fread底层是C实现的循环次数多时主要开销在每次调用的函数切换。把包数据存在一个预分配的cell里再用cellfun批量解析速度也能提升不少。6. 不想写解析器时怎么办6.1 tshark转换如果你的目的不是学习pcap格式而是快速把包转成表格tshark是效率最高的替代方案。tshark是Wireshark的命令行版本装Wireshark时通常自带。在MATLAB里可以直接调用系统命令[status, cmdout] system(tshark -r capture.pcap -T fields -e frame.time_epoch -e ip.src -e ip.dst -e tcp.srcport -e tcp.dstport -E headery -E separator, output.csv);这样生成的CSV可以直接用readtable读进MATLAB。tshark对pcapng、各种链路层的支持比自写代码完善得多缺点是它解析出来的payload处理相对麻烦特别是自定义协议还是要自己从原始字节里取。6.2 开源MATLAB工具箱MATLAB File Exchange上也有一些pcap读取工具箱比如pcap2matlab。它们的思路一般是调用libpcap库或自带解析器比从零写代码省事。但在用之前建议确认三件事是否支持你的MATLAB版本、是否支持你的pcap时间戳精度、是否支持你的linktype。很多老工具箱对pcapng不支持对USB抓包更是无能为力。如果只是临时用这些工具箱没问题如果要把解析流程固化到项目里我还是推荐自己维护一份轻量解析器。6.3 pcapng与兼容性提醒Wireshark 3.x默认保存格式是pcapng不是pcap。上面所有解析都是针对经典pcap格式的。如果拿到一个pcapng文件最简单的处理方式是用Wireshark另存为pcap或者用tshark转换tshark -r input.pcapng -F pcap -w output.pcap转换后再丢给MATLAB解析省事又稳妥。pcapng格式虽然更灵活但结构比pcap复杂很多包含多个interface描述块、增强包块、名称解析块等自己解析的性价比不高。这也是我建议“上策用tshark转pcap中策自己写pcap解析器下策硬着头皮解析pcapng”的原因。最后再说一个我自己的使用习惯不管写什么解析脚本我都先跑一个小样本把解析结果存成MAT文件避免每次调试都重新解析几GB原始文件。pcap读取看起来是个技术点但真正的坑往往藏在链路层识别、大小端、截断包这些细节里。把这几个点看住后面不管是对接雷达数据还是网络流量分析都能少走不少弯路。
返回列表