登录
推荐 文章 Go 技术 课程 下载 专题 AI
首页 >  Golang >  Go教程

Go gopacket 怎样把合成数据包写入离线PCAP文件

来源:17golang原创

时间:2026-10-31 04:14:40 150浏览 收藏

我第一次把测试用的合成包落成离线 PCAP 时,真正容易混淆的不是 Ethernet 或 UDP 字段,而是把“构造数据包”和“写入 PCAP 文件”当成了一件事。gopacket.SerializeLayers 只负责把各层变成字节,文件头、捕获时间和长度则要交给 pcapgo.Writer。

官方地址:https://pkg.go.dev/github.com/google/gopacket

最稳妥的做法是先按 Ethernet → IPv4 → UDP → Payload 组织可序列化层,使用 FixLengths: true 和 ComputeChecksums: true 生成包字节,再调用 WriteFileHeader 与 WritePacket 写入离线 PCAP。整个过程只落盘,不会把数据发送到网卡。

先把两段职责拆开

按我实际排查的顺序,链路可以分成两个边界。第一段是包模型:layers.Ethernet、layers.IPv4、layers.UDP 和 gopacket.Payload 描述“包里有什么”。第二段是文件容器:pcapgo.NewWriter 写文件头,WritePacket 把已经准备好的字节和 CaptureInfo 组成一条捕获记录。

Go gopacket 从 Ethernet IPv4 UDP Payload 到 SerializeBuffer 的静态结构关系图
图1:输入层、校验和配置与 SerializeBuffer 的静态关系说明图,不是截图,也不是运行证据。

这个拆分很有用:如果包字节不对,先看层顺序和序列化选项;如果文件无法被离线工具识别,再看文件头和捕获信息。不要一上来就把两个问题混在一起。

按层组织一个可重复的合成包

下面的例子只使用私有测试地址和文档保留端口,目的是生成一个可分析的 UDP 样本。UDP.SetNetworkLayerForChecksum 要在序列化前关联 IPv4 层,否则 UDP 校验和没有完整的网络层上下文。

package main

import (
    "fmt"
    "net"

    "github.com/google/gopacket"
    "github.com/google/gopacket/layers"
)

func buildPacket() ([]byte, error) {
    eth := &layers.Ethernet{
        SrcMAC:       net.HardwareAddr{0x02, 0x00, 0x00, 0x00, 0x00, 0x01}, // 使用本地测试 MAC,避免借用真实设备地址
        DstMAC:       net.HardwareAddr{0x02, 0x00, 0x00, 0x00, 0x00, 0x02}, // 目标仅用于离线样本,不代表实际发送对象
        EthernetType: layers.EthernetTypeIPv4,
    }
    ip4 := &layers.IPv4{
        Version:  4,
        TTL:      64, // 给离线分析保留常见的 IPv4 生存时间字段
        Protocol: layers.IPProtocolUDP,
        SrcIP:    net.IPv4(192, 0, 2, 10),
        DstIP:    net.IPv4(198, 51, 100, 20),
    }
    udp := &layers.UDP{
        SrcPort: 40000,
        DstPort: 9999,
    }
    udp.SetNetworkLayerForChecksum(ip4) // 让 UDP 能依据 IPv4 伪首部计算校验和

    payload := gopacket.Payload([]byte("offline-gopacket-sample")) // 负载只用于离线样本识别
    buf := gopacket.NewSerializeBuffer()
    opts := gopacket.SerializeOptions{
        FixLengths:       true,  // 自动回填 IPv4 总长度和 UDP 长度
        ComputeChecksums: true,  // 根据序列化后的字段重新计算校验和
    }
    if err := gopacket.SerializeLayers(buf, opts, eth, ip4, udp, payload); err != nil {
        return nil, fmt.Errorf("serialize packet layers: %w", err)
    }
    return append([]byte(nil), buf.Bytes()...), nil // 复制字节,避免后续复用 buffer 时内容被覆盖
}

这里没有手工填写 IPv4 的 IHL、总长度或 UDP 长度。它们属于由层内容推导出的字段,交给 FixLengths 更不容易因为改了负载却忘记同步长度。append 的复制也值得保留:SerializeLayers 会清空并复用写缓冲区,直接长期持有 buf.Bytes() 容易在下一次序列化后失效。

把字节写进离线 PCAP

包已经是字节后,才进入文件边界。新建 PCAP 时必须先写一次文件头,并且链路类型要和最外层一致;本例最外层是 Ethernet,所以使用 layers.LinkTypeEthernet。随后再用同一份字节填写捕获长度和原始长度。

pcapgo Writer 由文件头 CaptureInfo 和数据字节组成 PCAP 记录的静态结构图
图2:PCAP 文件头、CaptureInfo、packet bytes 与离线文件的静态边界说明图,不是抓包软件截图。
func writePCAP(path string, packetData []byte) error {
    f, err := os.Create(path)
    if err != nil {
        return fmt.Errorf("create pcap file: %w", err)
    }
    defer f.Close() // 无论写入成功或失败,都关闭文件句柄

    writer := pcapgo.NewWriter(f)
    if err := writer.WriteFileHeader(65535, layers.LinkTypeEthernet); err != nil {
        return fmt.Errorf("write pcap header: %w", err)
    }

    now := time.Now()
    info := gopacket.CaptureInfo{
        Timestamp:     now,
        CaptureLength: len(packetData), // 本次文件实际保存的字节数
        Length:        len(packetData), // 合成包没有被截断,原始长度相同
    }
    if err := writer.WritePacket(info, packetData); err != nil {
        return fmt.Errorf("write pcap packet: %w", err)
    }
    return nil
}

上面的函数需要补齐三个标准库导入:

import (
    "fmt"
    "os"
    "time"

    "github.com/google/gopacket"
    "github.com/google/gopacket/layers"
    "github.com/google/gopacket/pcapgo"
)

WriteFileHeader 只针对新文件调用一次;如果是追加到已经存在、且 snaplen 和链路类型一致的文件,处理方式不同,不能每次追加都重写文件头。对于单条或小批量合成包,先让每次写入都返回错误,再决定是否批量封装,排错更直接。

值班式排查:先看哪一个边界出问题

现象优先检查处理动作
UDP 长度或校验和不符合预期层顺序、SetNetworkLayerForChecksum、序列化选项确认 UDP 紧跟在 IPv4 后,并打开 FixLengths 和 ComputeChecksums
文件打不开或被识别成错误链路类型WriteFileHeader 的 snaplen 与 linktype新文件先写一次头,Ethernet 层对应 LinkTypeEthernet
离线工具显示包被截断CaptureLength 与 Length合成包未截断时两者都填 len(packetData);有意截断时再分别记录
多条包写入后内容像被覆盖SerializeBuffer 的生命周期写入前复制 buf.Bytes(),不要跨下一次序列化长期引用原切片

我会把排查顺序固定为“层 → 缓冲区 → 文件头 → 捕获信息”。这样每一层都有明确的处理动作和回滚点:如果序列化失败,不创建或替换最终文件;如果写包失败,保留错误并删除本次新建的临时文件,再决定是否重试。

什么时候换成 pcapng

如果目标只是给离线测试、协议解析或回归样本提供一条 Ethernet PCAP,pcapgo.Writer 足够简单。如果你需要多个接口、接口统计信息或更丰富的块结构,应重新评估 pcapgo.NewNgWriter 以及 pcapng 的写入接口。不要因为文件扩展名不同就只改后缀;容器的文件头和写入 API 也要一起切换。

官方 pcapgo 文档同时列出了 PCAP 的 Reader/Writer 和 pcapng 的 NgReader/NgWriter。本篇选择 PCAP,是为了让“合成层字节 → 单个离线容器”这条路径尽量短。

回滚和复盘清单

  1. 回滚:本次只写新文件,任何 WriteFileHeader 或 WritePacket 错误都停止后续写入,不覆盖上一份样本。
  2. 回滚:批量生成时为每个文件使用临时路径,全部写完再交给上层替换,避免半个文件被下游消费。
  3. 告警:记录序列化错误、文件头错误、写包错误和实际字节数,不把它们合并成一个模糊的“生成失败”。
  4. 复盘:确认层顺序、链路类型、snaplen、捕获时间与长度字段是否在需求变更后仍然一致。

常见问题

写入 PCAP 会不会把合成包发到真实网络?

不会。SerializeLayers 只生成内存字节,pcapgo.Writer 只写文件;只有显式调用网卡发送相关 API 才会进入发送路径。

为什么不能只调用 WritePacket?

新建空文件时,PCAP 还没有文件头,必须先调用一次 WriteFileHeader。追加已有文件时则要确保原文件的 snaplen 和链路类型匹配。

CaptureLength 和 Length 应该总是相等吗?

未截断的合成包通常相等;如果你的流程主动截短了写入字节,CaptureLength 应记录实际保存长度,Length 记录原始包长度。

何时选 pcapng 而不是 PCAP?

需要多个接口、接口统计或 pcapng 块能力时再选 pcapng;单一 Ethernet 离线样本优先保持 PCAP 写入路径简单。

声明:本文转载于:17golang原创 如有侵犯,请联系study_golang@163.com删除
相关阅读
更多>
最新阅读
更多>
课程推荐
更多>