ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

C# 抓包实战:SharpPcap 解析 IP/TCP/UDP 与三次握手

C# 抓包实战:SharpPcap 解析 IP/TCP/UDP 与三次握手 简介这是一份面向C#开发者、网络管理员与安全分析人员的网络抓包工具源码基于WinPcap/Npcap或.NET Socket实现可监听指定网卡与端口捕获并解析IP、TCP、UDP数据包帮助理解协议栈细节、排查通信问题。压缩包共82个文件约1.12MB以24个cs源码文件为核心配合14个png界面截图、8个resx与4个resources资源文件、4个ico图标以及sln解决方案、csproj工程、exe可执行程序、dll依赖库和txt说明等构成可直接运行的完整工程。项目涵盖套接字编程、TCP/IP头部字段解析、数据包过滤与界面展示等知识点代码中可见主窗体、过滤选项、抓包服务等模块划分便于读者对照学习底层通信机制。目前已有734人学习下载适合希望深入网络调试与协议分析的中级开发者参考。1. 抓包这件事C# 到底能不能干、值不值得干很多人第一次听到“用 C# 抓 IP/TCP/UDP 数据包”第一反应是这不是 Wireshark 的活吗我写个上位机凭什么要自己抓包答案往往出现在你没法装 Wireshark 的那台机器上——产线工控机、客户现场只允许跑自家程序的 Windows 主机、需要把抓到的报文实时喂给业务逻辑做告警的网关。这时候你要的不是一个给人看的抓包界面而是一个能把 IP 包头、TCP 三次握手、UDP 数据报这些原始字节流拿进代码里自己解析的库。C# 能做这件事路径有三条一是基于 WinPcap/Npcap 的 SharpPcap能拿到链路层原始帧最接近 Wireshark 的能力二是 .NET 自带的 Socket 原始套接字受系统权限和协议栈限制较多三是只监听本机流量的 TcpListener/UdpClient够用但看不到别人的包。选哪条取决于你要抓的是“本机进程的流量”还是“网卡上流过的所有流量”。这篇就把这三条路讲清楚让你知道什么场景该上哪套参数怎么设坑在哪。2. 三条抓包路线的选型SharpPcap、原始套接字、Socket 监听2.1 先搞清楚你要抓的是哪一层抓包这件事第一个要问的不是“用什么库”而是“我要抓哪一层的包”。链路层以太网帧能看到 MAC 地址、VLAN 标签网络层IP 包能看到源目 IP、TTL、分片标志传输层TCP/UDP能看到端口、序列号、标志位应用层才是 HTTP、Modbus 这些业务数据。SharpPcap 走的是链路层拿到的是完整帧往上你自己剥。原始套接字一般停在网络层能拿到 IP 包头但拿不到以太网头。TcpListener/UdpClient 直接给你应用层数据中间的头全被系统吃掉了。这个分层决定了你的解析代码要写多深。如果你只是想知道“哪个 IP 在跟我的设备通信”Socket 监听就够如果你要分析 TCP 三次握手有没有完成、有没有 RST 异常断开就必须拿到 TCP 头得用 SharpPcap 或原始套接字。热词里常出现的“tcp三次握手四次挥手”“ip包头”“udp划分ip数据报片”这些分析都要求你能看到传输层和网络层的原始头Socket 监听做不到。还有一个现实约束Windows 上原始套接字从 Vista 之后被大幅限制绑定到具体 IP 的原始套接字只能收发给本机的包想抓网卡上所有流量基本走不通。所以真正要“抓网卡”的场景SharpPcap Npcap 是事实标准。Npcap 是 Wireshark 安装时可选装的驱动装完之后 SharpPcap 才能通过它拿到网卡列表和原始帧。2.2 SharpPcap 抓包的最小可运行代码先装包NuGet 上搜 SharpPcap同时要把 Npcap 驱动装上安装时勾选 WinPcap API 兼容模式。下面这段代码列出所有网卡、打开第一块可用网卡、抓 10 个包并打印基本信息。using SharpPcap; using SharpPcap.LibPcap; using PacketDotNet; // 1. 列出所有网卡确认你要抓哪一块 var devices CaptureDeviceList.Instance; foreach (var dev in devices) { Console.WriteLine(${dev.Name} | {dev.Description}); } // 2. 打开第一块网卡超时 1000ms var device devices[0]; device.Open(new DeviceConfiguration { Mode DeviceModes.Promiscuous, // 混杂模式抓经过网卡的所有帧 ReadTimeout 1000 // 读超时毫秒 }); // 3. 注册收包回调 int count 0; device.OnPacketArrival (sender, e) { var rawPacket e.GetPacket(); var packet Packet.ParsePacket(rawPacket.LinkLayerType, rawPacket.Data); var ipPacket packet.ExtractIPPacket(); if (ipPacket null) return; var tcpPacket packet.ExtractTcpPacket(); var udpPacket packet.ExtractUdpPacket(); if (tcpPacket ! null) { Console.WriteLine($[TCP] {ipPacket.SourceAddress}:{tcpPacket.SourcePort} - ${ipPacket.DestinationAddress}:{tcpPacket.DestinationPort} $Flags{tcpPacket.Flags} Seq{tcpPacket.SequenceNumber}); } else if (udpPacket ! null) { Console.WriteLine($[UDP] {ipPacket.SourceAddress}:{udpPacket.SourcePort} - ${ipPacket.DestinationAddress}:{udpPacket.DestinationPort} $Len{udpPacket.PayloadData?.Length ?? 0}); } if (count 10) device.StopCapture(); }; // 4. 开始抓主线程等回调结束 device.StartCapture(); Console.ReadLine(); device.Close();逻辑说明CaptureDeviceList.Instance拿到的是 Npcap 枚举出来的网卡dev.Name形如\Device\NPF_{GUID}选错网卡是最常见的“抓不到包”原因。DeviceModes.Promiscuous是混杂模式交换机组网下你只能看到广播和发往本机的帧想抓别人的流量得在镜像口或集线器环境。Packet.ParsePacket是 PacketDotNet 提供的解析入口它按链路层类型自动往下剥ExtractIPPacket()拿到 IP 层再ExtractTcpPacket()拿到 TCP 层。参数说明ReadTimeout设太小会导致频繁空轮询设太大会让StopCapture响应变慢1000ms 是常用折中。tcpPacket.Flags是个枚举Syn、Ack、Fin、Rst都在里面判断三次握手就是看Syn和Syn|Ack的配对。udpPacket.PayloadData是 UDP 载荷注意 UDP 长度字段和实际载荷长度可能因为分片不一致热词里“udp划分ip数据报片”说的就是这种情况IP 层分片后单个 UDP 报文会被拆成多个帧你在回调里看到的是分片后的帧要自己按 IP 头的 Identification 和 FragmentOffset 重组。2.3 只抓本机流量TcpListener 和 UdpClient 的边界如果你的需求只是“我的上位机跟设备之间的通信我要记下来”那根本不用上 SharpPcap。TcpListener 接受连接后NetworkStream读到的就是应用层数据UdpClient 的Receive拿到的就是一个个数据报。这条路简单、不需要驱动、不需要管理员权限但代价是你只能看到自己进程参与的连接。// TCP 服务端监听 502 端口打印每个客户端发来的原始字节 var listener new TcpListener(IPAddress.Any, 502); listener.Start(); Console.WriteLine(Listening on 502...); while (true) { var client listener.AcceptTcpClient(); var remote client.Client.RemoteEndPoint as IPEndPoint; Console.WriteLine($Client connected: {remote}); var stream client.GetStream(); var buffer new byte[4096]; int read; while ((read stream.Read(buffer, 0, buffer.Length)) 0) { // 把字节转成十六进制方便对照协议文档 Console.WriteLine($[{remote}] {BitConverter.ToString(buffer, 0, read)}); } client.Close(); }逻辑说明AcceptTcpClient阻塞等待连接每个连接单独开线程或 Task 处理是常规做法热词里“c# tcplistener 多客户端”问的就是这个简单场景用Task.Run包一层即可连接数上千再考虑异步 Accept。stream.Read返回 0 表示对端关闭这时候要Close释放。UDP 那边更简单UdpClient.Receive(ref remoteEP)一次拿一个数据报注意 UDP 不保证顺序和到达丢包是正常的。参数说明IPAddress.Any表示监听所有网卡只想监听某块网卡就填具体 IP。缓冲区大小 4096 对 Modbus 这类小报文够用如果传文件要调大。这里拿不到 TCP 头所以你看不到三次握手只能看到连接建立后的数据这是这条路线的硬边界。3. 解析 IP/TCP/UDP 头字段偏移、字节序和分片重组3.1 IP 包头的固定 20 字节怎么读拿到原始字节后自己解析 IP 头是绕不开的基本功。IPv4 头最小 20 字节字段布局是固定的版本头长占 1 字节服务类型 1 字节总长度 2 字节标识 2 字节标志片偏移 2 字节TTL 1 字节协议 1 字节头校验和 2 字节源 IP 4 字节目的 IP 4 字节。网络字节序是大端C# 里BitConverter默认小端所以要么手动移位要么用BinaryPrimitives.ReadUInt16BigEndian。using System.Buffers.Binary; // data 是从 SharpPcap 拿到的 IP 层起始字节 int ihl (data[0] 0x0F) * 4; // 头长单位是 4 字节 int totalLen BinaryPrimitives.ReadUInt16BigEndian(data.AsSpan(2)); int identification BinaryPrimitives.ReadUInt16BigEndian(data.AsSpan(4)); int flagsAndOffset BinaryPrimitives.ReadUInt16BigEndian(data.AsSpan(6)); bool moreFragments (flagsAndOffset 0x2000) ! 0; int fragmentOffset (flagsAndOffset 0x1FFF) * 8; // 单位 8 字节 byte ttl data[8]; byte protocol data[9]; // 6TCP, 17UDP var srcIp new IPAddress(data.AsSpan(12, 4)); var dstIp new IPAddress(data.AsSpan(16, 4)); Console.WriteLine($IP {srcIp} - {dstIp} proto{protocol} ttl{ttl} $id0x{identification:X4} fragOff{fragmentOffset} mf{moreFragments});逻辑说明data[0] 0x0F取低 4 位是头长因为 IP 头可能有选项字段所以不能硬编码 20。flagsAndOffset的高 3 位是标志其中第 2 位是 MFMore Fragments低 13 位是片偏移。protocol字段决定后面是 TCP 还是 UDP6 和 17 是最常见的两个值。参数说明fragmentOffset乘以 8 才是字节偏移这是 IP 协议的规定。identification同一个数据报的所有分片相同重组时按这个字段分组再按fragmentOffset排序最后一片moreFragments为 false。热词里“udp划分ip数据报片”就是这个机制UDP 本身不管分片是 IP 层在分重组也在 IP 层你的应用层代码如果直接用 Socket 收 UDP系统已经帮你重组好了只有自己解析原始帧时才需要处理。3.2 TCP 头的关键字段和三次握手识别TCP 头最小 20 字节源端口、目的端口各 2 字节序列号 4 字节确认号 4 字节数据偏移保留标志共 2 字节窗口 2 字节校验和 2 字节紧急指针 2 字节。标志位里 SYN、ACK、FIN、RST、PSH、URG 各占一位判断连接状态全靠它们。// tcpData 是 IP 载荷的起始即 TCP 头开始处 int srcPort BinaryPrimitives.ReadUInt16BigEndian(tcpData.AsSpan(0)); int dstPort BinaryPrimitives.ReadUInt16BigEndian(tcpData.AsSpan(2)); uint seq BinaryPrimitives.ReadUInt32BigEndian(tcpData.AsSpan(4)); uint ack BinaryPrimitives.ReadUInt32BigEndian(tcpData.AsSpan(8)); int dataOffset (tcpData[12] 4) * 4; // TCP 头长 byte flags tcpData[13]; bool syn (flags 0x02) ! 0; bool ackFlag (flags 0x10) ! 0; bool fin (flags 0x01) ! 0; bool rst (flags 0x04) ! 0; string state (syn !ackFlag) ? SYN : (syn ackFlag) ? SYN-ACK : (fin) ? FIN : (rst) ? RST : DATA; Console.WriteLine($TCP {srcPort}-{dstPort} seq{seq} ack{ack} {state});逻辑说明tcpData[12] 4取高 4 位是数据偏移也就是 TCP 头长度单位 4 字节。flags在tcpData[13]SYN 是 0x02ACK 是 0x10FIN 是 0x01RST 是 0x04。三次握手的识别就是看同一个四元组源 IP、源端口、目的 IP、目的端口上先出现 SYN再出现 SYN-ACK最后出现 ACK。四次挥手则是 FIN、ACK、FIN、ACK 的序列。参数说明seq和ack是 32 位无符号数会回绕做流重组时要用差值而不是直接比较大小。dataOffset之后才是应用层数据长度是 IP 总长度减去 IP 头长再减去 TCP 头长。热词里“tcp连接”“tcp三次握手”在代码层面就是这几个标志位的组合判断没有玄学就是位运算。3.3 UDP 头只有 8 字节但分片是坑UDP 头极简源端口 2 字节目的端口 2 字节长度 2 字节校验和 2 字节后面全是载荷。长度字段包含头本身所以载荷长度是长度减 8。UDP 不保证可靠也没有连接状态抓到的每个 UDP 报文都是独立的。int srcPort BinaryPrimitives.ReadUInt16BigEndian(udpData.AsSpan(0)); int dstPort BinaryPrimitives.ReadUInt16BigEndian(udpData.AsSpan(2)); int udpLen BinaryPrimitives.ReadUInt16BigEndian(udpData.AsSpan(4)); int payloadLen udpLen - 8; Console.WriteLine($UDP {srcPort}-{dstPort} payload{payloadLen} bytes);逻辑说明UDP 长度字段是 16 位最大 65535减去 8 字节头就是载荷上限。如果 IP 层发生了分片你在这个 UDP 报文里看到的载荷可能只是完整数据报的一部分udpLen仍然是完整长度但实际字节数不够这时候要靠 IP 层的分片重组逻辑先把数据拼回来再解析 UDP。参数说明UDP 校验和在 IPv4 里是可选的全 0 表示不校验IPv6 里强制。做 UDP 网络调试时热词里“udp测试工具 ascii 命令输入”这类需求本质就是把收到的字节按 ASCII 或十六进制打印出来注意区分文本协议和二进制协议二进制协议直接转字符串会乱码。4. 避坑与排查抓不到包、乱码、权限报错的真实原因4.1 现象代码跑起来一个包都抓不到原因最常见的是网卡选错。CaptureDeviceList.Instance列出的第一块网卡往往是虚拟网卡VMware、Hyper-V、Loopback不是真正跑流量的那块。其次是没装 Npcap 或装的时候没勾 WinPcap 兼容模式SharpPcap 打开设备会直接抛异常或返回空列表。还有一种情况是网卡没开混杂模式交换机组网下只能看到广播和本机流量。解决先把所有网卡的Name和Description打印出来对照ipconfig /all里的描述找到物理网卡。确认 Npcap 服务npcap在运行。如果还是抓不到用 Wireshark 在同一块网卡上抓一下Wireshark 能抓到而你的代码抓不到问题就在代码的过滤条件或打开参数上。4.2 现象抓到的中文是乱码十六进制看着对但转字符串不对原因字节到字符串的编码搞错了。网络协议里的文本不一定是 UTF-8老设备常用 GBK 或 ASCII。另外 TCP 是流一个应用层报文可能被拆到多个 TCP 段里你按单个包转字符串遇到多字节字符被切断就乱码。解决先按十六进制打印确认字节本身没错再确定编码。GBK 用Encoding.GetEncoding(GBK)需要注册CodePagesEncodingProvider。TCP 流式协议要自己维护缓冲区按协议约定的分隔符或长度字段拼完整报文再解码不能一个包一个包地转。4.3 现象Open设备时报权限错误或“无法打开适配器”原因抓包需要管理员权限普通用户运行 SharpPcap 打开网卡会被拒绝。另外如果 Npcap 安装时选了“仅管理员可访问”模式非管理员进程一律打不开。解决以管理员身份运行程序或者在项目里加app.manifest声明requireAdministrator。如果是产线部署不想每次提权重装 Npcap 时选允许普通用户访问。注意这属于系统权限配置不同 Windows 版本表现略有差异以实际报错为准。4.4 现象UDP 收到的数据比预期短或者干脆收不到原因IP 分片。发送方发的 UDP 数据报超过 MTU通常 1500 字节IP 层会分片接收端如果没等所有分片到齐就解析拿到的就是残缺数据。另外 UDP 本身不保证到达网络拥塞时丢包很正常。解决自己解析原始帧时按 IP 头的identification缓存分片等moreFragments为 false 的那片到了再拼装。用 Socket 收 UDP 时系统已经重组但如果缓冲区设小了Receive会截断数据报把缓冲区调到 65535 以上。热词里“read udp: unknown error (code10054)”这类报错在 Windows 上通常是对方端口不可达触发的 ICMP 导致的属于正常现象捕获异常忽略即可。4.5 现象TCP 分析时序列号对不上重组出来的数据错位原因TCP 序列号是字节流编号不是包编号。重传、乱序、窗口滑动都会让序列号看起来“跳变”。如果只按到达顺序拼接遇到乱序就错位。解决维护一个按序列号排序的缓冲区收到包先按seq插入正确位置再按连续区间取出可交付数据。重传的包序列号重复直接丢弃。这是 TCP 流重组的核心逻辑SharpPcap 只给你原始包重组要自己写或者用现成的 TCP 重组库。5. 进阶把抓包做成能长期跑的服务几个实用技巧5.1 用过滤器减少无效包别让回调被淹没抓包服务跑久了最大的问题是包太多回调处理不过来就丢包。SharpPcap 支持 BPF 过滤表达式在打开设备时设置让驱动层就把不要的包丢掉比在回调里判断高效得多。device.Open(new DeviceConfiguration { Mode DeviceModes.Promiscuous, ReadTimeout 1000 }); // 只抓 502 端口的 TCP以及 68 端口的 UDP device.Filter tcp port 502 or udp port 68;逻辑说明Filter是标准的 BPF 语法tcp、udp、port、host、net这些关键字都支持。过滤在驱动层生效回调收到的就是过滤后的包CPU 占用会明显下降。参数说明表达式越精确越好tcp port 502比tcp好host 192.168.1.10 and tcp port 502更精确。注意过滤器写错不会报错只是抓不到包调试时先用 Wireshark 验证表达式。5.2 把抓到的包落盘方便事后复盘实时分析容易漏把原始帧按 pcap 格式写文件事后用 Wireshark 打开复盘是排查疑难问题的后悔药。SharpPcap 自带CaptureFileWriterDevice。var writer new CaptureFileWriterDevice(capture.pcap); device.OnPacketArrival (sender, e) { writer.Write(e.GetPacket()); // 原始帧直接写保留所有层 }; device.StartCapture();逻辑说明CaptureFileWriterDevice写出来的是标准 pcap 格式Wireshark 直接能开。写入的是原始帧包含以太网头所以事后能看到所有细节。参数说明文件会一直增长长期跑要按时间或大小切分比如每小时换一个文件。写盘是 IO 操作高流量下可能成为瓶颈可以先用内存队列缓冲后台线程批量写。5.3 一个我自己的习惯先验证再优化我做过好几个抓包相关的上位机血泪经验是不要一上来就写复杂的解析和重组逻辑。先用最简单的代码把包抓下来、落盘、用 Wireshark 确认抓到的内容是对的再在这个基础上加解析。抓包本身受驱动、权限、网卡影响太大如果解析代码写了几百行才发现根本抓不到包排查起来就是黑匣子。先跑通“能抓到、能落盘、Wireshark 能打开”这条最小链路后面所有分析都建立在这个可信的原始数据上。这个习惯帮我省了很多返工希望帮到你。本文还有配套的精品资源点击获取
返回列表