news 2026/7/22 18:28:53

22| 手写一个迷你tcpdump

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
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 的类型,我们最常使用的套接字类型是

  • 流式 Socket(SOCK_STREAM):收发 TCP 报文
  • 数据报 Socket(SOCK_DGRAM):收发面向无连接的 UDP 报文

但如果想开发更底层的应用,比如发送自定义的 IP 报文、伪造 IP、捕获流经本机网卡的数据报文,使用这两种套接字就无法实现了。为了实现这些更底层的操作,就需要使用原始套接字(Raw Socket),它的 type 为 SOCK_RAW。

第一个参数表示套接字使用的协议族 domain,比如常见的 IPv4(AF_INET)以及 IPv6(AF_INET6),原始套接字的协议族为 AF_PACKET(或 PF_PACKET)。

第三个参数 protocol 用于指定要接收的协议类型,在抓包的场景中,可以传入 ETH_P_ALL 让原始套接字接收到网卡上所有的数据链路层数据帧,从而进行网络数据包捕获和分析。

原始套接字(Raw Socket)的读写

创建完 Raw Socket,接下来就是对这个 socket 进行读写。Raw Socket 可以认为是无连接的,因为它的编程方式和 UDP 差不多,recvfromsendto系统调用在一个原始 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 用来获取对端地址,如果不关心可以设置为 NULL,addrlen 表示 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_type

struct 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 进行网络问题的定位和分析。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/7/22 18:23:11

HDVPSS图像缩放与编码实战:多相滤波器与SD_VENC配置详解

1. 项目概述&#xff1a;HDVPSS中的图像缩放与编码核心在嵌入式视频处理系统的开发中&#xff0c;尤其是涉及数字电视、视频监控或医疗影像等领域&#xff0c;我们常常面临一个核心挑战&#xff1a;如何高效、高质量地将不同分辨率、不同格式的视频流进行处理、缩放&#xff0c…

作者头像 李华
网站建设 2026/7/22 18:23:08

嵌入式视频处理:中断映射与色度上采样的核心原理与工程实践

1. 项目概述&#xff1a;高清视频处理中的中断与色度上采样在嵌入式视频处理系统的开发中&#xff0c;尤其是面对像德州仪器&#xff08;TI&#xff09;HDVPSS&#xff08;高清视频处理子系统&#xff09;这样复杂的硬件IP时&#xff0c;有两项底层技术是驱动整个系统稳定、高效…

作者头像 李华
网站建设 2026/7/22 18:20:44

TI Tiva TM4C129x Hibernation模块实战:RTC、篡改检测与低功耗管理

1. 项目概述在嵌入式系统&#xff0c;尤其是那些对功耗极其敏感、需要长时间独立运行的设备中&#xff0c;如何实现精准的时间管理、可靠的系统安全监控以及极致的功耗控制&#xff0c;是每一位嵌入式开发者必须直面的核心挑战。无论是部署在野外的环境监测节点&#xff0c;还是…

作者头像 李华
网站建设 2026/7/22 18:19:03

AtomPePacker技术揭秘:LZMA压缩算法在PE文件保护中的应用

AtomPePacker技术揭秘&#xff1a;LZMA压缩算法在PE文件保护中的应用 【免费下载链接】AtomPePacker A Highly capable Pe Packer 项目地址: https://gitcode.com/gh_mirrors/at/AtomPePacker AtomPePacker是一款功能强大的PE文件打包工具&#xff0c;它巧妙地运用LZMA压…

作者头像 李华
网站建设 2026/7/22 18:18:12

【SVM预测】基于人工蜂群算法优化支持向量机SVM实现数据预测附Matlab代码

1 简介为确定合理的底板防水煤岩柱尺寸,减少底板突水安全事故的发生,利用支持向量机(SVM)与人工蜂群算法(ABCA)综合研究底板破坏深度问题.由于SVM训练参数惩罚因子C和核函数宽度g的选择对预测精度的影响显著,采用ABCA优化该训练参数的选择过程,建立基于SVM的底板破坏深度预测模…

作者头像 李华