Node.js网络数据包捕获利器:node_pcap详解
本文从背景与原理讲起,逐步带你了解如何安装与配置 node_pcap
,掌握基础的抓包与解包操作,并通过代码示例与 ASCII 图解,帮助你快速上手并深入使用这款强大的网络数据包捕获库。
目录
- 背景与概述
- 3.1 Linux/macOS 环境
- 3.2 Windows 环境
- 3.3 使用 npm 安装 node\_pcap
- 4.1 列出可用网卡
- 4.2 创建 Live 捕获会话
- 4.3 应用 BPF 过滤器
- 5.1 数据包结构概览
- 5.2 解码以太网帧
- 5.3 解码 IP 层与传输层
- 5.4 示例:捕获并统计 TCP 连接
- 6.1 离线 pcap 文件分析
- 6.2 捕获 HTTP 流量并打印请求头
- 6.3 捕获 DNS 查询并解析响应
- 6.4 导出统计数据到 CSV
- 7.1 从网卡到用户空间的流程
- 7.2 node\_pcap 解包示意图
- 常见问题与解决
- 总结
背景与概述
在网络安全、流量监控、性能调试等场景中,实时捕获并分析网络数据包是极为重要的一环。传统上,系统管理员会借助 tcpdump
或 Wireshark
等工具在命令行或 GUI 环境下完成抓包与分析。但对于很多基于 Node.js 的项目,如果希望在应用层直接捕获网络流量、统计访问模式,或嵌入式地进行实时流量处理,就需要在代码中调用底层抓包接口。
node_pcap
正是这样一个 Node.js 原生扩展模块,它基于广泛使用的 C 语言抓包库 libpcap(Linux 下常见,macOS 中称为 pcap
)封装而来,能够让你在 Node.js 中轻松:
- 列举本机网卡接口
- 创建实时抓包会话
- 对原始数据包进行分层解析(以太网 → IP → TCP/UDP 等)
- 以事件回调形式处理每一个捕获到的数据包
从而可以在 Node.js 生态中完成类似 tcpdump -w file.pcap
、tcpdump -i eth0 port 80
等操作,并且能够将解析结果与应用逻辑紧密结合,实时打点、报警或存储。
node\_pcap 原理与依赖
2.1 什么是 libpcap?
- libpcap 是一个开源的、跨平台的 C 语言库,用于在用户空间捕获网络接口上的数据包,并对其进行过滤。它提供了简单的 API,让开发者可以打开一个网卡设备(如
eth0
),并设置一个 BPF(Berkeley Packet Filter)过滤表达式,只捕获指定的数据包。 - Linux 下对应的工具是
tcpdump
,Windows 下则有类似的WinPcap
(或 npcap)驱动。
libpcap 的主要功能
- 设备列表:列出所有可用网络接口
- 打开接口:以“混杂模式”或“非混杂模式”打开接口以捕获数据
- BPF 过滤:编译并加载一个字符串形式的过滤表达式,只捕获感兴趣的数据包
- 读取原始包:以回调或循环方式获取原始的二进制数据包(包含以太网头、IP 头等)
- 离线分析:打开并读取
.pcap
文件,用于离线解析
2.2 node\_pcap 与 libpcap 的关系
node_pcap
是一个C++/Node-API混合实现的原生模块,通过 N-API(或在早期版本中是 nan)封装 libpcap 的核心功能,并在 JavaScript 层暴露为易于使用的 API。- 在编译阶段,
node_pcap
会链接本地系统中安装的 libpcap(macOS/Linux 时通常以系统库方式存在;Windows 下需要安装 npcap/WinPcap 并在 PATH 或指定路径中找到相应的头文件与库)。 最终安装后,你会得到一个名为
node_pcap.node
的动态库文件,Node.jsrequire('pcap')
时会加载该动态库,提供如下能力:pcap.findalldevs()
:列出网卡pcap.createSession(interfaceName, filter)
:创建实时捕获会话session.on('packet', callback)
:当捕获到数据包时触发回调,并传入解析后的数据包对象
环境准备与安装
3.1 Linux/macOS 环境
安装 libpcap
Ubuntu / Debian:
sudo apt-get update sudo apt-get install -y libpcap-dev build-essential
CentOS / RHEL:
sudo yum install -y libpcap-devel gcc-c++ make
macOS (Homebrew):
brew update brew install libpcap
安装后,可通过
pkg-config --modversion libpcap
验证版本。Node.js 与 npm
- 建议使用 Node.js ≥ 12。可从官网 https://nodejs.org 下载,或操作系统包管理器安装。
- 安装好以后,
node -v
与npm -v
应正常输出版本号。
开发工具链
- Linux:
gcc
,g++
,make
等都已包含在build-essential
或相应开发包中。 macOS:安装 Xcode Command Line Tools:
xcode-select --install
- Linux:
3.2 Windows 环境
Windows 下需要额外注意:
安装 WinPcap / Npcap
- 推荐安装 Npcap(兼容 WinPcap API,并带有 Windows 10 支持)。下载后勾选“WinPcap API-compatible Mode”。
- 安装完成后,确保
Npcap
的安装目录(包含wpcap.dll
、Packet.dll
、头文件等)在系统 PATH 或者将其路径添加到PCAP_HOME
环境变量。
安装 Windows Build Tools
- 需要 Visual Studio 的 C++ 编译环境(可安装 Visual Studio Community Edition 或仅安装 “Build Tools for Visual Studio”)。
同时,全局安装
windows-build-tools
:npm install -g windows-build-tools
- 完成后,将自动安装 Python 与 C++ 构建工具,验证命令
cl.exe
与python
可正常执行。
Node.js 与 npm
- 与 Linux/macOS 相同,使用 Node.js 官方 Windows 安装包或 nvm-windows 安装。
3.3 使用 npm 安装 node\_pcap
完成系统依赖的安装后,即可在项目中安装 node_pcap
:
mkdir node_pcap_demo
cd node_pcap_demo
npm init -y
npm install pcap --save
说明:在 npm 中,这个包名叫做
pcap
,并不是node_pcap
。require('pcap')
即可加载。
- 安装时 npm 会自动调用 node-gyp 编译本地扩展,期间会链接系统中的 libpcap(或 Windows 下的 npcap)。
- 若编译报错,请检查 libpcap 开发包是否正确安装,以及环境变量是否指向正确的库路径。
基本用法:创建抓包会话
安装并引入成功后,就可以在 Node.js 中创建一个 “实时捕获会话” 了。下面演示最基础的用法:列出网卡、打开接口、捕获数据包并打印原始信息。
4.1 列出可用网卡
首先,我们需要了解本机有哪些网络接口可用:
// list_devs.js
const pcap = require('pcap');
// pcap.findalldevs() 返回一个包含接口信息的数组
const devices = pcap.findalldevs();
console.log('可用网络设备列表:');
devices.forEach((dev, idx) => {
console.log(`${idx}: ${dev.name} — ${dev.description || '无描述'}`);
});
运行示例
node list_devs.js
输出示例(Linux):
可用网络设备列表:
0: lo — 本地回环接口
1: eth0 — Intel(R) Ethernet Connection
2: wlan0 — Intel(R) Dual Band Wireless-AC
输出示例(macOS):
可用网络设备列表:
0: lo0 — lo0
1: en0 — en0
2: awdl0 — Apple Wireless Direct Link 0
3: utun0 — utun0
dev.name
是接口名称,在后续创建会话时需要传入dev.description
一般是接口的描述或别名,便于识别
4.2 创建 Live 捕获会话
假设我们选择接口 eth0
(或在 Windows 上选择 \\Device\\NPF_{...}
格式的接口名),可以创建一个“实时捕获会话”并注册回调:
// live_capture.js
const pcap = require('pcap');
// 1. 选择要监控的接口(此处硬编码为 eth0,可根据实际改动)
const interfaceName = 'eth0';
// 2. 可选:定义一个 BPF 过滤器,只捕获 TCP 端口 80 的流量
const filter = 'tcp port 80';
// 3. 创建会话:混杂模式 enabled,snap_length 默认为 65535
const pcapSession = pcap.createSession(interfaceName, filter);
console.log(`开始在接口 ${interfaceName} 上捕获数据包,过滤器:${filter}`);
// 4. 监听 'packet' 事件
pcapSession.on('packet', (rawPacket) => {
// rawPacket 是一个 Buffer,包含原始数据包(链路层 + 网络层 + 传输层等)
console.log('捕获到一个数据包,长度:', rawPacket.buf.length);
// 也可以使用 pcap.decode.packet(rawPacket) 得到更高层次的解析对象
});
运行示例
sudo node live_capture.js
注意:捕获原始数据包需要管理员权限(Linux 下常见 sudo,macOS 相同;Windows 下需以管理员身份运行 PowerShell / CMD)。
- 当有 HTTP 流量(TCP 80)经过该接口时,你将在控制台看到“捕获到一个数据包,长度:xxx”的日志。
4.3 应用 BPF 过滤器
在上例中我们使用了简单的 BPF 过滤器 tcp port 80
,它会让 libpcap 在内核层只捕获与 TCP 80 端口相关的报文,减少用户空间接收的负载。常见的 BPF 过滤表达式包括:
tcp
:仅捕获 TCP 包udp
:仅捕获 UDP 包ip src 192.168.1.100
:仅捕获源 IP 为 192.168.1.100 的 IP 包port 443
:捕获源或目的端口为 443 的 TCP/UDP 包net 10.0.0.0/8
:捕获目标网段为 10.0.0.0/8 的数据包ether proto 0x0800
:仅捕获以太网类型为 IPv4 的包
可以在创建会话时传入不同的过滤器,随时关闭或重启会话时也可修改过滤器。下面示例在运行时动态修改过滤器:
// dynamic_filter.js
const pcap = require('pcap');
const readline = require('readline');
const interfaceName = 'eth0';
let filter = 'tcp';
let pcapSession = pcap.createSession(interfaceName, filter);
console.log(`启动捕获,接口:${interfaceName},过滤器:${filter}`);
pcapSession.on('packet', (rawPacket) => {
console.log('Packet length:', rawPacket.buf.length);
});
// 通过命令行动态修改过滤器
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout
});
console.log('输入新的 BPF 过滤器,按回车确认:');
rl.on('line', (line) => {
const newFilter = line.trim() || '';
if (newFilter) {
console.log(`修改过滤器为:${newFilter}`);
// 先关闭旧会话,再创建新会话
pcapSession.close();
pcapSession = pcap.createSession(interfaceName, newFilter);
pcapSession.on('packet', (rawPacket) => {
console.log('Packet length:', rawPacket.buf.length);
});
}
console.log('输入新的 BPF 过滤器,按回车确认:');
});
- 运行后,初始过滤器为
tcp
,此后可以任意输入新的过滤表达式,它会立即生效。
解析与处理数据包
捕获到原始包之后,通常需要对以太网、IP、TCP/UDP 等各层报文进行解析,以提取源 IP、目的 IP、端口号、协议类型、应用层数据等信息。
5.1 数据包结构概览
在典型的以太网环境中,一个原始数据包在用户空间的 Buffer 布局如下(从链路层到应用层):
┌─────────────────────────────────────────────┐
│ Ethernet Frame (14 字节) │
│ ┌──────────────┬─────────────────────────┐ │
│ │ 目标 MAC (6 B) │ 源 MAC (6 B) │ │
│ └──────────────┴─────────────────────────┘ │
│ ┌──────────────┐ │
│ │ EtherType (2 B) │ 例如 0x0800 (IPv4) │ │
│ └──────────────┘ │
│─────────────────────────────────────────────│
│ IP Header (20–60 字节) │
│ ┌──────────────────────────────────────┐ │
│ │ 版本/首部长度 (1 B) │ │
│ │ 区分服务 (1 B) │ │
│ │ 总长度 (2 B) │ │
│ │ 标识/标志/片偏移 (2 B) │ │
│ │ TTL (1 B) │ │
│ │ 协议 (1 B) 例如 6=TCP,17=UDP │ │
│ │ 首部校验和 (2 B) │ │
│ │ 源 IP (4 B) │ │
│ │ 目的 IP (4 B) │ │
│ │ 可选项 (可选,长度 0–40 字节) │ │
│ └──────────────────────────────────────┘ │
│─────────────────────────────────────────────│
│ TCP Header (20–60 字节) 或 UDP (8 B) │
│ ┌──────────────────────────────────────┐ │
│ │ 源端口 (2 B) │ │
│ │ 目的端口 (2 B) │ │
│ │ 序列号 / 校验和等 │ │
│ └──────────────────────────────────────┘ │
│─────────────────────────────────────────────│
│ 应用层 Payload │
│ ┌──────────────────────────────────────┐ │
│ │ HTTP/GTP/DNS/… 等 │ │
│ └──────────────────────────────────────┘ │
└─────────────────────────────────────────────┘
在 JavaScript 中,node_pcap
会调用低层解析器(比如 pcap.decode.packet()
),将 rawPacket
Buffer 解析成一个分层结构的 JS 对象。示例如下。
5.2 解码以太网帧
常见的解码流程为:
// decode_ethernet.js
const pcap = require('pcap');
const util = require('util');
// 创建会话
const session = pcap.createSession('eth0', 'ip'); // 只捕获 IPv4 包
console.log('监听 eth0 的 IP 数据包...');
session.on('packet', (rawPacket) => {
const packet = pcap.decode.packet(rawPacket);
// packet.link 层表示以太网帧
const ethernet = packet.link;
console.log('以太网源 MAC:', ethernet.shost.toString());
console.log('以太网目的 MAC:', ethernet.dhost.toString());
console.log('EtherType:', ethernet.ethertype);
// 如果是 IPv4
if (ethernet.ethertype === 2048) {
console.log('捕获到一个 IPv4 包');
}
// 打印完整对象,便于调试
// console.log(util.inspect(packet, { depth: 4 }));
});
pcap.decode.packet(rawPacket)
会返回一个类似下面的对象结构(省略一些字段):{ link: { dhost: Buffer.from([0x00,0x1a,0xa0,0x12,0x34,0x56]), // 6 字节 Destination MAC shost: Buffer.from([0x00,0x1b,0x21,0xab,0xcd,0xef]), // 6 字节 Source MAC ethertype: 2048, // 0x0800 = IPv4 payload: { ... } // 下一层:network层对象 } }
5.3 解码 IP 层与传输层
在上步中,我们拿到 ethernet.payload
,这通常是一个 IP 层对象,结构类似:
packet.link.payload === {
version: 4,
headerLength: 20,
totalLength: 52,
id: 12345,
protocol: 6, // 6 = TCP, 17 = UDP, 1 = ICMP
srcaddr: '192.168.1.100',
dstaddr: '93.184.216.34',
payload: { ... } // 传输层对象(TCP/UDP/ICMP)
};
TCP 解码
const pcap = require('pcap');
const session = pcap.createSession('eth0', 'tcp');
session.on('packet', (rawPacket) => {
const packet = pcap.decode.packet(rawPacket);
const ethernet = packet.link;
const ipv4 = ethernet.payload;
if (ipv4.protocol === 6) { // TCP
const tcp = ipv4.payload;
console.log(`TCP 流量:${ipv4.srcaddr}:${tcp.sport} → ${ipv4.dstaddr}:${tcp.dport}`);
console.log(`TCP 序列号:${tcp.sequenceNumber}, 确认号:${tcp.ackNumber}`);
console.log(`TCP 标志:SYN=${tcp.syn}, ACK=${tcp.ack}, FIN=${tcp.fin}`);
console.log(`TCP 载荷长度:${tcp.dataLength} 字节`);
// 如果是 HTTP 请求包,可将 tcp.data 作为 Buffer 进行解析
// console.log('TCP 数据:', tcp.data.toString());
}
});
UDP 解码
const session = pcap.createSession('eth0', 'udp');
session.on('packet', (rawPacket) => {
const packet = pcap.decode.packet(rawPacket);
const ipv4 = packet.link.payload;
if (ipv4.protocol === 17) { // UDP
const udp = ipv4.payload;
console.log(`UDP 流量:${ipv4.srcaddr}:${udp.sport} → ${ipv4.dstaddr}:${udp.dport}`);
console.log(`UDP 载荷长度:${udp.length - 8} 字节`); // length 包含 header
// udp.data 是一个 Buffer
}
});
5.4 示例:捕获并统计 TCP 连接
下面示例演示如何在一定时间内,实时统计不同源 IP 与目的 IP 组合的 TCP 连接建立次数。思路如下:
- 创建对
tcp
包的监听 - 解析 TCP 标志,仅在 SYN 且非 ACK 报文时计数(表示发起新连接)
- 使用一个 Map 结构,键为
srcIP:dstIP
,值为计数器 - 每隔 10 秒打印一次统计结果
// count_tcp_connections.js
const pcap = require('pcap');
// 用于统计的 Map
const connCount = new Map();
// 创建会话,只捕获 TCP
const session = pcap.createSession('eth0', 'tcp');
console.log('开始统计 TCP 连接建立(仅统计 SYN 且非 ACK 报文)...');
session.on('packet', (rawPacket) => {
const packet = pcap.decode.packet(rawPacket);
const ipv4 = packet.link.payload;
if (ipv4.protocol !== 6) return; // 仅 TCP
const tcp = ipv4.payload;
// 只统计 SYN 且非 ACK,即新连接的第一次握手
if (tcp.syn && !tcp.ack) {
const key = `${ipv4.srcaddr}:${tcp.sport} -> ${ipv4.dstaddr}:${tcp.dport}`;
const prev = connCount.get(key) || 0;
connCount.set(key, prev + 1);
}
});
// 每隔 10 秒打印统计结果
setInterval(() => {
console.log('--- TCP 连接统计(前10秒) ---');
for (const [key, count] of connCount) {
console.log(`${key} : ${count}`);
}
console.log('-----------------------------\n');
connCount.clear(); // 重置计数
}, 10 * 1000);
运行该脚本后,若有对 web 服务器的访问,就会看到类似以下输出:
--- TCP 连接统计(前10秒) --- 192.168.1.100:52344 -> 93.184.216.34:80 : 5 192.168.1.100:52345 -> 93.184.216.34:80 : 3 ... -----------------------------
高级功能与示例
6.1 离线 pcap 文件分析
除了实时抓包,node_pcap
还支持离线分析 .pcap
文件。示例场景:我们已经用 tcpdump -w capture.pcap
录制了一段流量,现在想在 Node.js 中读取并统计 DNS 查询次数。
// offline_pcap_analysis.js
const pcap = require('pcap');
const fs = require('fs');
const util = require('util');
// 打开一个离线文件
const pcapSession = pcap.createOfflineSession('capture.pcap', 'udp port 53');
console.log('开始离线分析 capture.pcap 中 UDP 53 端口(DNS)流量...');
let queryCount = 0;
pcapSession.on('packet', (rawPacket) => {
const packet = pcap.decode.packet(rawPacket);
const ipv4 = packet.link.payload;
const udp = ipv4.payload;
const dnsData = udp.data; // DNS 报文的 Buffer
// 简单判断是否为 DNS 查询(QR 位 0)
// DNS 报文头部第 2 个字节的最高位是 QR 标志
if (dnsData && dnsData.length >= 12) {
const flags = dnsData.readUInt16BE(2);
const qr = (flags & 0x8000) >>> 15;
if (qr === 0) { // 查询
queryCount++;
}
}
});
pcapSession.on('complete', () => {
console.log(`DNS 查询次数:${queryCount}`);
});
- 运行:
node offline_pcap_analysis.js
- 当
pcapSession
读取完文件后,会触发complete
事件,并输出查询次数。
6.2 捕获 HTTP 流量并打印请求头
下面示例演示如何捕获 HTTP 流量(TCP 80),并在每个 HTTP 请求头完整到达时,打印出请求行与 Host、User-Agent 等常见字段。思路如下:
- 过滤器:
tcp port 80
- 在 TCP 数据流中 手动拼接 分片,将连续的 TCP payload 拼在一起
- 通过正则或简单字符查找,当检测到
\r\n\r\n
(HTTP 头结束标志)时,提取头部并打印;剩余部分继续缓存
// http_header_sniffer.js
const pcap = require('pcap');
// 用于保存各个 TCP 连接的流量缓存:key = srcIP:srcPort->dstIP:dstPort
const tcpStreams = new Map();
const session = pcap.createSession('eth0', 'tcp port 80');
console.log('开始捕获 HTTP 请求头...');
session.on('packet', (rawPacket) => {
const packet = pcap.decode.packet(rawPacket);
const ipv4 = packet.link.payload;
const tcp = ipv4.payload;
const data = tcp.data;
if (!data || data.length === 0) return;
// 构建连接唯一标识符
const connKey = `${ipv4.srcaddr}:${tcp.sport}->${ipv4.dstaddr}:${tcp.dport}`;
// 初始化缓存
if (!tcpStreams.has(connKey)) {
tcpStreams.set(connKey, Buffer.alloc(0));
}
// 将当前 TCP payload 追加到缓存
let buffer = Buffer.concat([tcpStreams.get(connKey), data]);
let idx;
// 检查是否存在完整的 HTTP 头部结束标志 "\r\n\r\n"
while ((idx = buffer.indexOf('\r\n\r\n')) !== -1) {
const headerBuf = buffer.slice(0, idx + 4);
const headerStr = headerBuf.toString();
console.log('====== HTTP 请求头 ======');
console.log(headerStr);
console.log('=========================\n');
// 将已经处理的部分从 buffer 中移除
buffer = buffer.slice(idx + 4);
}
// 更新缓存
tcpStreams.set(connKey, buffer);
// 可选:清理过大的缓存
if (buffer.length > 10 * 1024 * 1024) { // 超过 10MB,重置
tcpStreams.set(connKey, Buffer.alloc(0));
}
});
- 运行:
sudo node http_header_sniffer.js
当有 HTTP 请求经过时,比如用浏览器访问某网站,就会在终端中打印类似:
====== HTTP 请求头 ====== GET /index.html HTTP/1.1 Host: example.com User-Agent: Mozilla/5.0 (…) Accept: text/html,application/xhtml+xml,… Accept-Language: en-US,en;q=0.5 Accept-Encoding: gzip, deflate Connection: keep-alive =========================
6.3 捕获 DNS 查询并解析响应
在第 6.1 节中我们演示了离线 DNS 查询计数,这里再给出一个实时捕获 DNS 请求并解析响应的示例。思路:
- 过滤器:
udp port 53
- 解析 UDP payload 为 DNS 报文,简单提取问题域名与响应 IP
- 打印出来
// dns_sniffer.js
const pcap = require('pcap');
// 用于缓存事务 ID 与查询域名对应关系
const dnsQueries = new Map();
const session = pcap.createSession('eth0', 'udp port 53');
console.log('开始监听 DNS 查询与响应...');
session.on('packet', (rawPacket) => {
const packet = pcap.decode.packet(rawPacket);
const ipv4 = packet.link.payload;
const udp = ipv4.payload;
const data = udp.data;
if (!data || data.length < 12) return; // DNS 报文最小 12 字节
// 解析 DNS 头部
const transactionId = data.readUInt16BE(0);
const flags = data.readUInt16BE(2);
const qr = (flags & 0x8000) >>> 15; // 0 = query, 1 = response
if (qr === 0) {
// 查询报文,解析域名
let offset = 12;
const labels = [];
while (true) {
const len = data.readUInt8(offset);
if (len === 0) {
offset += 1;
break;
}
labels.push(data.slice(offset + 1, offset + 1 + len).toString());
offset += len + 1;
}
const domain = labels.join('.');
dnsQueries.set(transactionId, domain);
console.log(`DNS 查询:ID=${transactionId}, 域名=${domain}`);
} else {
// 响应报文,检查是否有对应查询
const domain = dnsQueries.get(transactionId) || '';
// QDCOUNT = 1,一般只有一个问题,跳过问题段
let offset = 12;
while (data.readUInt8(offset) !== 0) {
const len = data.readUInt8(offset);
offset += len + 1;
}
offset += 5; // 跳过 0 + QTYPE(2) + QCLASS(2)
// ANCOUNT
const anCount = data.readUInt16BE(6);
for (let i = 0; i < anCount; i++) {
// 跳过 NAME(可能是指针)
const namePointer = data.readUInt8(offset);
if ((namePointer & 0xc0) === 0xc0) {
// 指针,跳过 2 字节
offset += 2;
} else {
// 其他情况(少见),简化处理
while (data.readUInt8(offset) !== 0) {
offset += data.readUInt8(offset) + 1;
}
offset += 1;
}
const type = data.readUInt16BE(offset);
const dataLen = data.readUInt16BE(offset + 8);
if (type === 1) { // A 记录
const ipBuf = data.slice(offset + 10, offset + 10 + 4);
const ip = [...ipBuf].join('.');
console.log(`DNS 响应:域名=${domain}, IP=${ip}`);
}
offset += 10 + dataLen; // 跳过 TYPE(2)+CLASS(2)+TTL(4)+RDLENGTH(2)+RDATA
}
// 清除缓存
dnsQueries.delete(transactionId);
}
});
- 运行:
sudo node dns_sniffer.js
当本地发起 DNS 查询(例如
nslookup google.com
),会看到:DNS 查询:ID=52344, 域名=www.google.com DNS 响应:域名=www.google.com, IP=142.250.72.68
6.4 导出统计数据到 CSV
假设我们想将第 5.4 节统计的 TCP 连接数据导出为 CSV 文件,以便后续在Excel或其他工具中分析。结合 Node.js 的 fs
与 csv-stringify
库即可轻松实现。
npm install csv-stringify
// count_tcp_to_csv.js
const pcap = require('pcap');
const fs = require('fs');
const stringify = require('csv-stringify');
const connCount = new Map();
const session = pcap.createSession('eth0', 'tcp');
session.on('packet', (rawPacket) => {
const packet = pcap.decode.packet(rawPacket);
const ipv4 = packet.link.payload;
if (ipv4.protocol !== 6) return;
const tcp = ipv4.payload;
if (tcp.syn && !tcp.ack) {
const key = `${ipv4.srcaddr}:${tcp.sport},${ipv4.dstaddr}:${tcp.dport}`;
connCount.set(key, (connCount.get(key) || 0) + 1);
}
});
// 每隔 10 秒输出到 CSV
setInterval(() => {
const rows = [];
for (const [key, count] of connCount) {
const [src, dst] = key.split(',');
rows.push([src, dst, count]);
}
stringify(rows, { header: true, columns: ['src', 'dst', 'count'] }, (err, output) => {
if (err) {
console.error('CSV 序列化失败:', err);
} else {
fs.writeFileSync('tcp_connections.csv', output);
console.log('已将统计结果写入 tcp_connections.csv');
}
});
connCount.clear();
}, 10 * 1000);
- 运行:
sudo node count_tcp_to_csv.js
每 10 秒将当前统计结果写入
tcp_connections.csv
,内容示例:src,dst,count 192.168.1.100:52344,93.184.216.34:80,7 192.168.1.100:52345,93.184.216.34:80,4
图解:数据包捕获流程
7.1 从网卡到用户空间的流程
以下为简化的抓包流程示意图(ASCII 版):
┌────────────────────────┐
│ 网络接口卡 (NIC) │
│ ↓ 数据帧 (以太网帧) │
├────────────────────────┤
│ 内核空间 (Kernel) │
│ - 驱动接收帧 │
│ - 交给 pcap (BPF 过滤) │
│ ┌─────────────────┐ │
│ │ libpcap/BPF │ │
│ └─────────────────┘ │
│ - 通过 socket 传给用户 │
└───────┬──────────────────┘
│
▼
┌────────────────────────┐
│ 用户空间 (Userland) │
│ Node.js 进程 │
│ - node_pcap 会话绑定 │
│ - libpcap 将原始帧 Buffer│
│ 提交给 Node 回调 │
└────────────────────────┘
- NIC 接收以太网帧:由网卡硬件捕获线缆上的电磁信号,并将以太网帧交给驱动。
- 内核空间过滤 (libpcap/BPF):若用户在创建会话时指定了 BPF 过滤器(如
tcp port 80
),则在内核空间只保留符合该表达式的帧,并丢弃其他帧。 - 通过 socket 传递给用户空间:过滤后的原始数据以裸帧形式(包含链路层头部)从内核复制到用户进程。
- node\_pcap 回调:Node.js 通过 C++ 扩展接收数据,触发 JS 中的
session.on('packet', ...)
回调,将原始 Buffer 传给 JS 层。
7.2 node\_pcap 解包示意图
当原始 Buffer 到达 JS 回调后,pcap.decode.packet(buffer)
会执行如下分层解码:
原始 Buffer (rawPacket.buf)
└─────────────────────────────────────────────────────────────┐
│
├─────────────────────────────────────────────────────────────┤
│ Step 1: 以太网解析 (Ethernet parse) │
│ - 读取前 14 字节:目标 MAC (6B)、源 MAC (6B)、EtherType (2B) │
│ - 将剩余部分作为 IP 层数据 │
│ → 构造 ethernet = { shost, dhost, ethertype, payload: ip } │
├─────────────────────────────────────────────────────────────┤
│ Step 2: IP 解析 (IPv4/IPv6 parse) │
│ - 根据 Ethernet.ethertype 判断是 IPv4 (0x0800) 还是 IPv6 (0x86DD) │
│ - 提取 IP 头部字段,如 srcaddr, dstaddr, protocol, hdrlen 等 │
│ - 计算 IP payload 的偏移:hdrlen 字节后 │
│ → 构造 ip = { srcaddr, dstaddr, protocol, payload: next } │
├─────────────────────────────────────────────────────────────┤
│ Step 3: 传输层解析 (TCP/UDP/ICMP) │
│ - 判断 ip.protocol(6=TCP, 17=UDP, 1=ICMP 等) │
│ - 若 TCP:解析 srcPort, dstPort, seq, ack, flags, data │
│ - 若 UDP:解析 srcPort, dstPort, length, data │
│ - 若 ICMP:解析类型、代码、校验等 │
│ → 构造 tcp = { sport, dport, sequenceNumber, flags, data } │
│ 或 udp = { sport, dport, length, data } │
└─────────────────────────────────────────────────────────────┘
最终 pcap.decode.packet()
返回的对象结构示例:
{
link: { // 以太网层
shost: <Buffer 00 1a a0 12 34 56>,
dhost: <Buffer 00 1b 21 ab cd ef>,
ethertype: 2048,
payload: { // IP 层
version: 4,
headerLength: 20,
totalLength: 60,
protocol: 6,
srcaddr: '192.168.1.100',
dstaddr: '93.184.216.34',
payload: { // TCP 层
sport: 52344,
dport: 80,
sequenceNumber: 123456789,
ackNumber: 987654321,
syn: true,
ack: false,
fin: false,
dataLength: 0,
data: <Buffer ...>
}
}
}
}
调试与性能优化建议
在实际项目中,连续高并发地捕获并解析大量数据包时,需要关注以下调试和性能优化点。
8.1 BPF 优化技巧
- 尽量在内核层过滤:BPF 过滤器会在内核空间根据表达式仅返回符合条件的 packet,减少用户空间(Node.js)接收和解析的压力。例如,如果只关心 HTTP 流量,就直接使用
tcp port 80
而不是先捕获tcp
再在 JavaScript 中判断端口。 避免过于复杂的表达式:有时将多个过滤表达式组合会增加 BPF 执行开销,建议分步测试,找出性能最优的表达式。例如:
- 较差:
(tcp and dst port 80) or (udp and dst port 53)
- 较优:
tcp dst port 80 or udp dst port 53
- 较差:
- 使用“快速过滤”:如果你的内核或 NIC 支持硬件加速过滤,可以在创建会话时传入
pcap.createSession(interface, {filter: expr, buffer_size: 1000000})
,让 libpcap 尝试在 NIC 上加载过滤规则(取决于平台和驱动)。
8.2 减少包处理开销
- 批量处理:如果单个报文处理非常昂贵,考虑将捕获的原始 Buffer 暂存到一个队列中,在另一个子进程或 Worker 线程中批量解包/解析,减少主线程阻塞。
- 简单日志与复杂解析分离:对于某些统计操作,仅需简单读取 IP 或端口号,可在 JS 层直接用
rawPacket.buf.slice()
读取对应字节,而无需完整调用decode.packet()
,减少内存分配与对象创建。 - 内存预分配:如果需要对每个包都创建临时 Buffer 或 Array,可考虑复用同一个 Buffer 对象或使用
Buffer.allocUnsafe()
减少 GC 负担。 - 节流或抽样:在高流量环境下,可只捕获或解析某些关键时间段的报文,用
Math.random()
或定期开关会话,降低整体负载。
常见问题与解决
“pcap is not found” 或 “Cannot find module 'pcap'”
- 原因:
npm install pcap
时编译失败或node_modules/pcap
缺失。 - 解决:确保系统已安装
libpcap-dev
(Linux)或Npcap
(Windows),删除node_modules/pcap
后重新npm install pcap
,查看编译日志定位缺失依赖。
- 原因:
权限不足:Error opening device eth0: Permission denied
- 原因:非特权用户无法捕获网卡流量。
- 解决:在 Linux/macOS 上使用
sudo node script.js
;也可使用setcap cap_net_raw,cap_net_admin+eip $(which node)
赋予 Node 进程抓包权限。
Windows 下 “cannot open capture device”
- 原因:可能未正确安装 Npcap 或未以管理员身份运行。
- 解决:确认已安装 Npcap(启用 WinPcap 兼容模式),并以管理员身份打开 PowerShell 或 CMD,再运行
node script.js
。
高流量时内存泄漏或卡顿
- 原因:捕获并解析包时,JS 层分配大量对象(
decode.packet
会创建多个层次的对象),GC 负担加重。 - 解决:仅在必要时解析,尽量先用原始 Buffer 判断关键信息再调用解包;或采用 C++ 插件二次封装,将解析放在 Native 层进行优化。
- 原因:捕获并解析包时,JS 层分配大量对象(
BPF 过滤器不起作用
- 原因:过滤表达式语法错误或不被支持。
- 解决:在命令行使用
tcpdump -d "your filter"
测试表达式;或先简化为port 80
,再逐步增加约束,直到符合需求。
总结
本文围绕 Node.js 网络数据包捕获利器:node\_pcap,从基础到高级,做了如下全面讲解:
- 背景与概述:为何要在 Node.js 中捕获原始数据包,以及
node_pcap
的优势。 - 原理与依赖:介绍 libpcap/BPF 基础,以及
node_pcap
如何在底层调用 libpcap。 - 环境准备与安装:分别说明 Linux/macOS 与 Windows 下的安装步骤,确保系统可以正确编译并加载
pcap
模块。 - 基本用法:列出网卡、创建 Live 会话、应用 BPF 过滤器,如何在 Node.js 代码中捕获基本的数据包。
- 数据包解析:详细讲解以太网层、IP 层、TCP/UDP 层的解包方法,并通过代码示例展示如何提取关键信息(MAC、IP、端口、标志位等)。
- 高级示例:展示离线 pcap 文件分析、实时抓取 HTTP 请求头、DNS 查询与响应解析以及统计数据导出到 CSV,满足常见网络分析场景。
- 图解:用 ASCII 流程图说明从网卡到用户空间的抓包流程,以及
decode.packet()
的分层解析过程。 - 调试与性能优化:提供 BPF 优化、减少解析开销、内存管理等实用建议,帮助在高流量环境下保持稳定。
- 常见问题:列举安装与运行时常见报错及对应解决方案,便于快速定位与修复。
通过本文,你应当能够:
- 快速搭建一个 Node.js 抓包环境,实时监听并解析感兴趣的网络流量
- 对原始数据包进行分层解码,准确提取以太网、IP、TCP/UDP 等头部字段
- 在代码中使用 BPF 过滤器减少无关流量,提高性能
- 在离线 pcap 文件中进行批量分析,或将统计结果导出以便后续处理
- 针对高并发场景优化数据包解析流程,避免内存泄漏和阻塞
Node.js 与 libpcap 的结合,使得原本需要 tcpdump
、wireshark
这类工具才能完成的网络抓包与分析任务,都可以嵌入到你的 JavaScript 应用中,实现实时、可编程的流量监控与处理,为网络安全、性能调优及运维自动化提供了强大手段。希望这篇详解能帮助你快速上手,并在实际项目中发挥 node_pcap
的威力。
评论已关闭