freeBuf
主站

分类

云安全 AI安全 开发安全 终端安全 数据安全 Web安全 基础安全 企业安全 关基安全 移动安全 系统安全 其他安全

特色

热点 工具 漏洞 人物志 活动 安全招聘 攻防演练 政策法规

点我创作

试试在FreeBuf发布您的第一篇文章 让安全圈留下您的足迹
我知道了

官方公众号企业安全新浪微博

FreeBuf.COM网络安全行业门户,每日发布专业的安全资讯、技术剖析。

FreeBuf+小程序

FreeBuf+小程序

0

1

2

3

4

5

6

7

8

9

0

1

2

3

4

5

6

7

8

9

0

1

2

3

4

5

6

7

8

9

使用 gopacket 从网络捕获及重组数据包
星阑科技 2023-04-26 14:46:17 223512
所属地 北京

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

概述

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

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

  • layers:每次都可能使用该子包。它包含内置于 gopacket 的用于解码数据包协议的逻辑。注意,下面的所有示例代码假定已经导入 gopacket 和 gopacket/layers。

  • pcap:使用 libpcap 从网络读取数据包的 C 绑定。

  • pfring:使用 PF_RING 从网络读取数据包的 C 绑定。

  • afpacket:从网络上读取数据包的 Linux AF_PACKET 的 C 绑定。

  • tcpassembly:TCP 流重组。

此外,如果打算直接编写代码,那么请查看 examples (https://github.com/google/gopacket/tree/master/examples)子目录,其中包含许多使用 gopacket 库构建的简单二进制示例。

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

基本应用

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

// Decode a packet
packet := gopacket.NewPacket(myPacketData, layers.LayerTypeEthernet, gopacket.Default)
// Get the TCP layer from this packet
if tcpLayer := packet.Layer(layers.LayerTypeTCP); tcpLayer != nil {
  fmt.Println("This is a TCP packet!")
  // Get actual TCP data from this layer
  tcp, _ := tcpLayer.(*layers.TCP)
  fmt.Printf("From src port %d to dst port %d\n", tcp.SrcPort, tcp.DstPort)
}
// Iterate over all layers, printing out each layer type
for _, layer := range packet.Layers() {
  fmt.Println("PACKET LAYER:", layer.LayerType())
}

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

// Decode an ethernet packet
ethP := gopacket.NewPacket(p1, layers.LayerTypeEthernet, gopacket.Default)
// Decode an IPv6 header and everything it contains
ipP := gopacket.NewPacket(p2, layers.LayerTypeIPv6, gopacket.Default)
// Decode a TCP header and its payload
tcpP := gopacket.NewPacket(p3, layers.LayerTypeTCP, gopacket.Default)

从源读取数据包

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

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

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

packetSource := ...  // construct using pcap or pfring
for packet := range packetSource.Packets() {
  handlePacket(packet)  // do something with each packet
}

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

惰性解码

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

// Create a packet, but don't actually decode anything yet
packet := gopacket.NewPacket(myPacketData, layers.LayerTypeEthernet, gopacket.Lazy)
// Now, decode the packet up to the first IPv4 layer found but no further.
// If no IPv4 layer was found, the whole packet will be decoded looking for
// it.
ip4 := packet.Layer(layers.LayerTypeIPv4)
// Decode all layers and return them.  The layers up to the first IPv4 layer
// are already decoded, and will not require decoding a second time.
layers := packet.Layers()

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

无拷贝解码

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

// This channel returns new byte slices, each of which points to a new
// memory location that's guaranteed immutable for the duration of the
// packet.
for data := range myByteSliceChannel {
  p := gopacket.NewPacket(data, layers.LayerTypeEthernet, gopacket.NoCopy)
  doSomethingWithPacket(p)
}

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

已知层的指针

在解码过程中,某些层作为已知的层类型被存储在数据包中。比如 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 方法。这很有用,比如,获取所有数据包的负载,而不管其底层数据类型如何:

// Get packets from some source
for packet := range someSource {
  if app := packet.ApplicationLayer(); app != nil {
    if strings.Contains(string(app.Payload()), "magic string") {
      fmt.Println("Found magic string in a packet!")
    }
  }
}

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

packet := gopacket.NewPacket(myPacketData, layers.LayerTypeEthernet, gopacket.Default)
if err := packet.ErrorLayer(); err != nil {
  fmt.Println("Error decoding some part of the packet:", err)
}

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

Flow 和 Endpoint

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

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

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

packet := gopacket.NewPacket(myPacketData, layers.LayerTypeEthernet, gopacket.Lazy)
netFlow := packet.NetworkLayer().NetworkFlow()
src, dst := netFlow.Endpoints()
reverseFlow := gopacket.NewFlow(dst, src)

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

flows := map[gopacket.Endpoint]chan gopacket.Packet
packet := gopacket.NewPacket(myPacketData, layers.LayerTypeEthernet, gopacket.Lazy)
// Send all TCP packets to channels based on their destination port.
if tcp := packet.Layer(layers.LayerTypeTCP); tcp != nil {
  flows[tcp.TransportFlow().Dst()] <- packet
}
// Look for all packets with the same source and destination network address
if net := packet.NetworkLayer(); net != nil {
  src, dst := net.NetworkFlow().Endpoints()
  if src == dst {
    fmt.Println("Fishy packet has same network source and dst: %s", src)
  }
}
// Find all packets coming from UDP port 1000 to UDP port 500
interestingFlow := gopacket.FlowFromEndpoints(layers.NewUDPPortEndpoint(1000), layers.NewUDPPortEndpoint(500))
if t := packet.NetworkLayer(); t != nil && t.TransportFlow() == interestingFlow {
  fmt.Println("Found that UDP flow I was looking for!")
}

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

channels := [8]chan gopacket.Packet
for i := 0; i < 8; i++ {
  channels[i] = make(chan gopacket.Packet)
  go packetHandler(channels[i])
}
for packet := range getPackets() {
  if net := packet.NetworkLayer(); net != nil {
    channels[int(net.NetworkFlow().FastHash()) & 0x7] <- packet
  }
}

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

实现自己的解码器

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

// Create a layer type, should be unique and high, so it doesn't conflict,
// giving it a name and a decoder to use.
var MyLayerType = gopacket.RegisterLayerType(12345, gopacket.LayerTypeMetadata{Name: "MyLayerType", Decoder: gopacket.DecodeFunc(decodeMyLayer)})

// Implement my layer
type MyLayer struct {
  StrangeHeader []byte
  payload []byte
}
func (m MyLayer) LayerType() gopacket.LayerType { return MyLayerType }
func (m MyLayer) LayerContents() []byte { return m.StrangeHeader }
func (m MyLayer) LayerPayload() []byte { return m.payload }

// Now implement a decoder... this one strips off the first 4 bytes of the
// packet.
func decodeMyLayer(data []byte, p gopacket.PacketBuilder) error {
  // Create my layer
  p.AddLayer(&MyLayer{data[:4], data[4:]})
  // Determine how to handle the rest of the packet
  return p.NextDecoder(layers.LayerTypeEthernet)
}

// Finally, decode your packets:
p := gopacket.NewPacket(data, MyLayerType, gopacket.Lazy)

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


使用 DecodingLayerParser 快速解码

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

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

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

func main() {
  var eth layers.Ethernet
  var ip4 layers.IPv4
  var ip6 layers.IPv6
  var tcp layers.TCP
  parser := gopacket.NewDecodingLayerParser(layers.LayerTypeEthernet, &eth, &ip4, &ip6, &tcp)
  decoded := []gopacket.LayerType{}
  for packetData := range somehowGetPacketData() {
    if err := parser.DecodeLayers(packetData, &decoded); err != nil {
      fmt.Fprintf(os.Stderr, "Could not decode layers: %v\n", err)
      continue
    }
    for _, layerType := range decoded {
      switch layerType {
        case layers.LayerTypeIPv6:
          fmt.Println("    IP6 ", ip6.SrcIP, ip6.DstIP)
        case layers.LayerTypeIPv4:
          fmt.Println("    IP4 ", ip4.SrcIP, ip4.DstIP)
      }
    }
  }
}

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

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


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

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

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

dlp := gopacket.NewDecodingLayerParser(LayerTypeEthernet)
dlp.SetDecodingLayerContainer(gopacket.DecodingLayerSparse(nil))
var eth layers.Ethernet
dlp.AddDecodingLayer(&eth)
// ... add layers and use DecodingLayerParser as usual...

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

func main() {
  var eth layers.Ethernet
  var ip4 layers.IPv4
  var ip6 layers.IPv6
  var tcp layers.TCP
  dlc := gopacket.DecodingLayerContainer(gopacket.DecodingLayerArray(nil))
  dlc = dlc.Put(&eth)
  dlc = dlc.Put(&ip4)
  dlc = dlc.Put(&ip6)
  dlc = dlc.Put(&tcp)
  // you may specify some meaningful DecodeFeedback
  decoder := dlc.LayersDecoder(LayerTypeEthernet, gopacket.NilDecodeFeedback)
  decoded := make([]gopacket.LayerType, 0, 20)
  for packetData := range somehowGetPacketData() {
    lt, err := decoder(packetData, &decoded)
    if err != nil {
      fmt.Fprintf(os.Stderr, "Could not decode layers: %v\n", err)
      continue
    }
    if lt != gopacket.LayerTypeZero {
      fmt.Fprintf(os.Stderr, "unknown layer type: %v\n", lt)
      continue
    }
    for _, layerType := range decoded {
      // examine decoded layertypes just as already shown above
    }
  }
}

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

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

创建数据包数据

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

ip := &layers.IPv4{
  SrcIP: net.IP{1, 2, 3, 4},
  DstIP: net.IP{5, 6, 7, 8},
  // etc...
}
buf := gopacket.NewSerializeBuffer()
opts := gopacket.SerializeOptions{}  // See SerializeOptions for more details.
err := ip.SerializeTo(buf, opts)
if err != nil { panic(err) }
fmt.Println(buf.Bytes())  // prints out a byte slice containing the serialized IPv4 layer.

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

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

buf := gopacket.NewSerializeBuffer()
opts := gopacket.SerializeOptions{}
gopacket.SerializeLayers(buf, opts,
  &layers.Ethernet{},
  &layers.IPv4{},
  &layers.TCP{},
  gopacket.Payload([]byte{1, 2, 3, 4}))
packetData := buf.Bytes()

最后说明

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

import (
  _ "github.com/google/gopacket/layers"
)

TCP 流重组

环境说明

  • 操作系统:Ubuntu 20.10

  • Go:go version go1.20.3 linux/amd64

·设置代理

$ go env -w GOPROXY=https://goproxy.cn,direct

安装 libpcap 

下载 libpcap 源码

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

解压、切换目录

$ tar zxvf libpcap-1.10.4.tar.gz
$ cd libpcap-1.10.4/

安装依赖

$ sudo apt install -y gcc flex bison make

编译

$ ./configure
$ make
$ sudo make install

重建动态库缓存

$ sudo ldconfig
# 查看
$ sudo ldconfig -p | grep -i pcap

TCP 流重组示例

创建项目

$ mkdir gopacket-tcp-reassembly-test
$ cd gopacket-tcp-reassembly-test/
$ go mod init gopacket-tcp-reassembly-test
$ go get -u github.com/google/gopacket@v1.1.19

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

package main

import (
  "bufio"
  "bytes"
  "flag"
  "github.com/google/gopacket"
  "github.com/google/gopacket/layers"
  "github.com/google/gopacket/pcap"
  "github.com/google/gopacket/tcpassembly"
  "github.com/google/gopacket/tcpassembly/tcpreader"
  "io"
  "log"
  "net/http"
  "sync"
  "time"
)

// Cache 用于检索响应对应的请求。这样做的原因包括:
// 1. 解析 HTTP 响应时,需要相应的请求对象
// 2. 将响应和请求配对
type Cache struct {
  mtx sync.RWMutex
  // 格式为:{网络层哈希值: {传输层哈希值: 请求对象, ...}, ...}
  m map[uint64]map[uint64]*http.Request
  // 用于判断 Flow 是请求还是响应
  requests map[string]struct{}
}

func NewCache() *Cache {
  return &Cache{
    m:        make(map[uint64]map[uint64]*http.Request),
    requests: make(map[string]struct{}),
  }
}

// Get 用于在解析响应前,从 Cache 中获取请求
func (c *Cache) Get(netHash, transportHash uint64) *http.Request {
  c.mtx.RLock()
  defer c.mtx.RUnlock()
  if transportMap, found := c.m[netHash]; found {
    if r, found := transportMap[transportHash]; found {
      return r
    }
  }
  return nil
}

// Set 用于在解析完请求后,将其保存到 Cache 中
func (c *Cache) Set(net, transport gopacket.Flow, r *http.Request) {
  c.mtx.Lock()
  defer c.mtx.Unlock()
  transportMap, found := c.m[net.FastHash()]
  if found {
    transportMap[transport.FastHash()] = r
  } else {
    c.m[net.FastHash()] = map[uint64]*http.Request{transport.FastHash(): r}
  }
  c.requests[net.Src().String()+":"+transport.Src().String()] = struct{}{}
}

// Delete 用于从 Cache 中删除条目
func (c *Cache) Delete(net, transport gopacket.Flow) {
  c.mtx.Lock()
  defer c.mtx.Unlock()
  // 如果是请求,那么删除 requests 中的条目
  key := net.Src().String() + ":" + transport.Src().String()
  if _, found := c.requests[key]; found {
    delete(c.requests, key)
    return
  }
  // 如果是响应,那么删除缓存的请求对象
  transportMap, found := c.m[net.FastHash()]
  if !found {
    return
  }
  delete(transportMap, transport.FastHash())
  if len(transportMap) == 0 {
    delete(c.m, net.FastHash())
  }
}

var cache = NewCache()

// httpStream 真正地处理 HTTP 请求的解码
type httpStream struct {
  net, transport gopacket.Flow
  r              tcpreader.ReaderStream
}

func (h *httpStream) run() {
  buf := bufio.NewReader(&h.r)
  for {
    var err error
    // Magic Number 用于确定协议。比如 HTTP 请求报文、HTTP 响应报文
    var magicNumber []byte
    var resp *http.Response
    var req *http.Request
    isResponse := false
    // PEEK(返回下 N 个字节,但不推进 Reader)出前 2 个字节,以确定 Magic Number
    magicNumber, err = buf.Peek(2)
    if err != nil {
      // 如果遇到其它错误,那么先打印错误信息,再返回
      if err != io.EOF {
        log.Printf("Error peeking magic number, %s\n", err)
      } else {
        // 如果遇到 EOF,那么清理缓存
        cache.Delete(h.net, h.transport)
      }
      return
    }
    // 如果前 2 个字节是 HT,那么认为该 Flow 是 HTTP 响应
    if bytes.Equal(magicNumber, []byte{'H', 'T'}) {
      isResponse = true
    }
    if isResponse {
      resp, err = http.ReadResponse(buf, cache.Get(h.net.FastHash(), h.transport.FastHash()))
    } else {
      if req, err = http.ReadRequest(buf); err == nil {
        cache.Set(h.net, h.transport, req)
      }
    }
    if err == io.EOF {
      // 必须读到 EOF...非常重要!
      cache.Delete(h.net, h.transport)
      return
    } else if err != nil {
      log.Printf("Error reading stream, %s, %s, %s", h.net, h.transport, err)
      continue
    }
    if req != nil {
      body, _ := io.ReadAll(req.Body)
      _ = req.Body.Close()
      log.Printf("http request: %#v\n", *req)
      log.Println(string(body))
    } else {
      body, _ := io.ReadAll(resp.Body)
      // 读完 resp.Body 时,必须调用 resp.Body.Close
      _ = resp.Body.Close()
      log.Printf("http response: %#v\n", *resp)
      log.Println(string(body))
    }
  }
}

// 使用 tcpassembly.StreamFactory 和 tcpassembly.Stream 接口,构建 HTTP 请求解析器。
// httpStreamFactory 实现 tcpassembly.StreamFactory
type httpStreamFactory struct{}

func (h *httpStreamFactory) New(net, transport gopacket.Flow) tcpassembly.Stream {
  hs := &httpStream{
    net:       net,
    transport: transport,
    r:         tcpreader.NewReaderStream(),
  }
  // 重要...必须保证读取 reader 流的数据
  go hs.run()
  // ReaderStream 实现 tcpassembly.Stream, 因此可以返回指向它的指针
  return &hs.r
}

func main() {
  // 解析命令行参数
  var iface = flag.String("i", "eth0", "Interface to get packets from")
  var snaplen = flag.Int("s", 1600, "SnapLen for pcap packet capture")
  flag.Parse()

  var handle *pcap.Handle
  var err error
  // pcap.OpenLive 打开设备,返回 *pcap.Handle。
  // 它接受设备名称(eth0),每个数据包的最大大小(snaplen),是否将接口设置为混杂模式(即是否接收目的地址不为本机的包),以及超时作为参数。
  // 警告:该函数仅接受毫秒时间戳。对于纳秒解决方案,使用 pcap.InactiveHandle。
  // 如果将 timeout 设置为 30s,那么每 30s 刷新一次数据包;设置成负数,将立刻刷新数据包
  handle, err = pcap.OpenLive(*iface, int32(*snaplen), true, time.Second)
  if err != nil {
    log.Panic(err)
  }
  // 务必释放 Handle
  defer handle.Close()

  // 设置 TCP 流重组
  // 1. 创建 httpStreamFactory 结构体,实现 tcpassembly.StreamFactory 接口
  streamFactory := &httpStreamFactory{}
  // 2. 创建连接池
  streamPool := tcpassembly.NewStreamPool(streamFactory)
  // 3. 创建重组器
  assembler := tcpassembly.NewAssembler(streamPool)
  // 创建数据包源。
  // handle.LinkType() 表示从 2 层以太网链路上抓取数据包
  packetSource := gopacket.NewPacketSource(handle, handle.LinkType())
  // 从该 Channel 中,持续地读取数据包
  packets := packetSource.Packets()
  ticker := time.Tick(time.Minute)
  for {
    select {
    case packet := <-packets:
      if packet.NetworkLayer() == nil || packet.TransportLayer() == nil ||
        packet.TransportLayer().LayerType() != layers.LayerTypeTCP {
        log.Println("Unusable packet")
        continue
      }
      tcp := packet.TransportLayer().(*layers.TCP)
      // 4. 将数据包添加到重组器中
      assembler.AssembleWithTimestamp(packet.NetworkLayer().NetworkFlow(), tcp, packet.Metadata().Timestamp)
    case <-ticker:
      // 每隔 1 分钟,刷新之前 2 分钟内不活跃的连接
      assembler.FlushOlderThan(time.Now().Add(time.Minute * -2))
    }
  }
}

安装测试 Nginx

$ sudo apt install -y nginx
# 测试 Nginx 是否成功启动
$ curl http://127.0.0.1

启动数据包捕获程序

# 在项目目录下执行
$ sudo go run main.go -i lo

访问 Nginx

$ curl http://127.0.0.1

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

其它示例

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

package main

import (
  "fmt"
  "github.com/google/gopacket/pcap"
  "log"
)

func main() {
  // 尝试迭代当前机器上的所有接口
  devices, err := pcap.FindAllDevs()
  if err != nil {
    log.Panic(err)
  }
  // 打印设备信息
  fmt.Println("Devices found:")
  for _, device := range devices {
    fmt.Println("\nName: ", device.Name)
    fmt.Println("Description: ", device.Description)
    fmt.Println("Devices addresses:")
    for _, address := range device.Addresses {
      fmt.Println("- IP address: ", address.IP)
      fmt.Println("- Subnet mask: ", address.Netmask)
    }
  }
}

参考文档

1. https://pkg.go.dev/github.com/google/gopacket

# 系统安全 # 开源安全工具 # 企业安全
免责声明
1.一般免责声明:本文所提供的技术信息仅供参考,不构成任何专业建议。读者应根据自身情况谨慎使用且应遵守《中华人民共和国网络安全法》,作者及发布平台不对因使用本文信息而导致的任何直接或间接责任或损失负责。
2. 适用性声明:文中技术内容可能不适用于所有情况或系统,在实际应用前请充分测试和评估。若因使用不当造成的任何问题,相关方不承担责任。
3. 更新声明:技术发展迅速,文章内容可能存在滞后性。读者需自行判断信息的时效性,因依据过时内容产生的后果,作者及发布平台不承担责任。
本文为 星阑科技 独立观点,未经授权禁止转载。
如需授权、对文章有疑问或需删除稿件,请联系 FreeBuf 客服小蜜蜂(微信:freebee1024)
被以下专辑收录,发现更多精彩内容
+ 收入我的专辑
+ 加入我的收藏
星阑科技 LV.7
北京星阑科技有限公司
  • 257 文章数
  • 45 关注者
API用户行为分析监测
2023-10-24
API NEWS | 第三方API安全性最佳实践
2023-10-18
第三方API安全性最佳实践
2023-10-18
文章目录