@netkitty/pcap
v1.2.0
Published
Node.js streaming read / write / parse of pcap, pcapng and .cap / tcpdump capture files — format auto-detected by magic number (classic libpcap in both endiannesses and microsecond/nanosecond variants, plus pcapng), with transparent gzip and LZ4 decompres
Maintainers
Readme
@netkitty/pcap
在 Node.js 端流式地读取、写入和解析抓包文件——支持 pcap、pcapng,以及经典的 .cap/tcpdump 输出,全程
不依赖具体格式。PcapReader 和 PcapWriter 都基于 node:fs 的文件句柄做流式处理,所以再大的抓包也
不必整个装进内存;PcapReader 还能跟读一个仍在写入的文件。
English docs: README.md
PcapParser 只是一层很薄的 EventEmitter 外壳,把读取流喂给 @netkitty/pcap-core
——一个只处理内存字节、可以安全在浏览器里运行的解析内核,真正的字节活儿都在它那里做(靠魔数识别格式、
解析两种字节序和微秒/纳秒变体的经典 pcap,以及 pcapng)。
安装
npm i @netkitty/pcap
# 或者用聚合包:import ... from 'netkitty/pcap'快速上手
读取抓包
PcapReader 会逐包扫过文件,每个包报告一份 IPcapPacketInfo——序号、时间戳,以及这条记录在文件里的
字节范围。要拿整帧字节,调用 readPacketData(info);它用的是解析器报告的 packetOffset 和
packetLength,所以对 pcap 和 pcapng 都一样成立。
import {PcapReader, IPcapPacketInfo} from '@netkitty/pcap'
const reader = new PcapReader({
filename: '/path/to/capture.pcap',
onPacket: async (info: IPcapPacketInfo): Promise<void> => {
const frame: Buffer = await reader.readPacketData(info) // 整帧字节,pcap 或 pcapng 通用
console.log(`#${info.index} @${info.seconds}.${info.microseconds} — ${frame.length} 字节`)
}
})
reader.on('done', (): void => console.log('已读到文件末尾'))
await reader.start() // 一直读到末尾,然后触发 'done'onPacket 可以是异步的——在它执行期间读取流会被暂停,结束后再恢复,所以背压是自动的,await 的过程中不会
丢包。同样的信息也会以 packet 事件抛出,如果你更喜欢用监听器而不是回调,可以改用它。
写入抓包
PcapWriter 会新建一个经典 pcap 文件(先写好全局头),或者往已存在的文件后面追加,再用 write() 把
每一帧流式写出。传入帧的字节,以及拆成整秒和不足一秒的微秒余数两部分的抓包时间戳。传 format: 'pcapng'
就改成写 pcapng(先写段头 + 接口描述块,再每帧一个增强型包块)。
import {PcapWriter} from '@netkitty/pcap'
const writer = new PcapWriter({filename: '/path/to/out.pcap'})
const now: number = Date.now()
writer.write(frameBytes, Math.floor(now / 1000), (now % 1000) * 1000)
await writer.close() // 刷新并关闭文件句柄每写入一个包都会触发 packet 事件,携带这一帧落盘后的 IPcapPacketInfo(偏移、长度、时间戳)。
wroteCount 记录已经写了多少帧。
跟读还在增长的文件
设置 watch: true,读取器读到当前末尾后不会停,而是继续跟着文件走,文件一有新帧追加就接着读出来——把
一个 PcapWriter 和一个开了 watch 的 PcapReader 配对,就能一边录制一边消费同一份抓包。
const reader = new PcapReader({filename: '/path/to/growing.pcap', watch: true, onPacket})
await reader.start() // watch 模式下不会自己走到 'done',要靠你调 stop()/close() 才停
await reader.stop() // 或者用 reader.close(),它会顺带移除所有监听器编辑抓包
PcapEdit.rewrite 把一份抓包里的每个包流式地过一遍处理函数,再把结果写到一个新文件——读取端会透明处理
pcap/pcapng 和 gzip/LZ4,输出格式由你选。处理函数的返回值可以是:什么都不返回(保留)、null/false
(丢弃)、一个 Buffer(替换字节)、一个 {frame?, seconds?, microseconds?}(改字段),或一个数组
(展开成多个包)。
import {PcapEdit} from '@netkitty/pcap'
const {read, written} = await PcapEdit.rewrite({
input: 'in.pcapng.gz', // pcap/pcapng、gzip/lz4 都能读
output: 'out.pcap',
onPacket: (frame, info) => {
if (isNoise(frame)) return null // 丢弃
return {frame: anonymize(frame), seconds: info.seconds - 3600} // 替换字节 + 改时间
}
})常见编辑已经做成了可组合的 transform——用 PcapEdit.chain(...) 串起来:
await PcapEdit.rewrite({
input: 'in.pcap', output: 'out.pcap',
onPacket: PcapEdit.chain(
PcapEdit.setSourceMac('00:11:22:33:44:55'),
PcapEdit.setDestinationMac('aa:bb:cc:dd:ee:ff'),
PcapEdit.constantInterval(1000), // 包间隔固定 1 ms
)
})- 改时间(整文件):
shiftTime(seconds, microseconds)、setStartTime(seconds, microseconds)、scaleTime(factor)、constantInterval(interval, unit?)(unit∈'us' | 'ms' | 's' | 'min',默认微秒)。 - 以太网 MAC:
setSourceMac(mac)、setDestinationMac(mac)、swapMac()(假设是以太网链路层)。 truncate(maxBytes)截短帧。
需要按字段编辑(IP 地址、端口、校验和)时,在处理函数里用 @netkitty/codec 把帧解码、改
字段、再编码后返回字节即可——pcap 刻意不依赖 codec。
PcapEdit.patchInPlace(file, info, frame) 可以不重写整个文件就地覆盖一个包的字节——仅当替换字节和
原包等长且文件未压缩时有效。
只改某个帧范围内的时间
PcapEdit.retime 施加一个时间编辑,可以用 1 基、闭区间的帧范围 range: {from, to?} 限定。关键在于:
当你改动范围内部的帧间隔后,范围之后的每一帧都会按净时间变化量整体平移,从而保持时间线连续——
不会出现空洞,对本来有序的抓包也不会时间倒流。edit 是 {type:'scale', factor}、
{type:'constantInterval', interval, unit?}、{type:'shift', delta, unit?} 或
{type:'setStart', seconds, microseconds?} 之一(只有 scale/constantInterval 认范围;带范围的 shift
表示从 from 起平移后缀;setStart 仅整文件)。
// 把第 100..500 帧之间的间隔改成恰好 1 ms;第 501 帧起整体平移,保持随后的间隔不变
await PcapEdit.retime({
input: 'in.pcap', output: 'out.pcap',
edit: {type: 'constantInterval', interval: 1, unit: 'ms'},
range: {from: 100, to: 500}
})时间戳是微秒精度(纳秒抓包按微秒精度改)。PcapEdit.micros(value, unit) 把时长换算成微秒。改时间不可与
chain() 组合(它的传播是非局部的)——一次时间编辑用 retime,字节编辑用 rewrite + chain。
进度
rewrite 和 retime 都接受 onProgress 回调——按字节比例(总字节数一开始就知道,包数不知道),
按整百分比节流(progressPercentStep,默认 1),并保证最后一定回调一次 ratio: 1:
await PcapEdit.retime({
input: 'big.pcapng.gz', output: 'out.pcapng', format: 'pcapng',
edit: {type: 'scale', factor: 0.5},
onProgress: ({ratio, read, written}) => process.stdout.write(`\r${(ratio * 100).toFixed(0)}%`)
})对 gzip/LZ4 输入,比例是相对解压后大小的(开头的解压阶段会停在 0% 直到第一个包)。进度回调里抛异常 不会中断整个操作。
关键概念
不依赖格式,按内容识别。 读取器并不关心文件是 pcap 还是 pcapng;
@netkitty/pcap-core会从魔数 (而非文件扩展名)自动识别格式,支持:- 经典 libpcap —— 四种变体全覆盖:大端/小端 × 微秒/纳秒时间戳(
a1b2c3d4/d4c3b2a1/a1b23c4d/4d3cb2a1)。这就是 tcpdump、Wireshark、libpcap 写出来的格式,不管扩展名是.pcap、.cap、.dump还是没有后缀。 - pcapng —— 段头 / 接口描述 / (简单)增强包区块,时间戳精度按接口的
if_tsresol(0a0d0d0a)。
正因为是按内容识别,文件名起错或没后缀也照样能解;反过来,一个不是 libpcap 的
.cap(比如微软 Network Monitor 抓包、魔数不同)会干净地报unknown magic number错误,而不会瞎解。reader.format会给出识别到的PcapFileFormat('pcap' | 'pcapng')。- 经典 libpcap —— 四种变体全覆盖:大端/小端 × 微秒/纳秒时间戳(
透明解压。 用 gzip(
.pcap.gz/.pcapng.gz,魔数1f 8b)或 LZ4 frame 格式 (.pcap.lz4,魔数04 22 4d 18)压缩过的抓包会被自动解压——读取器识别压缩魔数,把整个文件解开, 之后流式解析和readPacketData()都从解压后的字节上取,所以压缩包读出来跟原始未压缩文件完全一致。 gzip 用 Node 内置的zlib;LZ4 用@netkitty/pcap-core里那个零依赖的纯 JSLz4FrameDecompress。(zstd 暂不支持,先自己解开,比如zstd -d capture.pcapng.zst。)按 info 取字节,不靠猜。
readPacketData(info)会定位到解析器报告的packetOffset,精确读出packetLength个字节,所以对每种格式都是对的。旧的readPacket(offset, length)已弃用——它 假设记录头是固定的 16 字节经典 pcap 格式,只对经典 pcap 文件有效。全程流式。 读取器和写入器都基于文件句柄按
chunkSize分块处理(默认1518 * 10字节),所以 不论文件多大,内存占用都是平的。
API
new PcapReader(options)
| 选项 | 类型 | 默认值 | 含义 |
| ----------- | --------------------------------------------- | ------------ | ------------------------------------------ |
| filename | string | — | 要读取的抓包文件路径 |
| watch | boolean | false | 读到当前末尾后继续跟读文件 |
| chunkSize | number | 1518 * 10 | 每次文件读取的字节数 |
| onPacket | (info: IPcapPacketInfo) => Promise \| void | — | 逐包回调;它 await 期间读取会被暂停 |
| onStart | () => Promise \| void | — | start() 开始时触发 |
| onStop | () => Promise \| void | — | 停止读取时触发 |
| onDone | () => Promise \| void | — | 读到文件末尾时触发 |
| onError | (err: Error) => void | — | 解析/读取出错 |
start(): Promise<void>——重置并开始读取(直读到末尾,或在watch下持续跟读)。stop(): Promise<void>——停止读取并拆除底层流。close(): Promise<void>——先停止,再移除所有监听器。readPacketData(info: IPcapPacketInfo): Promise<Buffer>——取某个已报告包的整帧字节。readPacket(offset, length): Promise<Buffer>——已弃用,仅适用于经典 pcap。- 事件:
packet(IPcapPacketInfo)、start、stop、done、close、error。
new PcapWriter(options)
| 选项 | 类型 | 默认值 | 含义 |
| ------------------- | --------------------- | -------- | -------------------------------------------------------- |
| filename | string | — | 输出路径;不存在则新建(带头),已存在则追加 |
| format | 'pcap' \| 'pcapng' | 'pcap' | 输出格式;'pcapng' 会先写 SHB + IDB,再每帧一个 EPB |
| includePacketData | boolean | true | 在触发的 packet 信息里附带 base64 形式的原始字节 |
当消费方只需要元数据时,把 includePacketData 设成 false——这样会跳过每个包的 base64 编码;字节仍然
会正常写入文件。
write(packet: Buffer, seconds: number, microseconds: number): void——追加一帧及其时间戳。close(): Promise<void>——刷新并关闭文件句柄。wroteCount: number——目前已写入的帧数。- 事件:
packet(IPcapPacketInfo)。
PcapParser
PcapReader 内部使用的流式外壳,你也可以直接驱动它。
PcapParser.parse(input: string | ReadStream): PcapParser——基于文件路径或读取流创建解析器。format: PcapFileFormat | null——识别出格式后给出的格式名。- 事件:
globalHeader、sectionHeader、packetHeader、packetData、packet(IPcapPacketInfo)、end、error。
从 @netkitty/pcap-core 再导出
IPcapPacketInfo、PcapFileFormat,经典 pcap 的字节生成器 GeneratePCAP、GeneratePCAPHeader、
GeneratePCAPData(配套类型 GeneratePCAPInputPacket / GeneratePCAPPacket),pcapng 的字节生成器
GeneratePcapng、GeneratePcapngSectionHeader、GeneratePcapngInterfaceDescription、
GeneratePcapngEnhancedPacket(配套类型 GeneratePcapngInputPacket / GeneratePcapngPacket /
GeneratePcapngOptions),以及用于透明读取 .lz4 的纯 JS Lz4FrameDecompress。
