gopacket 是谷歌开源的项目,它为 Golang 提供捕获及处理网络数据包的能力。其底层基于 libpcap(在 Linux 上)和 npcap(在 Windows 上)。


1. 概述

包 gopacket 为 Go 语言提供数据包解码功能。

gopacket 包含多个带有额外功能的子包,包括:

此外,如果打算直接编写代码,那么请查看 examples 子目录,其中包含许多使用 gopacket 库构建的简单二进制示例。

由于 x/sys/unix 依赖,pcapgo/EthernetHandle、afpacket 和 bsdbpf 至少需要 Go 1.7。除此之外,所需的最小 Go 版本是 1.5。

1.1. 基本使用

gopacket 以 []byte 的形式接收数据包数据,并且将其解码为具有非零“层“数的数据包。每层对应于字节中的一个协议。解码数据包后,可以从数据包中请求数据包的层。

可以从许多起始点解码数据包。许多基础类型都实现了 Decoder 接口,这使我们能够解码我们没有完整数据的数据包。

1.2. 从源读取数据包

大多数情况下,你不会只是拥有数据包数据的 []byte。相反,你将希望从某处(文件、接口等)读取数据包,然后处理它们。为此,需要构建 PacketSource。

首先,需要构造实现 PacketDataSource 接口的对象。在 gopacket/pcap 和 gopacket/pfring 子包中,有该接口的实现...请查看文档,了解关于其用法的更多信息。一旦拥有 PacketDataSource,可以将其与选择的解码器一起传进 NewPacketSource,创建 PacketSource。

一旦拥有 PacketSource,可以以多种方式从其中读取数据包。请查看 PacketSource 的文档,了解更多细节。最简单的方式是 Packets 函数,该函数返回一个 Channel,然后异步地将数据包写进该 Channel,如果 packetSource 达到文件结束(end-of-file),则关闭 Channel。

可以通过设置 packetSource.DecodeOptions 中的字段更改 packetSource 的解码选项...查看下面的部分,了解更多细节。

1.3. 惰性解码

gopacket 选择性地惰性解码数据包数据,这意味着它只在需要处理函数调用时解码数据包层。

惰性解码的数据包不是并发安全的(concurrency-safe)。由于并非所有层都已解码,所以每次调用 Layer() 或 Layers() 都可能修改数据包,以解码下一层。如果在多个协程中并发地使用数据包,那么不要使用 gopacket.Lazy 选项。此时,gopacket 将完全解码数据包,所有未来的函数调用将不修改该对象。

1.4. 无拷贝解码

默认情况下,gopacket 将拷贝传递给 NewPacket 的切片,在数据包内存储该拷贝。因此对切片下层的字节的修改不会影响数据包及其层。如果可以保证不更改底层切片字节,那么使用 NoCopy 告诉 gopacket.NewPacket,它将使用被传入的切片本身。

最快的解码方式是同时使用 Lazy 和 NoCopy,但是请注意,从上面的许多警告中可以看出,对于某些实现来说,其中之一或两者都可能是危险的。

1.5. 已知层的指针

在解码过程中,某些层作为已知的层类型被存储在数据包中。比如 IPv4 和 IPv6 都是 NetworkLayer 层,而 TCP 和 UDP 都是 TransportLayer 层。gopacket 支持 4 个层,对应于 TCP/IP 分层模式的 4 个层(大致相当于 OSI 模型的 2、3、4 和 7 层)。可以使用 packet.LinkLayer、packet.NetworkLayer、packet.TransportLayer 和 packet.ApplicationLayer 函数,访问这些层。这些函数返回相应的接口(gopacket.{Link,Network,Transport,Application}Layer),前三层提供用于获取该特定层的源地址/目标地址的方法,而最后一层提供用于获取负载数据的 Payload 方法。这很有用,比如,获取所有数据包的负载,而不管其底层数据类型如何:

ErrorLayer 是特别有用的层,它在数据包的解析部分存在错误时被设置。

请注意,我们没有从 NewPacket 返回错误,因为我们在遇到错误层之前,可能已经成功解码了许多层。即便 TCP 层格式不正确,仍然可以正确地获取 Ethernet 和 IPv4 层。

1.6. Flow 和 Endpoint

gopacket 有两个有用的对象,Flow 和 Endpoint,用于以协议无关的方式,传达数据包来自于 A,进入 B 的事实。常用的层类型 LinkLayer、NetworkLayer 和 TransportLayer 都提供用于提取其 Flow 信息的方法,而无需关心底层 Layer 的类型。

Flow 是简单的对象,由两个 Endpoint 组成,一个源和一个目的地。它详细地说明数据包的某一层的发送方和接收方。

Endpoint 是源或目的地的可哈希表示。比如,对于 LayerTypeIPv4,Endpoint 包含 v4 IP 数据包的 IP 地址字节。Flow 可以分解为 Endpoint,Endpoint 可以组合成 Flow:

Endpoint 和 Flow 都能用作映射键,相等运算符可以比较它们,因此根据端点准则,可以很容易地将所有数据包分组在一起:

出于负载均衡的目的,Flow 和 Endpoint 都拥有 FastHash() 函数,该函数提供其内容的快速、非加密散列。特别重要的是 Flow FastHash() 是对称的:A -> B 与 B -> A 具有相同的哈希值。示例用法如下:

这允许我们拆分数据包流,同时仍然确保每个流看到一个 Flow(及其双向的反向)的所有数据包。

1.7. 实现自己的解码器

如果你的网络有一些奇怪的封装,那么可以实现自己的解码器。在本例中,我么处理用 4 字节头封装的 Ethernet 数据包。

有关编码解码器如何工作的更多细节,请查看 Decoder 和 PacketBuilder 的文档,或者查看 RegisterLayerType 和 RegisterEndpointType,以了解如何向 gopacket 添加 Layer/Endpoint 类型。

1.8. 使用 DecodingLayerParser 快速解码

TLDR(Too Long,Didn't Read):DecodingLayerParser 解码数据包数据花费的时间大约是 NewPacket 的 10%,但仅适用于已知的数据包堆栈。

使用 gopacket.NewPacket 或 PacketSource.Packets 的基本解码有些慢,因为它需要分配新数据包和每个相应的层。它非常通用,可以处理所有已知的层类型,但有时只关心特定的一组层,所以这种通用性是浪费的。

DecodingLayerParser 通过直接将数据包层解码进预分配对象的方式,完全地避免内存分配,然后可以引用这些对象,获取数据包的信息。示例如下:

这里需要注意的重要事项是,解析器修改传入的层(eth、ip4、ip6、tcp),而非分配新层,因此极大地加快了解码过程。它甚至基于层类型进行分支...它将处理 (eth, ip4, tcp) 或 (eth, ip6, tcp) 堆栈。但它不处理任何其它类型...由于没有传入其它解码器, (eth, ip4, udp) 堆栈将在 ip4 后,停止解码,并且只通过 “decoded” 切片传回 [LayerTypeEthernet, LayerTypeIPv4](以及说明无法解码 UDP 数据包的错误)。

不幸的是,并非所有层都可以被 DecodingLayerParser 使用...只有实现 DecodingLayer 接口的层才是可用的。此外,可以创建不是 Layer 本身的 DecodingLayer…请查看 layers.IPv6ExtensionSkipper,以获取示例。

1.9. 使用 DecodingLayerContainer 实现更快的和自定义解码

默认情况下,DecodingLayerParser 使用原生 Map 存储和搜索要解码的层。虽然是通用的,但在某些情况下,该方案可能不是最优的。比如,如果只有几层,那么通过稀疏数组索引或线性数组扫描可能提供更快的操作。

为适应这些场景,引入了 DecodingLayerContainer 接口及其实现:DecodingLayerSparse、DecodingLayerArray 和 DecodingLayerMap。可以使用 SetDecodingLayerContainer 方法指定 DecodingLayerParser 的容器实现。示例:

如果想跳过间接层(尽管牺牲一些功能),那么可以使用 DecodingLayerContainer 作为解码工具。在这种情况下,必须自己处理未知的层类型和 Panic。示例:

DecodingLayerSparse 是最快的,但在使用的层可以解码的 LayerType 值不大时最有效,否则可能导致更大的内存占用。DecodingLayerArray 非常紧凑,主要用于解码层数不多的情况(最多约 10-15 层,但请自行进行基准测试)。DecodingLayerMap 是最通用的,默认情况下,DecodingLayerParser 使用 DecodingLayerMap。请参考层子包中的测试和基准测试,以进一步了解使用示例和性能度量。

如果希望使用自己内部的数据包解码逻辑,那么可以选择实现自己的 DecodingLayerContainer。

1.10. 创建数据包数据

除提供解码数据包数据的能力外,gopacket 还允许从头开始创建数据包。许多 gopacket 层实现了 SerializableLayer 接口;可以通过以下方式,将这些层序列化为 []byte:

SerializeTo 将给定的层前置到 SerializeBuffer,将当前缓冲区的 Bytes() 切片视为序列化层的有效负载。因此,可以通过以相反顺序序列化一组层(比如,负载,然后 TCP,然后 IP,然后 Ethernet)的方式,序列化整个数据包。SerializeBuffer 的 SerializeLayers 函数是完成该任务的辅助函数。

比如,为生成一个(空的,并且无用的,因为未设置字段)Ethernet(IPv4(TCP(Payload)))数据包,可以运行:

1.11. 最后说明

如果使用 gopacket,那么几乎肯定希望确保已导入 gopacket/layers,因为导入时,它将设置所有 LayerType 变量,填充许多令人关注的变量/映射(DecodersByLayerName 等)。因此,建议即便不直接使用任何层函数,仍然使用下面的代码导入它:


2. TCP 流重组

2.1. 环境说明

2.2. 安装 libpcap

2.2.1. 下载 libpcap 源码

本文使用 https://www.tcpdump.org/release/libpcap-1.10.4.tar.gz

2.2.2. 解压、切换目录
2.2.3. 安装依赖
2.2.4. 编译
2.2.5. 重建动态库缓存

2.3. TCP 流重组示例

2.3.1. 创建项目

在项目目录下,创建 main.go:

2.3.2. 安装测试 Nginx
2.3.4. 启动数据包捕获程序
2.3.5. 访问 Nginx

数据包捕获程序将输出重组的 HTTP 请求和响应。


3. 其它示例

3.1. 迭代当前机器上的所有网络接口


参考文档