这个练习,我建议每个搞嵌入式或者Linux网络的人都能完整走一遍。起因是帮一个朋友调一块ARM板子,内核是他们基于官方源码改的,网卡驱动用了厂商的闭源模块,上位机通过UDP持续传数字量,结果就是莫名其妙地断流。我第一反应是网线、是交换机的协商问题,甚至是DMA的缓存一致性出问题了,折腾了一下午,最后在抓包工具里一眼看出根因——板子的IP段和上位机压根不在一个子网里,掩码配置错了一位。
从那次之后,我就一直在想怎么把“抓包”这个技能讲透。很多人以为抓包就是打开Wireshark点一下开始,然后看列表里有没有红色的包。但真正的嵌入式网络调试,抓包的功夫在包外面:你得懂子网掩码是怎么把一个IP包送进该去的网段,你得知道网卡驱动从硬件拿到包之后经过了什么路径才交到协议栈手里,你得清楚在哪个层次能抓到什么、抓不到什么。这三个知识点,恰好被“一次抓包练习”串成了一条完整的链路。
这篇文章就是把我的练习过程完整还原出来:从QEMU虚拟化环境搭建、嵌入式内核镜像准备,到子网划分的计算与配置,再深入到驱动层和协议栈看数据包的真实走向,最后是tcpdump和Wireshark的实战操作与踩坑记录。适合正在入门Linux驱动开发的人、被网络问题折磨的嵌入式工程师,以及那些想真正把抓包从“会用工具”提升到“能定位问题”的人。
1. 练习的整体设计与思路
1.1 抓包这件事能拆成几个层次
抓包不是单一动作,你在不同位置下手,看到的东西完全是两个世界。我的经验是把它分成三层来理解。
最上面一层是应用层抓包,典型的是Fiddler、Charles这类代理工具,以及各种小程序抓包、APP抓包工具。它们的原理是把自己注册成系统代理,所有HTTP/HTTPS流量都从它这里过一遍,所以能直接看到请求URL、响应体、Cookie这些业务数据。这一层的好处是贴近业务逻辑,非常适合调试接口联调、登录态、支付回调这类问题。但它的视野非常窄——只能看到本机上经过代理的流量,像底层UDP包、ARP请求、ICMP这类的全看不见,更看不到驱动或者硬件层面的行为。
中间一层是协议栈层抓包,也就是大家最熟悉的tcpdump和Wireshark。Linux上它们通过libpcap库访问AF_PACKET套接字族,在网卡驱动把数据包送进内核协议栈的入口处就把报文复制了一份。这一层能看到的覆盖面就大多了:可以抓所有进出网卡的包,不管协议是什么,不管最终是发给哪个进程还是被内核直接丢弃。ARP、ICMP、TCP握手、UDP组播全部一览无余。做网络排障、协议分析、子网配置验证,都是在这一层上进行。
再往下一层是驱动层抓包。这里没有现成的工具能一键完成,通常靠的是在驱动代码里加探针、加计数器,或者用kprobe/ftrace这些内核动态追踪手段挂在特定函数上。这一层能看到的是协议栈层面看不到的信息:网卡通过DMA把数据写到哪个内存地址、硬件环(Ring Buffer)里有哪些描述符、驱动在收包过程中丢弃了多少包、校验和卸载(Checksum Offload)有没有出问题。这些问题直接决定“包到底进没进内核”,如果协议栈层抓不到包,问题大概率就藏在驱动和硬件的交界处。
这三层的关系很像物流快递:应用层抓包是盯着快递员手里的面单看,协议栈层抓包是蹲在分拣中心门口数进出车辆,驱动层抓包则是直接拆开货车看装货清单。面单丢了可以在分拣中心找,分拣中心没记录就得查装货环节了。
1.2 为什么要把子网、内核、驱动放在一次练习里
我见过太多人学会Wireshark操作之后,碰到真实网络问题依然一头雾水。原因很简单:抓包工具只能告诉你“发生了什么”,不能告诉你“为什么会这样”。而答案往往藏在子网计算和驱动的细节里。
举一个最常见的场景:两个设备配置了相同网段的IP,一个在192.168.125.0/24,另一个配成了192.168.125.0/25,表面看都在同一个网段里,但实际通信时第三个设备就会有一半地址永远不通。抓包能看到ARP请求满天飞,却始终没有响应。这时候如果不懂子网划分的二进制与运算逻辑,你根本无法理解为什么包会消失。
驱动层面的情况更隐蔽。我曾经遇到过一个网卡在内核升级之后吞吐率暴跌的问题,应用层看着TCP重传不断,协议栈抓包却看到数据确实到了网卡驱动。最后在驱动代码里加了计数器才发现是驱动对多队列的支持失效,所有流量压在一个RSS队列上。这种问题,在应用层抓包一百次都找不到头绪。
所以这次练习我专门把三个点揉在一起:先用子网配置的小实验制造问题,再用抓包工具观察现象,最后沿着内核网络路径追到驱动层去解释根因。走完这一圈,你再看任何抓包结果都不会只停留在“有没有红色标记”的层面了。
2. 环境准备:虚拟机、内核和串口驱动
2.1 为什么选QEMU而不是实体机
练习网络抓包,环境选择很关键。用实体机做实验的好处是真实,但坏处也很明显:不好加断点、改内核参数可能要刷机、网卡型号和驱动行为不可控。我用的是QEMU虚拟机方案,一句话概括就是“软件定义硬件”,这对练习来说是极大的优势。
Linux内核虚拟化技术已经非常成熟,用QEMU可以直接指定虚拟网卡的型号。比如最常用的virtio-net,它本身就是一个标准的半虚拟化网卡驱动模型,驱动代码在Linux内核源码的drivers/net/virtio_net.c里,想读源码随时可以翻。也可以指定e1000这种模拟的Intel网卡,这样还能顺带看看老牌千兆网卡驱动的代码结构。无论选哪个,都比在真实硬件上瞎猜强太多。
我用的启动命令大致长这样:
qemu-system-arm -M vexpress-a9 \ -kernel zImage \ -dtb vexpress-v2p-ca9.dtb \ -drive file=rootfs.ext4,format=raw \ -append "root=/dev/mmcblk0 rw console=ttyAMA0" \ -netdev tap,id=net0,ifname=tap0,script=no,downscript=no \ -device virtio-net-device,netdev=net0这里的关键在于-netdev接一个tap设备。tap设备相当于在宿主机上开了一个虚拟网卡的口子,虚拟机通过virtio-net设备收发数据,和宿主机的tap0接口之间形成了一个虚拟的“网线连接”。这样我在宿主机上对tap0接口抓包,看到的就是虚拟机的完整收发流量。
另一个好处是快照和回滚。做子网错配实验的时候,我可以在一个干净的虚拟机状态下手动改坏网络配置,练完一遍直接恢复快照重新来,不心疼。这对反复练习排查流程特别友好。
2.2 串口调试与内核镜像的准备
虚拟机网络通之前,得先保证能进系统控制台。QEMU默认把串口重定向到stdio,我刚才的命令里console=ttyAMA0就是这个用途,板级串口直接在终端里操作。但如果你在真实板子上做同样的事,就绕不开USB转串口模块和对应的驱动。
调试嵌入式板卡最常见的是CH340和CP2102这两种USB转串口芯片。前者常见于各种开发板自带调试口,后者多在老款周立功调试器和部分工控板卡上。Linux下CH340驱动内核自带的,插上之后一般会生成/dev/ttyUSB0节点,用minicom或者picocom直接连就行。Windows下就得手动装驱动,不过芯片厂商官方驱动做得已经很省事了。JLink和STLink这类调试器同理,它们本身也带虚拟串口功能,驱动装好后同样会暴露成串口设备。
这里想提醒新手一件事:串口驱动装好了连不上,十有八九是波特率、数据位这些参数没对齐,而不是驱动坏了。嵌入式Linux系统调试串口最常见的配置是115200-8-N-1。检查完这个再去看驱动状态,别上来就重装驱动。
内核镜像准备方面,我习惯用内核源码直接编一个调试版内核。练习用的配置不需要很全,但有几个选项必须开:CONFIG_PACKET(用于tcpdump的AF_PACKET套接字)、CONFIG_NET(基础网络协议栈)、CONFIG_INET(TCP/IP)、CONFIG_IP_MULTICAST(如果后面要做组播实验)。编译的时候建议打开CONFIG_DEBUG_INFO,后续用kprobe、ftrace看内核符号表会方便很多。
2.3 网络拓扑规划
环境搭好之后,我给练习准备了一个三节点的拓扑:一个QEMU虚拟机和宿主机上的两个网络命名空间。宿主机上创建两个netns,与虚拟机通过不同网段的tap接口互联,这样可以模拟出跨网段通信和多子网隔离的效果。
规划地址时我把整个练习实验分成几个独立网段:
虚拟机eth0: 192.168.115.10/24 -> 对应宿主机tap0 netns red: 192.168.115.20/24 -> 对应宿主机veth-red netns blue: 192.168.125.100/25 -> 对应宿主机veth-blue 宿主机tap0: 192.168.115.1/24虚拟机在网段A里和red互访,虚拟机通过宿主机的IP转发能力去访问blue所在的网段B。这样实验就能覆盖同一个广播域内的ARP通信、跨子网的路由转发、以及子网掩码错配时三种不同的丢包现象。准备阶段一定要把每个接口的MAC地址记录下来,因为后面驱动层抓包需要靠MAC地址区分不同来源的报文。
3. 子网划分:从原理到实操
3.1 子网掩码的底层逻辑
很多人记子网划分靠背表:/24是255.255.255.0,/25是255.255.255.128,/26是255.255.255.192。背表没有错,但一旦地址段变了、或者出现非标准掩码,你就得理解它真正的计算方式。
子网掩码干的活其实是一件事:把IP地址拆成“网络部分”和“主机部分”。方法很机械——把IP地址和掩码都转成二进制,然后逐位做与运算,掩码为1的位保留下来,掩码为0的位全部清零,得到的结果就是网络号。
拿192.168.115.10/24举例。IP地址的二进制是:
11000000 10101000 01110011 00001010/24意味着掩码前面24位都是1,后面8位是0。与运算之后,前24位原样保留,后8位清0,网络号就是192.168.115.0。这台设备在判断目标192.168.115.20是否和自己同网段时,就拿后面的20也做同样运算,看网络号是不是192.168.115.0。如果是,直接ARP开干;不是,就得把包扔给默认网关。
我建议这个运算至少要自己手算过一轮,不要依赖工具。因为后续排查网络问题时,别人报一个IP和掩码,你要在三秒内反应出它属于哪个子网、广播地址是多少、可用主机范围多宽。这个心算能力直接决定排障速度。
3.2 手工计算与工具辅助
手工计算的核心是记住几个基准点。以192.168.x.x段为例:
/24掩码0xFFFFFF00,主机位8位,一个子网里面有256个IP,可用主机254个。这个最常用,不多说。
/25掩码0xFFFFFF80,主机位7位,相当于把一个C类网段切成两半。第一个子网192.168.115.0/25覆盖0到127,第二个子网192.168.115.128/25覆盖128到255。每个子网可用主机数是126个。
/26掩码0xFFFFFFC0,主机位6位,一个C类切成四份。每份64个IP,可用主机62个。
/30掩码0xFFFFFFFC,主机位2位,每个子网只有4个IP,可用主机只有2个,专门用于点对点链路。
我自己练习时,会先在纸上画一张这样的表:
| 掩码位数 | 子网掩码 | 子网内IP总数 | 可用主机数 | 切分数量(C类) |
|---|---|---|---|---|
| 24 | 255.255.255.0 | 256 | 254 | 1 |
| 25 | 255.255.255.128 | 128 | 126 | 2 |
| 26 | 255.255.255.192 | 64 | 62 | 4 |
| 27 | 255.255.255.224 | 32 | 30 | 8 |
| 30 | 255.255.255.252 | 4 | 2 | 64 |
画完这轮,再配合子网计算工具去验证结果。网上有很多现成的子网计算器,输入IP和掩码之后会直接给网络号、广播地址、可用范围。但你要做的是先用脑算结果,再用工具核验,而不是反过来。我见过太多人用工具算出来192.168.115.0/25的广播地址是192.168.115.128,这就是没搞清楚主机位全1才是广播地址。
顺带说一句,VLAN和子网经常会被摆在一起讲,但它们是两层的东西。VLAN是二层概念,在交换机上隔离广播域;子网是三层的划分,决定IP包怎么寻路。一个VLAN里可以跑多个子网,一个子网也可以跨多个VLAN。搞混这个会在排查问题时走很多弯路。
3.3 掩码错配实验:抓包看一场ARP哑剧
这个实验是整个练习里我觉得最有教学价值的环节。场景极其简单,但现象非常迷惑人。
我开两个QEMU虚拟机,分别叫A和B。A配置192.168.125.10/24,B配置192.168.125.20/25。注意A和B的IP地址本身没有冲突,都在192.168.125.x这个网段里,但子网掩码不同。
A的/24网段是192.168.125.0,广播地址192.168.125.255;B的/25网段是192.168.125.0,广播地址192.168.125.127。看起来两个网络号一样?其实细节在主机部分。
在A的视角里,它计算B的地址192.168.125.20和自己同网段,于是直接发ARP请求询问“谁是192.168.125.20”。这个ARP请求以广播形式发出去,源IP是192.168.125.10,目标IP是192.168.125.20,以太网目的地址是ff:ff:ff:ff:ff:ff。
在B的视角里,它收到一个源IP为192.168.125.10的ARP请求。它先用自己的掩码/25去算这个源地址的网络号,算出来192.168.125.0,和自己网络号一致,所以它会正常响应,告诉所有人“我是192.168.125.20”。
到这里还没有问题。问题出在A第二次发数据的时候。A会把这个ARP响应缓存下来,然后尝试往B发送实际的数据包。但更要命的场景是主机数超过126个的情况:如果C也在这个网段里,配的是192.168.125.130/25,A用/24掩码算C的网络号发现还是同一个网段,于是A会向C发ARP。而C用自己的/25掩码算A的网络号时,A的地址192.168.125.10落在C这个子网的第一个分段里,所以C又认为A和自己同网段,响应了。
但真正诡异的现象是参考B和C同时存在的情形:A发ARP广播找192.168.125.130,B收到这个广播后用自己的掩码做计算,发现目标地址192.168.125.130不在自己的子网范围内,于是B不会去理这个ARP——它甚至不会缓存在ARP表里,更不会代理响应。所以A能听到C的响应,所有设备也能看到A的ARP广播,但B会彻底无视这个和它同一网段的IP。如果你在B上抓包,能看到广播帧进来了,却看不到B有任何反应。
我管这个现象叫“ARP哑剧”。在抓包结果里,你会看到大量重复的ARP Request广播,但响应者寥寥,或者干脆没有。很多人在这一步就直接开始怀疑网线、防火墙了。实际上就是掩码不一致导致对“谁是目标设备”的判断出现了偏差。
跑这个实验时,tcpdump的抓包命令是:
tcpdump -i eth0 -e -n -vvv arp-e参数很重要,它会把链路层的MAC地址打出来。你会在输出里看到请求包的目的MAC是广播地址,而响应包的源MAC是响应设备的真实地址,目的MAC是请求者的单播地址。这种细节不用-e是看不见的。
4. 内核网络路径与驱动层抓包
4.1 数据包从网卡到socket的旅程
这一节是整个练习的深水区。我尽量用通俗的方式来拆这趟旅程,但该说的专业名词一个都不能少。
以virtio-net为例,网卡收到数据之后的第一步不是CPU去搬数据,而是网卡通过DMA把数据直接写进内存里预先分配好的缓冲区。这个缓冲区被组织成环形结构,叫Ring Buffer,里面是一个个描述符,每个描述符指向一块内存,记录包的长度、状态等信息。数据写完内存后,网卡会发送一个中断通知CPU。
高负载场景下如果每个包都中断一次,CPU会被打断得怀疑人生。所以现代驱动普遍用NAPI机制:第一次中断来了之后,驱动就在softirq(软中断)上下文中进入轮询模式,一次处理一批包,处理完再决定是继续还是重新开中断。这就是你在内核日志里偶尔看到的“eth0 NIC Link is Up”之外,驱动层面真正忙碌的地方。
轮询到的包被组织成sk_buff结构体,这是整个Linux网络协议栈的核心数据结构。它里面不仅有数据本身,还带着一整串的元数据:接收到的设备、协议类型、校验和状态、时间戳、VLAN标签等等。驱动做完必要的预处理(比如校验sk_buff的头部、处理硬件时间戳)之后,调用netif_receive_skb把这个sk_buff送进内核协议栈的大门。
netif_receive_skb进入__netif_receive_skb_core之后,会做几件事:把包交给ptype_all上注册的处理函数(tcpdump的libpcap就在这里拿到包),然后根据包的协议类型(比如IPv4是0x0800)找到ptype_base上对应的处理函数,进入IP层。IP层做完路由查找、分片重组、选项处理之后,如果是本机地址的包,会交给TCP或UDP层的socket收发队列,最终唤醒等待在socket上的进程。
这张流程图如果你在脑子里画不出来,可以类比成快递分拣中心:DMA相当于卡车卸货到传送带上,NAPI是分拣员批量取货,sk_buff是每件快递的快递单,netif_receive_skb是传送带的入口分拣员,ptype是每个分拣口上的标牌。
4.2 驱动层我们能看见什么
协议栈层抓包只能告诉你“分拣中心丢了件”,却看不到“卡车根本没卸货”。想看卸货动作,就得深入到驱动层。
首先是网卡驱动的核心回调函数集合netdev_ops。在virtio-net驱动源码里你会看到这个结构体实例:
static const struct net_device_ops virtio_net_driver_ops = { .ndo_open = virtnet_open, .ndo_stop = virtnet_close, .ndo_start_xmit = virtnet_xmit, .ndo_get_stats64 = virtnet_stats, ... };收到数据包时,DMA描述符的处理在virtnet_poll这个NAPI轮询函数里;发送数据包时,virnet_xmit会被协议栈调用。这些函数的入口和出口,就是查驱动问题的最佳挂载点。
有没有不编译内核就能看驱动状态的方法?有。ethtool是最直接的手段:
ethtool -S eth0这条命令会输出一组驱动统计计数器,里面有个关键的字段rx_dropped,表示驱动因为Ring Buffer满或者其他原因主动丢弃的包数。如果这个数字持续增长,说明驱动已经从硬件拿到数据了,但在交到协议栈之前就丢了。典型原因是处理速度跟不上到达速率,或者某个队列被单一CPU核卡住。
内核导出的统计信息在/proc/net/dev里也能看到。它给出的是累计收发包数和累计丢弃数,配合watch使用可以观察趋势。但它的颗粒度比较粗,不区分丢包发生在驱动层还是协议栈层。
我实际操作中还经常用ip -s link,它会汇总Link层的状态包括错误计数和丢弃计数。判断丢包位置的逻辑很简单:如果ip -s link显示RX dropped在涨,但是tcpdump抓到的包数没涨,说明包压根没进协议栈,问题锁定在驱动或硬件层;如果tcpdump抓到了包,但socket收不到,问题在协议栈或应用层。
4.3 驱动层调试的实用手段
遇到驱动层的疑难杂症,我的第一选择不是重新编译内核,而是先用内核自带的动态追踪工具确认函数调用路径。kprobe可以在不修改内核代码的情况下,动态地在指定函数地址挂上探针,打印参数和返回值。
一个基础的ftrace示例:
echo function > /sys/kernel/debug/tracing/current_tracer echo netif_receive_skb > /sys/kernel/debug/tracing/set_ftrace_filter echo 1 > /sys/kernel/debug/tracing/tracing_on cat /sys/kernel/debug/tracing/trace把netif_receive_skb挂上之后,每收到一个包,ftrace就记录一条记录。如果这个记录一直在增长,但你在应用层socket里收不到,那就是协议栈内部的问题;如果这个记录也停了,说明驱动没有成功把包送到这里来。
更进一步的,可以直接在驱动代码里改一行,加一个原子计数器。比如在virtnet_poll函数入口加:
static atomic_t rx_polls = ATOMIC_INIT(0); atomic_inc(&rx_polls);然后通过debugfs暴露给用户空间,就能精确看到驱动层面每秒钟有多少次轮询被调用。这对定位多队列负载不均、NAPI权重配置不合理这类问题非常有效。
还有一个值得关注的就是驱动的命名空间。有些设备厂商提供的闭源网卡驱动和特定内核版本绑定很死,换一个新内核就出兼容性问题。这类问题没有通用解法,唯一的建议是:优先选内核主线自带的驱动,先跑通业务再回头折腾优化。我在练习中用的virtio-net就是标准内核驱动,省掉了很多不必要的变量。
5. 抓包工具的实战操作
5.1 tcpdump:命令行下的主力
Wireshark是大家最熟悉的抓包工具,但我个人解决嵌入式问题的时候,八成以上时间都在用tcpdump。原因很简单:嵌入式板子上没有图形界面,而且tcpdump可以在服务器上直接跑,抓完再把pcap文件拉到本地用Wireshark分析。
我平时最常用的基础抓包命令组合是:
tcpdump -i eth0 -n -e -vvv -XX逐个拆解一下:-i指定接口,-n不做DNS反向解析,否则抓包工具会尝试把每个IP都解析成域名,卡得不行;-e显示链路层的MAC地址;-vvv打出尽可能多的协议字段;-XX则同时输出十六进制和ASCII码,方便逐字节看报文内容。
如果流量比较大,建议过滤条件务必加上。常用表达式我总结如下:
抓ICMP:
tcpdump -i eth0 icmp抓指定网段的ARP:
tcpdump -i eth0 -n arp and net 192.168.115.0/24抓TCP握手(SYN包):
tcpdump -i eth0 'tcp[tcpflags] & tcp-syn != 0'抓UDP指定端口:
tcpdump -i eth0 udp port 5005抓包保存到文件的习惯一定要养成:
tcpdump -i eth0 -w /tmp/cap.pcap -C 10 -W 10-C 10表示每个文件大小到10MB就切换,-W 10表示最多保留10个文件,自动滚动覆盖。这样在长时间抓包时不用担心磁盘被撑爆。
小技巧:抓包加一个时间戳。tcpdump默认显示的时间是秒级,精确到不够用。可以加参数:
tcpdump -i eth0 -tttt输出会带完整的年月日时分秒,配合微秒级别的抓包序号,定位时间相关的问题特别有用。
5.2 Wireshark:图形化的流量分析
tcpdump负责采集,Wireshark负责分析。pcap文件拉到本地之后,我一般按下面几个顺序操作。
第一件事是设置着色规则。Wireshark内置的默认规则里,TCP乱序、重传会被标成浅红色,ICMP错误会标成深红。第一次看的人可能觉得“好多错误”,其实大量重传并不一定是坏事,可能只是网络拥塞的正常表现。关键要看重传有没有伴随持续性的丢包。
第二件事是使用过滤语法精确定位。Wireshark的显示过滤和tcpdump的抓包过滤是两套东西,前者只影响显示不影响抓包内容。常用过滤表达式:
icmp tcp.flags.syn == 1 tcp.analysis.ack_rtt > 0.1 arp.opcode == 1 udp.port == 5005 eth.addr == 52:54:00:12:34:56第三件事是Follow TCP Stream。右键任何一条TCP流,选Follow,Wireshark会把整条流的上行下行数据重组在一起显示。这个功能特别适合调试应用层协议,可以直接看到请求响应是否按预期匹配、有没有字符串被截断,省去手工拼接的麻烦。
但Wireshark也有一个天然陷阱:如果这个pcap文件是跨子网的流量,它会根据抓包机的IP和掩码做路由判断,在包列表里显示“No response found”之类的提示。这并不一定是真的通信失败,可能是抓包机位于中间节点,只看到了单向的流量。解释现象之前,先搞清楚抓包位置在整个拓扑里的角色。
5.3 用抓包结果反向验证子网和驱动
抓包不只是拿来诊断问题,也是验证自己配置是否生效的手段。我在练习中反复用抓包结果来验证三个方面的配置状态。
验证子网配置:在虚拟机里ping宿主机tap0的IP,同时在宿主机上抓包。观察ARP请求的目标MAC是否是全f,以及响应包的源MAC是否对得上tap0的MAC地址。如果ARP响应有、ICMP却没有,说明二层通了三层配置有问题;如果ARP都出不来,就是掩码或者网关方向的问题。
验证驱动收发状态:抓包的同时监控ethtool -S的输出。每次ping一个包,观察rx_packets是否增加。如果rx_packets在涨但tcpdump抓不到,那是AF_PACKET路径的问题;如果rx_packets不涨,那就是硬件和驱动的问题。这样一个二分法能飞快缩小排查范围。
验证协议栈行为:抓包时故意发一个TTL为1的包,在Wireshark里观察ICMP Time Exceeded的回复。这个实验能验证路由查找功能是否工作正常,也能看出来内核是否启用了反向路径过滤(rp_filter),后者在异步场景下会造成稀奇古怪的丢包。
这样操作的逻辑是:每次都先用抓包定义一个“预期现象”,再拿实际现象去对照。如果对不上,就去检查设定预期时依赖的前提条件是否成立。这比漫无目的地翻统计文件高效得多。
6. 常见问题与排查技巧实录
6.1 抓不到包?先按这个顺序查
抓包工具的报错通常分为两类:一类是“权限不足”,另一类是“一个包都没抓到”。后者才是最耗时的。
我的固定排查顺序是这样的:
第一步,确认抓的是对的接口。虚拟机里除了eth0还可能有lo、eth1这样的接口,抓包前用ip addr确认当前通信走了哪个网卡。很多人对着lo抓了半天,发现真实流量都在eth0上。
第二步,确认是不是被系统安全机制拦住了。Linux的iptables或者nftables规则如果配置了DROP策略,包可能在很早的阶段就被丢弃了,驱动和协议栈根本看不到。抓包之前先清空目标链的规则,或者至少确认不是这个原因。嵌入式系统上有些出厂镜像会预置防火墙规则,这是新手最容易踩的坑。
第三步,确认网卡是否工作在半混杂模式以外的特殊状态。正常情况下接收只针对发给自己或者广播多播的包。你抓其他设备的通信流量,必须把网卡设置成混杂模式,否则绝大部分包都进不来。tcpdump通常会自动把网卡设为混杂模式,但如果你手动把网卡状态重置过,一定要重新确认。
第四步,检查硬件卸载功能。现在的网卡普遍支持校验和卸载、分片卸载、批量接收卸载。这些功能会把本来应该是小包的报文在驱动层面重组成更大的包,导致Wireshark显示的包大小和实际不符,甚至某些包被合并显示。如果怀疑是这个问题,在驱动层面关闭GRO/LRO试试:
ethtool -K eth0 gro off lro off我遇到过最诡异的一次“抓不到包”,是网卡驱动对多队列支持不完整,部分队列的包直接由硬件DMA到内存后无人处理。tcpdump挂在主队列上死活看不到流量,但业务却正常得很。最后是用ethtool -l eth0看到的队列数量和实际驱动注册的队列数不一致,才挖到驱动BUG。
6.2 典型问题速查表
我把练习过程中高概率遇到的问题整理成了一张表,方便复制到自己的笔记里:
| 现象 | 可能原因 | 排查方法 |
|---|---|---|
| ARP请求有,ICMP无 | 子网掩码不一致,路由判断错误 | 两边都检查掩码和网关 |
| 双向ARP都有,TCP握手无 | iptables规则拦截 | 清空防火墙规则重试 |
| tcpdump抓不到包,但业务正常 | 抓错接口/网卡多队列 | ip addr确认接口,ethtool -l检查队列 |
| rx_dropped持续增长 | Ring Buffer满,NAPI处理不及时 | ethtool -G调整队列深度 |
| 抓包文件巨大 | 没有过滤条件 | 用-B限制文件大小,用匹配表达式过滤 |
| 包长不符,小包变巨帧 | GRO/LRO卸载导致 | ethtool -K gro off lro off |
| 只看到请求看不到响应 | rp_filter反向路径过滤 | 检查/proc/sys/net/ipv4/conf/*/rp_filter |
| 应用层socket收不到,但抓包有 | 套接字绑错端口/地址 | 用ss -lntp核对监听状态 |
这个表不是标准答案,它代表的是我摔过跟头之后记录下来的高频场景。每次遇到新问题,我都会在表里加一行,几个月下来就成了自己的排障手册。建议你也这么做,经验不沉淀下来,下次遇到相似问题还得从头查起。
6.3 我自己踩过的几个坑
最后分享几个真实经历,算是给这次练习做个注脚。
第一个坑就是最开始提到的掩码配错。当时板子的配置文件里写的是192.168.115.10/16,上位机是192.168.115.50/24。按理说/16网段更大,覆盖了/24,板子能访问上位机,但上位机访问板子的包会走默认网关,回不到板子所在的直连网段。结果就是上位机永远ping不通板子,板子却能ping通上位机。这种不对称的连通性,几乎全是掩码或者路由问题。抓包结合两边同时进行,一眼就能解释。
第二个坑是UDP校验和卸载导致的误导。有一回用tcpdump抓包看到大量Checksum Incorrect的标记,以为网络传输出了比特错误,折腾了很久的网线和端口。后来才意识到这是网卡开启校验和卸载后的正常现象:包在驱动层面计算完校验和填充进头部之后,软件层抓到的拷贝还是填之前的值,自然显示校验错。判断这种情况很简单:看Wireshark里的IP层校验和是否正常,以及这个报错是不是一致地发生在所有包上。
第三个坑是抓包位置不完整。只抓一端的时候,很难区分是包没发出去,还是发出去了对方没回,还是对方回了但你收不到。后来我养成了双机同抓的习惯:两边同时开tcpdump,抓完对比同一段时间的报文。如果A→B方向的包在B上也有,那就是B的回包或路由问题;如果A→B方向的包在B上压根没有,那就是链路或中间设备的问题。这个习惯让很多看似玄学的现象变得一目了然。
6.4 给新手的几条实操建议
如果你现在正要开始做这样的抓包练习,我有几点建议可以让你少走弯路。
第一,练习环境尽量简单。不要一上来就搭一堆容器、加一堆网桥、搞一堆VLAN,先把两个设备用一根虚拟网线连起来,把ICMP抓明白,再慢慢加复杂度。环境越复杂,变量越多,排查问题时就越难定位。
第二,养成写抓包日志的习惯。每跑一个实验,记下时间、接口、命令、目测结果、实际结果。这听起来很啰嗦,但当你遇到“昨天还好好的今天就不通了”的问题时,日志就是你最快的恢复手段。
第三,要学会区分“典型的错误包”和“正常的非预期现象”。Wireshark里标红的并不等于故障,它只是告诉你这个包不符合常规模式。比如TCP重传,可能是网络拥堵导致丢包,也可能是接收端的处理速度跟不上导致的回压。拿到任何标红现象后,先结合业务思考,再下结论。
第四,也是最重要的一点,每次抓包前先想清楚“我期待看到什么”。如果你连预期的报文结构都说不出来,那抓包结果只会让你更困惑。反过来,一旦明确了预期,抓包工具就成了验证假设的手段,而不是盲目撞运气。
这次练习做下来,最大的收获不是学会了多少个过滤表达式,而是对整个网络链路建立了一种“透视感”。再遇到形形色色的网络问题,我会本能地在脑子里拆出子网、驱动、协议栈这几层,然后判断问题到底在哪一层。抓包只是把那层放大给你看的工具,真正的功夫,在于你是否知道该放大哪一层。