22| 手写一个迷你tcpdump
tcpdump 和 Wireshark 是网络问题定位中的神器尽管它们已经被使用了多年许多人仍然不了解它们是如何实现抓包的。这篇文章将介绍 tcpdump 背后的底层实现并展示如何模拟 tcpdump 的部分格式来抓取 8080 端口的包。最终效果如下192.168.1.2.59043 192.168.1.4.8080 Flags [S], seq 2188010653, ack 0, win 65535, length 0 192.168.1.4.8080 192.168.1.2.59043 Flags [S.], seq 500839884, ack 2188010654, win 65160, length 0 192.168.1.2.59043 192.168.1.4.8080 Flags [.], seq 2188010654, ack 500839885, win 2058, length 0 192.168.1.2.59043 192.168.1.4.8080 Flags [P.], seq 2188010654, ack 500839885, win 2058, length 80 192.168.1.4.8080 192.168.1.2.59043 Flags [.], seq 500839885, ack 2188010734, win 509, length 0为了实现上述功能我们需要一些前置知识首先需要了解原始套接字Raw Socket的概念。原始套接字Raw Socket我们来看创建套接字的socket系统调用的函数签名#include sys/socket.h int socket(int domain, int type, int protocol);其中第二个参数type表示了 socket 的类型我们最常使用的套接字类型是流式 SocketSOCK_STREAM收发 TCP 报文数据报 SocketSOCK_DGRAM收发面向无连接的 UDP 报文但如果想开发更底层的应用比如发送自定义的 IP 报文、伪造 IP、捕获流经本机网卡的数据报文使用这两种套接字就无法实现了。为了实现这些更底层的操作就需要使用原始套接字Raw Socket)它的 type 为 SOCK_RAW。第一个参数表示套接字使用的协议族 domain比如常见的 IPv4AF_INET以及 IPv6AF_INET6原始套接字的协议族为 AF_PACKET(或 PF_PACKET)。第三个参数 protocol 用于指定要接收的协议类型在抓包的场景中可以传入 ETH_P_ALL 让原始套接字接收到网卡上所有的数据链路层数据帧从而进行网络数据包捕获和分析。原始套接字Raw Socket的读写创建完 Raw Socket接下来就是对这个 socket 进行读写。Raw Socket 可以认为是无连接的因为它的编程方式和 UDP 差不多recvfrom和sendto系统调用在一个原始 Raw Socket 上接收和发送数据报。抓包的场景是从 Raw Socket 读取数据我们来重点看一下 recvfrom 函数。ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags, struct sockaddr *src_addr, socklen_t *addrlen);其中 buf 用来指定读取的数据缓冲区len 表示缓冲区的长度src_addr 用来获取对端地址如果不关心可以设置为 NULLaddrlen 表示 src_addr 结构体的长度。实战抓包先来定义以太网帧、IP 帧、TCP 帧的数据结构#define ETHER_ADDR_LEN 6 // 以太网帧头部结构体 struct EthernetHeader { u_char ether_dhost[ETHER_ADDR_LEN]; /* 目的以太网地址 */ u_char ether_shost[ETHER_ADDR_LEN]; /* 源以太网地址 */ u_short ether_type; /* 以太网类型 */ }; // IPv4 头部 struct IPv4Header { u_char ip_vhl; /* 版本号和头部长度(in words) */ u_char ip_tos; /* 服务类型 */ u_short ip_len; /* 总长度 */ u_short ip_id; /* 标识 */ u_short ip_off; /* 片偏移 */ u_char ip_ttl; /* 生存时间 */ u_char ip_p; /* 协议 */ u_short ip_sum; /* 校验和 */ struct in_addr ip_src; /* 源地址 */ struct in_addr ip_dst; /* 目的地址 */ }; // TCP 头部 struct TCPHeader { u_short th_sport; /* 源端口号 */ u_short th_dport; /* 目的端口号 */ u_int th_seq; /* 序列号 */ u_int th_ack; /* 确认号 */ u_char th_offset; /* 数据偏移 */ u_char th_flags; /* TCP flags */ u_short th_win; /* 窗口大小 */ u_short th_sum; /* 校验和 */ u_short th_urp; /* 紧急数据偏移量 */ };这样我们就可以把读到的数据直接映射到结构体上如下所示:char buf[2048]; ssize_t n recvfrom(sock_fd, buf, sizeof(buf), 0, nullptr, nullptr); auto *eth_header (struct EthernetHeader *) buf; auto *ipv4_header (IPv4Header *) (buf sizeof(EthernetHeader)) auto *tcp_header (TCPHeader *) (buf sizeof(EthernetHeader) sizeof(IPv4Header));接下来我们写一个 helper 函数用来解析tcp_flags:std::string tcp_flags(u_char flags) { std::string result; if (flags TH_FIN) { result F; } if (flags TH_SYN) { result S; } if (flags TH_RST) { result R; } if (flags TH_PUSH) { result P; } if (flags TH_URG) { result U ; } if (flags TH_ACK) { result .; } return result; }这里过滤一下包只打印端口号为 8080 的 tcp 请求完整的代码如下int main() { int sock_fd socket(AF_PACKET, SOCK_RAW, htons(ETH_P_ALL)); if (sock_fd 0) { std::cerr Failed to create socket std::endl; return 1; } while (true) { char buf[2048]; struct sockaddr addr; socklen_t addr_len sizeof(addr); ssize_t n recvfrom(sock_fd, buf, sizeof(buf), 0, nullptr, nullptr); if (n 0) { std::cerr Failed to receive data std::endl; continue; } auto *eth_header (struct EthernetHeader *) buf; ushort ether_type ntohs(eth_header-ether_type); if (ether_type ETHERTYPE_IP) { auto *ipv4_header (IPv4Header *) (buf sizeof(EthernetHeader)); if (ipv4_header-ip_p IPPROTO_TCP) { auto *tcp_header (TCPHeader *) (buf sizeof(EthernetHeader) sizeof(IPv4Header)); u_char tcp_header_length (tcp_header-th_offset 4) * 4; if (ntohs(tcp_header-th_dport) 8080 || ntohs(tcp_header-th_sport) 8080) { uint16_t tcp_header_len (tcp_header-th_offset 4) * 4; std::cout inet_ntoa(ipv4_header-ip_src) . ntohs(tcp_header-th_sport) inet_ntoa(ipv4_header-ip_dst) . ntohs(tcp_header-th_dport) Flags [ tcp_flags(tcp_header-th_flags) ], seq ntohl(tcp_header-th_seq) , ack ntohl(tcp_header-th_ack) , win ntohs(tcp_header-th_win) , length n - sizeof(EthernetHeader) - sizeof(IPv4Header) - tcp_header_length std::endl; } } } } }其中有一个值得注意的是tcp_header_length的计算方式u_char tcp_header_length (tcp_header-th_offset 4) * 4;这是因为 TCP 头中的 data offset 的值在高四位以 32-bit words 的形式来指定头部的长度。我们来编译运行一下上面的代码$ g minidump.cpp -o minidump $ sudo ./minidump接下来启动一个 tcp server监听 8080 端口这里启动一个静态文件服务器使用 curl 访问一下就可以在 minidump 的输出中看到握手、发送数据和挥手的过程了。内核层面是如何处理的前面提到的都是应用层的注册行为那内核是如何处理的呢这就要提到ptype_all链表定义在net/core/dev.c文件中struct list_head ptype_all;链表元素类型为packet_typestruct packet_type { __be16 type; struct net_device *dev; int (*func) (struct sk_buff *, struct net_device *, struct packet_type *, struct net_device *); // ...omit some... struct list_head list; };其中type 表示需要匹配的 Ethernet 协议类型比如我们前面介绍的ETH_P_ALL表示所有的协议类型。dev表示需要处理的网络设备NULL 表示所有网络设备func表示当收到包时的回调函数list 指向链表头当网络设备收到一个包时会调用net/core/dev.c文件中的__netif_receive_skb_core函数内核会遍历ptype_all链表调用deliver_skb将数据包发送给注册的抓包进程。static int __netif_receive_skb_core(struct sk_buff **pskb, bool pfmemalloc, struct packet_type **ppt_prev) // .. list_for_each_entry_rcu(ptype, ptype_all, list) { if (pt_prev) ret deliver_skb(skb, pt_prev, orig_dev); pt_prev ptype; } // .. }当使用socket(AF_PACKET, SOCK_RAW, htons(ETH_P_ALL))创建一个原始套接字时内核会将对应的packet_type结构体插入到ptype_all链表中。从这里我们能更加明显的感受到应用层能实现多少功能还得靠内核能开放多少能力。小结这篇文章介绍了抓包程序的实现原理我们需要了解 Raw Socket 编程、如何比较方便的解析各层的网络包最后如果你感兴趣你可以再深入研究一下内核底层的 ptype_all 链表是如何为 tcpdump 等抓包程序服务的。通过这些知识我们可以更好地理解和使用 tcpdump 和 Wireshark 进行网络问题的定位和分析。

相关新闻

21| 什么是 TUN/TAP 设备

21| 什么是 TUN/TAP 设备

我们先来看一下 Linux 内核中关于 tuntap 的描述,文件在 Documentation/networking/tuntap.txt, TUN/TAP provides packet reception and transmission for user space programs. It can be seen as a simple Point-to-Point or Ethernet device, which,…

2026/7/22 18:29:18阅读更多 →
机器人磁编码器充磁工艺:关键参数与核心流程解析

机器人磁编码器充磁工艺:关键参数与核心流程解析

工业机器人的关节要做精准运动,电机转子的角度和转速必须实时掌握。承担这个任务的器件叫编码器。目前主流有两类:光电式和磁电式。磁编码器现在越来越占优势。它不怕油污、不怕振动,使用寿命长,成本也比光电编码器低。中高端机器…

2026/7/22 18:29:18阅读更多 →
第一阶段-第7天 — 复习与综合练习

第一阶段-第7天 — 复习与综合练习

今日目标 回顾本周学到的所有概念完成一个综合小练习查漏补缺,巩固基础 本周知识回顾 核心概念清单概念一句话解释特征模型的输入数据标签模型要学习的正确答案权重w控制输入对输出的影响大小偏置b输入为0时的基础输出值预测模型根据当前参数给出的答案损失预测与真…

2026/7/22 18:29:18阅读更多 →
arrow.nvim vs harpoon:为什么这款Neovim书签插件更值得选择

arrow.nvim vs harpoon:为什么这款Neovim书签插件更值得选择

arrow.nvim vs harpoon:为什么这款Neovim书签插件更值得选择 【免费下载链接】arrow.nvim Bookmark your files, separated by project, and quickly navigate through them. 项目地址: https://gitcode.com/gh_mirrors/ar/arrow.nvim arrow.nvim是一款专为N…

2026/7/22 19:19:26阅读更多 →
gym-trading环境参数配置指南:优化你的交易模拟场景

gym-trading环境参数配置指南:优化你的交易模拟场景

gym-trading环境参数配置指南:优化你的交易模拟场景 【免费下载链接】gym-trading Environment for reinforcement-learning algorithmic trading models 项目地址: https://gitcode.com/gh_mirrors/gy/gym-trading gym-trading是一个专为强化学习算法交易模…

2026/7/22 19:19:26阅读更多 →
嵌入式系统GPMC内存控制器配置:从时序原理到NAND/NOR Flash实战

嵌入式系统GPMC内存控制器配置:从时序原理到NAND/NOR Flash实战

1. 项目概述与GPMC核心价值在嵌入式系统开发,尤其是基于TI Sitara系列处理器的项目中,与外部存储设备打交道是家常便饭。无论是需要存放大量图片、音频文件的NAND Flash,还是需要直接运行代码的NOR Flash,一个高效、稳定且灵活的内…

2026/7/22 19:19:26阅读更多 →
前端开发新范式:GitHub_Trending/cla/claude-skills前端框架技能

前端开发新范式:GitHub_Trending/cla/claude-skills前端框架技能

前端开发新范式:GitHub_Trending/cla/claude-skills前端框架技能 【免费下载链接】claude-skills 345 Claude Code skills & agent skills & plugins (30 Agents, 70 custom commands, 330 skills, customizable references, scripts)for Claude Code, Code…

2026/7/22 19:19:26阅读更多 →
TI GPMC预取与ECC配置实战:提升嵌入式存储性能与可靠性

TI GPMC预取与ECC配置实战:提升嵌入式存储性能与可靠性

1. 项目概述与核心价值在嵌入式系统开发,尤其是基于德州仪器(TI)Sitara系列处理器的项目中,内存访问的效率和可靠性是决定系统性能上限与稳定性的两大基石。处理器与外部存储器(如NOR Flash、NAND Flash、SRAM或FPGA&a…

2026/7/22 19:19:26阅读更多 →
EDMA3高级应用:从视频帧传输到乒乓缓冲与链式传输实战

EDMA3高级应用:从视频帧传输到乒乓缓冲与链式传输实战

1. 项目概述与EDMA3核心价值 在嵌入式系统开发,尤其是涉及音视频处理、高速数据采集或实时通信的领域,数据搬运的效率往往是整个系统性能的瓶颈。想象一下,一个640x480分辨率、每秒30帧的视频流,意味着CPU每秒钟需要处理超过900万…

2026/7/22 19:17:25阅读更多 →
Go语言静态资源打包方案对比与实践指南

Go语言静态资源打包方案对比与实践指南

1. 项目背景与核心需求在Go语言开发中,我们经常需要处理静态资源文件的打包问题。无论是Web应用的模板文件、前端资源,还是配置文件、证书等,都需要随程序一起分发。传统做法是将这些文件与编译后的二进制文件放在同一目录下,但这…

2026/7/22 0:53:59阅读更多 →
Go语言实现高性能LDAP认证服务的架构与实践

Go语言实现高性能LDAP认证服务的架构与实践

1. 项目背景与核心价值LDAP(轻量级目录访问协议)作为企业级身份认证的黄金标准,已经服务了超过80%的财富500强公司。我在金融科技领域实施统一认证体系时,发现传统Java方案存在启动慢、内存占用高等痛点。而Go语言凭借其协程并发模…

2026/7/22 0:53:59阅读更多 →
【AI面试官实战指南】:用ChatGPT模拟10类高频技术岗面试,3天提升应答精准度92%

【AI面试官实战指南】:用ChatGPT模拟10类高频技术岗面试,3天提升应答精准度92%

更多请点击: https://intelliparadigm.com 第一章:AI面试官实战指南的核心价值与适用场景 AI面试官并非替代人类HR的“黑箱工具”,而是以可解释、可审计、可迭代的方式,赋能招聘全链路的关键基础设施。其核心价值在于将主观经验沉…

2026/7/22 0:53:59阅读更多 →
中小企业小程序开发公司怎么选:预算、上手和售后避坑指南

中小企业小程序开发公司怎么选:预算、上手和售后避坑指南

中小企业做小程序,最常见的矛盾是预算有限,但又不希望功能太单薄;没有技术团队,但又希望后续能自己运营;想快速上线,又担心隐性收费和售后失联。选型时如果只看“低价套餐”或“案例数量”,很容…

2026/7/22 0:01:17阅读更多 →
GEO优化如何沉淀长期内容资产?广拓时代谈AI搜索时代的内容ROI

GEO优化如何沉淀长期内容资产?广拓时代谈AI搜索时代的内容ROI

企业做营销,最怕钱花完了,资产没有留下。 效果广告能带来一段时间的曝光,但预算停止后,流量往往也随之停止。短视频内容可能在几天内冲高,也可能很快沉下去。AI搜索时代,企业需要重新思考一个问题&#xff…

2026/7/22 0:01:17阅读更多 →
Agent 终态判定:何时该停止思考、给出最终回复

Agent 终态判定:何时该停止思考、给出最终回复

Agent 终态判定:何时该停止思考、给出最终回复 一、你的 Agent 在"再想想"的循环里绕了 12 轮,用户已经关窗口了 Agent 与人最大的区别是:人知道什么时候该停下来给答案,Agent 会一直"想"下去。你给 Agent 接…

2026/7/22 0:01:17阅读更多 →
YOLOv8推理性能优化:从1.2FPS到35FPS的全链路加速实践

YOLOv8推理性能优化:从1.2FPS到35FPS的全链路加速实践

如果你在部署 YOLOv8 时,发现推理速度只有可怜的 1-2 FPS,而别人的演示视频却能跑到 30 FPS 以上,那么问题很可能不在模型本身,而在于你的整个处理链路。很多开发者拿到一个训练好的 YOLOv8 模型后,会直接使用官方示例…

2026/7/21 22:53:50阅读更多 →
Coze与Dify对比指南:低代码AI应用开发从入门到实战

Coze与Dify对比指南:低代码AI应用开发从入门到实战

1. 从零到一:为什么你需要了解 Coze 和 Dify?如果你对 AI 应用开发感兴趣,但一看到“大模型”、“智能体”、“工作流”这些词就头疼,觉得门槛太高,那这篇文章就是为你准备的。很多开发者,包括我自己&#…

2026/7/22 18:55:50阅读更多 →
AI生图工具怎么选?2026年6月版实测对比

AI生图工具怎么选?2026年6月版实测对比

做自媒体的朋友应该都有体会:配图一直是个让人头疼的问题。2026年,AI生图工具已经非常成熟了,但工具太多反而不知道怎么选。以下是截至2026年6月我对主流AI生图工具的实测对比。Midjourney V8.1:速度之王2026年6月11日&#xff0c…

2026/7/22 18:55:50阅读更多 →