ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

UDP协议实验:用nc抓包与Wireshark逐字段分析

UDP协议实验:用nc抓包与Wireshark逐字段分析

简介:这份文档面向计算机网络课程学习者与网络协议初学者,围绕UDP协议的数据格式、首部字段含义、网卡offload机制以及Linux下nc命令的使用展开,帮助读者在虚拟网络拓扑中完成一次完整的UDP通信抓包与分析实验。压缩包内共1个docx文件,约1.13MB,以图文并茂的实验报告形式组织,涵盖实验目的、分步操作记录、Wireshark抓包截图以及问题与分析等模块。资源已有180人学习,内容从创建虚拟网络拓扑、配置静态路由、关闭网卡offload,到用nc搭建UDP服务端与客户端、抓包解析源端口、目的端口、长度与校验和,并附有校验和手动计算过程,适合需要完成课程实验或巩固UDP协议细节的读者对照参考。

1. 用 nc 抓一次 UDP,比背十遍首部字段管用

很多人学 UDP 协议,止步于「无连接、不可靠、首部 8 字节」这三句话。真到线上排查一个 UDP 丢包或者校验和异常,脑子里那点概念根本不够用。这份「计算机网络实验三:UDP 协议探索和分析」的资源,走的是另一条路:用 Linux network namespace 搭一套虚拟拓扑,把两台主机、若干路由器塞进一个物理机里,再用nc发一行字符,让 Wireshark 在 tap 接口上把 UDP 用户数据报原样抓下来,逐字段对着看。它解决的是「协议格式只存在于课本、没在真实报文里验证过」这个问题,适合正在做网络协议实验的学生,也适合想补一补 Linux 网络调试基本功的从业者。整份资源围绕 UDP 数据格式、首部字段含义、网卡 offload 技术、nc命令四条线展开,实验步骤和结果记录都给到了截图级别。

2. 搭虚拟拓扑:namespace、veth 与网桥怎么串起来

这套实验的地基是 Linux network namespace。一个 namespace 就是一套独立的网络协议栈,有自己的网卡、路由表、ARP 缓存,彼此默认隔离。把多个 namespace 用 veth pair(虚拟网线)连起来,再挂到网桥上,就得到一台物理机内部的「多主机 + 交换机」拓扑。相比开虚拟机,namespace 启动快、资源占用低,抓包也方便,是协议实验里很常见的做法。

2.1 拓扑结构与命名约定

实验里的主机叫 ns56A、ns57C 这类名字,路由器另有编号,接口名带 tap 前缀(如 tap57C),说明拓扑里用了 tap 设备或者 veth 的一端按主机名命名。搭建时我一般按「先建 namespace,再建 veth,最后接网桥」的顺序来,脚本化之后重复执行不容易乱。核心命令就这几条:

# 创建两个主机 namespace 和两个路由器 namespace ip netns add ns56A ip netns add ns57C ip netns add router1 ip netns add router2 # 建一对 veth,一端放进 ns56A,另一端放进 router1 ip link add veth56A type veth peer name vethR1a ip link set veth56A netns ns56A ip link set vethR1a netns router1 # 在 namespace 内启用接口并配 IP ip netns exec ns56A ip link set veth56A up ip netns exec ns56A ip addr add 192.168.56.126/24 dev veth56A ip netns exec router1 ip link set vethR1a up ip netns exec router1 ip addr add 192.168.56.1/24 dev vethR1a

每建一对 veth 就立刻up并配地址,是为了避免后面排查时面对一堆 down 状态的接口。ip netns exec <ns> <cmd>是进入某个 namespace 执行命令的标准写法,等价于先ip netns exec <ns> bash再在里面敲命令。实验步骤里让你「用 bash 把终端窗口模拟成主机 ns56A 的终端」,本质就是起一个绑定在该 namespace 上的 shell。

2.2 验证拓扑:三条命令别省

拓扑建完必须验证,否则后面路由不通你会怀疑人生。实验里给了三条命令,我建议一条不落地跑:

# 1. 列出所有 namespace,确认数量对得上 ip netns list # 2. 在每个 namespace 内看接口配置,确认 IP 和 up 状态 for ns in $(ip netns list | awk '{print $1}'); do echo "===== $ns =====" ip netns exec $ns ifconfig -a done # 3. 看网桥挂载了哪些接口 brctl show

ip netns list只告诉你 namespace 存在,不告诉你里面接口是否 up;ifconfig -a能看到全部接口包括 down 的,重点核对 IP 和 MTU;brctl show确认网桥的成员端口,如果某个 veth 没挂上桥,同网段主机之间就是不通。这三条命令覆盖了「对象存在、配置正确、连接正确」三个层面,缺一个都可能留下隐患。常见翻车点是 veth 建好了但忘了ip link set up,接口状态是 DOWN,ifconfig里看不到 RUNNING 标志,这时候 ping 一定不通。

3. 静态路由与 offload:让报文按你预期走、按原样抓

拓扑通了只是第一步,跨网段通信要靠路由。实验里各路由器配静态路由,让 ns56A 到 ns57C 的报文经过指定路径。这一步的意义不只是「能通」,而是让路径可控——你知道报文从哪个接口出、经过哪几跳,抓包时才能对上号。

3.1 静态路由的配置与验证

静态路由的核心是「去程和回程都要配」。只配去程,请求能到、响应回不来,是新手最常踩的坑。以 ns56A 所在网段经 router1 转发为例:

# ns56A 上:去往 192.168.57.0/24 的报文交给 router1 ip netns exec ns56A ip route add 192.168.57.0/24 via 192.168.56.1 # router1 上:去往 57 网段交给 router2,回程 56 网段直连 ip netns exec router1 ip route add 192.168.57.0/24 via 192.168.56.2 # ns57C 上:回程路由指向自己的网关 ip netns exec ns57C ip route add 192.168.56.0/24 via 192.168.57.1

via后面跟的是下一跳地址,必须和本接口同网段且可达。配完用ip netns exec <ns> ip route逐台核对,再用ping或traceroute验证路径。如果路由表里出现两条同目的网段但掩码不同的条目,Linux 按最长前缀匹配,容易配出「看起来对、实际走错」的情况,建议每条路由加完立刻ip route get <目的IP>确认实际选路。

3.2 关掉 offload:把校验和计算还给 CPU

这是整个实验里最有价值、也最容易被忽略的一步。现代网卡普遍支持 checksum offload,运输层校验和的计算与填充由网卡硬件完成,CPU 只负责把首部里的校验和字段先填个占位值。问题在于:Wireshark 在 tap 接口抓包时,抓到的是「网卡处理之前」的报文,校验和字段还是占位值,看起来像错的。实验要求关闭 offload,就是为了让 CPU 真正算出校验和,抓到的报文和课本上的格式完全一致。

# 查看当前 offload 状态 ethtool -k tap57C # 关闭校验和相关的 offload ethtool -K tap57C tx off rx off tso off gso off gro off # 再次确认,tx-checksumming 等应为 off ethtool -k tap57C | grep -i checksum

tx off关发送方向校验和卸载,rx off关接收方向,tso/gso/gro关分片与聚合相关的卸载。只关 checksum 有时不够,因为 TSO/GSO 会把大包在网卡侧才分段,抓包看到的是超长「伪报文」。全关之后抓到的才是「CPU 视角」的真实报文。这一步不做,后面手动算校验和一定对不上,血泪经验。

3.3 用 nc 发起一次 UDP 会话

nc(netcat)是实验里的通信工具,UDP 模式下服务端和客户端各一条命令:

# ns57C 上启动 UDP 服务,监听 4499 端口,-l 监听 -u UDP -v 详细输出 ip netns exec ns57C nc -lvu 4499 # ns56A 上作为客户端,向服务端 IP 和端口发数据 ip netns exec ns56A nc -u 192.168.57.254 4499

服务端-lvu三个参数分别是监听、UDP、详细模式;客户端-u指定 UDP,后面跟目的地址和端口。两边都起来后,在客户端敲一行字符回车,服务端就能收到;服务端回一行,客户端也能收到。这里有个细节:UDP 是无连接的,客户端不发数据之前,服务端根本不知道对端是谁,所以「服务端什么时候获知客户端端口」这个问题的答案是——收到第一个报文时,从首部的源端口字段读出来的。这也是实验问题 2 的考点。

4. 抓包分析:把 8 字节首部逐字段对一遍

Wireshark 在 tap57C 上抓包,过滤条件用udp.port == 4499就够。一次完整的请求响应会抓到两个 UDP 用户数据报:客户端发给服务端的、服务端回给客户端的。展开 UDP 首部,四个字段一目了然:源端口、目的端口、长度、校验和。

4.1 首部字段与实验数据对照

把实验记录里的两组数据整理成表,对照着看更清楚:

字段客户端→服务端服务端→客户端说明
源端口439904499客户端端口由系统动态分配
目的端口449943990回程目的端口即请求源端口
长度1518首部 8 字节 + 数据长度
校验和0x78b70xb454覆盖伪首部 + UDP 首部 + 数据

长度字段 15 意味着数据部分是 7 字节,对应hi,57C\n正好 7 个字符(含换行)。服务端回的 18 说明数据 10 字节。端口 43990 落在动态端口范围(通常 49152 以上,但 Linux 的ip_local_port_range默认从 32768 起,实际分配可能更低),属于临时端口,由内核在首次发送时分配。这个细节实验问题 2 专门问了,答「动态端口、操作系统临时分配」是对的。

4.2 手动验算校验和

UDP 校验和的计算分三步:构造伪首部、按 16 位字求和、取反码。伪首部包含源 IP、目的 IP、协议号(UDP 为 17)、UDP 长度,不参与传输但参与计算。以客户端报文为例:

伪首部:192.168.56.126 | 192.168.57.254 | 0x0011 | 0x000F UDP首部:0xABD6(43990) | 0x1193(4499) | 0x000F | 0x0000(计算时置0) 数据:0x6869 0x2C35 0x3743 0x0A00(奇数长度补0)

把所有 16 位字做反码和(进位回卷加到低位),结果取反就是校验和。接收方验证时,把收到的校验和也加进去再求反码和,结果为 0 即通过。实验里客户端报文校验通过,说明发送端计算正确、路径上没有篡改。手动算一遍的意义在于:你会真正理解「为什么奇数长度要补一个全 0 字节」「为什么伪首部要算进去」——因为校验和覆盖的是「伪首部 + UDP 首部 + 数据」这个整体,任何一部分变了校验和都得重算。

提示:Wireshark 里如果看到校验和显示「incorrect」,先别急着怀疑报文有问题,八成是抓包点在做 offload 的网卡上,回到 3.2 把 offload 关掉再抓。

5. 避坑与排查:五个让实验卡住的真实问题

5.1 现象:ping 不通,接口状态却是 UP

原因通常是路由只配了单向,或者某台路由器的ip_forward没开。namespace 默认继承宿主机的转发设置,但独立 namespace 里net.ipv4.ip_forward可能是 0。解决:ip netns exec router1 sysctl -w net.ipv4.ip_forward=1,逐台路由器确认,再用ip route get核对选路。

5.2 现象:Wireshark 抓到的校验和全是错的

原因就是 offload 没关干净,或者只关了tx没关rx,又或者抓包点在 veth 而非 tap 上。解决:ethtool -K <接口> tx off rx off tso off gso off gro off全关,确认ethtool -k输出里 checksum 相关项为 off,再重新抓。

5.3 现象:nc 服务端起不来,提示地址被占用

原因可能是 4499 端口已被别的进程占用,或者 namespace 里没有对应接口。解决:ip netns exec ns57C ss -lunp | grep 4499查占用,换端口或杀掉旧进程;确认nc绑定的是正确接口的地址。

5.4 现象:客户端发了数据,服务端没反应

UDP 无连接,服务端没收到不会报错。先确认服务端确实在监听(ss -lun),再确认客户端目的 IP 和端口没写错,最后在中间路由器上抓包看报文到没到。常见原因是目的 IP 写成了服务端所在网段的广播地址或写错一位。

5.5 现象:手动算校验和对不上抓包值

原因多半是伪首部里的 UDP 长度填错(要用 UDP 首部长度字段的值,不是 IP 总长度),或者数据部分奇数长度忘了补 0,又或者字节序搞反(网络字节序是大端)。解决:按「伪首部 12 字节 + UDP 首部 8 字节 + 数据补齐偶数」的顺序逐段核对,用 Python 写个小脚本验算比手算靠谱。

6. 进阶:用脚本把校验和验算自动化

手动算一次校验和是理解,每次都手算就是折磨。我习惯写个十几行的 Python 脚本,把抓包导出的十六进制串丢进去直接出结果,顺便验证自己对伪首部的理解对不对:

import struct def udp_checksum(src_ip, dst_ip, src_port, dst_port, payload: bytes): # 构造伪首部:源IP、目的IP、保留0、协议号17、UDP长度 udp_len = 8 + len(payload) pseudo = struct.pack('!4s4sBBH', src_ip, dst_ip, 0, 17, udp_len) # UDP首部,校验和字段先置0 header = struct.pack('!HHHH', src_port, dst_port, udp_len, 0) data = header + payload # 奇数长度补一个0字节 if len(data) % 2: data += b'\x00' total = 0 for i in range(0, len(data), 2): total += (data[i] << 8) + data[i + 1] total = (total & 0xFFFF) + (total >> 16) # 进位回卷 return (~total) & 0xFFFF # 用实验数据验证:源 192.168.56.126 目的 192.168.57.254 src = bytes([192, 168, 56, 126]) dst = bytes([192, 168, 57, 254]) print(hex(udp_checksum(src, dst, 43990, 4499, b'hi,57C\n')))

struct.pack('!4s4sBBH', ...)里的!表示网络字节序(大端),4s是 4 字节字符串装 IP,BBH是保留字节、协议号、长度。求和循环里每加一次就做(total & 0xFFFF) + (total >> 16),这是反码和的关键——进位要回卷加到低位,不能简单截断。最后~total & 0xFFFF取反并保留 16 位。跑出来如果和 Wireshark 里的 0x78b7 一致,说明你对伪首部和字节序的理解都对了;不一致就逐段打印中间值,八成能定位到是长度填错还是补零漏了。

这个脚本还能反过来用:把抓到的报文数据部分喂进去,算出校验和再和首部字段比对,等于给抓包结果做了一次独立验证。从那以后我每次做协议实验,抓完包都强制走一遍「脚本验算 + Wireshark 对照」,两边对上了才敢往报告里写结论。希望这套流程帮到你。

本文还有配套的精品资源,点击获取

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进