项目概述
在网络安全、协议分析和系统调试领域,能够直接地“看到”网络线缆上流动的原始数据包是一项核心能力。raw-socket-sniffer 是一个基于 C/C++ 开发的轻量级网络嗅探工具,它摒弃了对 libpcap 等第三方重量级库的依赖,直接利用 Linux 内核提供的 Raw Socket (原始套接字) 机制来捕获链路层或网络层的数据包。
该项目旨在为开发者提供一个简洁、透明的参考实现,展示如何从底层构建一个能够截获、解析并分析 TCP/IP 协议栈数据的嗅探程序。
核心技术原理
1. 什么是 Raw Socket?
在标准的 Socket 编程中,我们通常使用 SOCK_STREAM (TCP) 或 SOCK_DGRAM (UDP),此时操作系统内核会帮我们处理复杂的协议头(如 IP 头、TCP 头)。而 SOCK_RAW 允许应用程序绕过传输层,直接访问网络层(IP 层)甚至链路层(以太网层)的数据。
2. 捕获机制
raw-socket-sniffer 通过调用 socket(AF_PACKET, SOCK_RAW, htons(ETH_P_ALL)) 创建套接字。
- AF_PACKET:指定在设备驱动层直接接收数据包。
- SOCK_RAW:要求内核不对数据包进行任何处理,原封不动地交给用户空间。
- ETH_P_ALL:指示捕获所有协议类型的流量(包括 IP, ARP, ICMP 等)。
项目功能特性
- 零依赖:仅依赖标准 C/C++ 库和 Linux 系统调用,无需安装
tcpdump或wireshark的开发库。 - 全协议捕获:支持捕获以太网帧及其承载的所有上层协议。
- 实时解析:能够将二进制流实时转换为人类可读的协议字段(如源IP、目的IP、端口号、协议类型)。
- 高性能:采用直接内存映射或高效的系统调用,最大限度减少数据拷贝开销。
快速上手实例
为了让你快速理解该项目的运作方式,下面是一个基于该项目逻辑的简化实现实例。
1. 基础代码实现 (sniffer.cpp)
#include <iostream>
#include <vector>
#include <cstring>
#include <sys/socket.h>
#include <netinet/in.h>
#include <netinet/ip.h>
#include <netinet/tcp.h>
#include <netinet/if_ether.h>
#include <arpa/inet.h>
#include <unistd.h>
void process_packet(unsigned char* buffer, int size) {
// 1. 解析以太网头 (Ethernet Header)
struct ethhdr *eth = (struct ethhdr *)buffer;
// 检查是否为 IP 协议 (0x0800)
if (ntohs(eth->h_proto) == ETH_P_IP) {
// 2. 解析 IP 头 (IP Header)
struct iphdr *ip = (struct iphdr *)(buffer + sizeof(struct ethhdr));
struct sockaddr_in source, dest;
memset(&source, 0, sizeof(source));
source.sin_addr.s_addr = ip->saddr;
memset(&dest, 0, sizeof(dest));
dest.sin_addr.s_addr = ip->daddr;
std::cout << "IP Packet: " << inet_ntoa(source.sin_addr)
<< " -> " << inet_ntoa(dest.sin_addr)
<< " Protocol: " << (int)ip->protocol << std::endl;
// 3. 如果是 TCP 协议,进一步解析 TCP 头
if (ip->protocol == IPPROTO_TCP) {
struct tcphdr *tcp = (struct tcphdr *)(buffer + sizeof(struct ethhdr) + ip->ihl * 4);
std::cout << " TCP Port: " << ntohs(tcp->source)
<< " -> " << ntohs(tcp->dest) << std::endl;
}
}
}
int main() {
// 创建 Raw Socket
int sock_raw = socket(AF_PACKET, SOCK_RAW, htons(ETH_P_ALL));
if (sock_raw < 0) {
perror("Socket Error");
return 1;
}
std::cout << "Sniffer is running... Press Ctrl+C to stop." << std::endl;
unsigned char *buffer = (unsigned char *)malloc(65536);
while (true) {
struct sockaddr saddr;
int saddr_size = sizeof(saddr);
// 接收数据包
int data_size = recvfrom(sock_raw, buffer, 65536, 0, &saddr, (socklen_t*)&saddr_size);
if (data_size < 0) {
perror("Recvfrom error");
break;
}
process_packet(buffer, data_size);
}
close(sock_raw);
free(buffer);
return 0;
}
2. 编译与运行
由于 Raw Socket 需要访问网络硬件底层,必须拥有 root 权限 才能运行。
# 编译 g++ sniffer.cpp -o sniffer # 运行 (必须使用 sudo) sudo ./sniffer
深度解析:数据包的解剖过程
在 raw-socket-sniffer 项目中,数据包的解析遵循一个“剥洋葱”的过程:
- L2 (链路层) \(\rightarrow\) 检查
ethhdr。确定数据包是从哪个 MAC 地址来的,以及它承载的是什么协议(通常是 IPv4)。 - L3 (网络层) \(\rightarrow\) 偏移
sizeof(ethhdr)字节,解析iphdr。这里我们可以获取源 IP 和目的 IP,并检查协议号(TCP=6, UDP=17, ICMP=1)。 - L4 (传输层) \(\rightarrow\) 根据 IP 头的
ihl(Internet Header Length) 字段计算偏移量,解析tcphdr或udphdr。此时可以获取源端口和目的端口。 - Payload (应用层) \(\rightarrow\) 越过传输层头部,剩下的就是实际传输的业务数据(如 HTTP 请求、DNS 查询等)。
项目进阶方向
如果你想在 raw-socket-sniffer 的基础上进行扩展,可以尝试以下方向:
1. 过滤器实现 (BPF)
目前的实现是捕获所有包,这在流量巨大的环境下会导致 CPU 占用过高。可以研究 Berkeley Packet Filter (BPF),在内核空间就过滤掉不需要的包,只将感兴趣的包传给用户空间。
2. 多线程异步处理
使用一个线程专门负责 recvfrom 将数据包写入环形缓冲区 (Ring Buffer),另一个线程负责解析和打印,避免解析耗时导致丢包。
3. 协议深度解析
增加对 DNS、HTTP、TLS 握手包的解析逻辑,将原始的十六进制数据转换为可读的协议字段。
注意事项与安全警告
- 权限要求:Raw Socket 仅限 root 或具有
CAP_NET_RAW能力的用户使用。 - 法律合规:请仅在自己拥有的设备或获得授权的网络环境中进行测试。未经许可嗅探他人流量属于违法行为。
- 性能瓶颈:在万兆网卡环境下,简单的
recvfrom可能会导致严重的丢包,建议研究PACKET_MMAP机制。



还没有评论,来说两句吧...