Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[feature][doc] Add proper documentation & examples #68

Merged
merged 5 commits into from
Sep 7, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,6 @@ examples/pcap/pcap

# Any test binaries
*.test

# VS Code files
.vscode
199 changes: 127 additions & 72 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,95 +1,150 @@
# A lightweight and high-performance network packet capture library
# A high-performance network packet capture library

[![Github Release](https://img.shields.io/github/release/fako1024/slimcap.svg)](https://github.com/fako1024/slimcap/releases)
[![GoDoc](https://godoc.org/github.com/fako1024/slimcap?status.svg)](https://godoc.org/github.com/fako1024/slimcap/)
[![Go Report Card](https://goreportcard.com/badge/github.com/fako1024/slimcap)](https://goreportcard.com/report/github.com/fako1024/slimcap)
[![Build/Test Status](https://github.com/fako1024/slimcap/workflows/Go/badge.svg)](https://github.com/fako1024/slimcap/actions?query=workflow%3AGo)
[![CodeQL](https://github.com/fako1024/slimcap/actions/workflows/codeql.yml/badge.svg)](https://github.com/fako1024/slimcap/actions/workflows/codeql.yml)

This package provides a simple interface to network packet capture / sniffing. It is focused on high performance and does *not* perform any payload / network layer decoding.

**Note:** This is currently considered WIP. The interface and / or features are not stable and can change at any point in time !
This package provides a simple yet powerful interface to perform network packet capture / sniffing. It is focused on high performance / traffic throughput.

## Features
- Support for network packet capture via AF_PACKET (Linux) directly from network socket or via ring buffer
- Minimal memory and CPU usage footprint, support for zero-copy operations
- Support for raw payload / IP layer packet capture via AF_PACKET (Linux) directly from network socket or using a ring buffer
- Minimal CPU usage and memory (allocation) footprint, support for zero-copy operations
- Virtual / mock capture sources including traffic replay from PCAP files (or even "chaining" multiple sources)
- Inherent support for packet type / direction detection
- Written in native Go (no `CGO` dependency)

> [!WARNING]
> **This package does *not* perform any payload / network layer decoding**\
> `slimcap` is aimed at doing the heavy lifting of extracting up to the IP layer of network packets with the utmost performance possible (and hence limits itself to packets which actually _have_ an IP layer). All further parsing / processing must
> be done by the caller.

## Installation
```bash
go get -u github.com/fako1024/slimcap
```

## Examples
#### Perform simple capture of a few packets on a network interface (c.f. `examples/dump`)
## Usage
#### **Perform simple capture of a few packets on a network interface using the various options of the `capture.Source` interface, using a fixed capture length (i.e. snaplen) of 64 bytes:**
```go
package main
listener, err := afpacket.NewSource("enp1s0",
afpacket.CaptureLength(link.CaptureLengthFixed(64)),
)
if err != nil {
// Error handling
}

import (
"flag"
"log"
// Capture a packet from the wire (allocate & copy)
p, err := listener.NextPacket(nil)
if err != nil {
// Error handling
}
fmt.Printf("Received packet on enp1s0 (total len %d): %v (inbound: %v)\n",
p.TotalLen(), p.Payload(), p.IsInbound())

// Capture a packet from the wire (copy to existing / reusable buffer packet)
pBuf := listener.NewPacket()
p, err := listener.NextPacket(pBuf)
if err != nil {
// Error handling
}
fmt.Printf("Received packet on enp1s0 (total len %d): %v (inbound: %v)\n",
p.TotalLen(), p.Payload(), p.IsInbound())

// Capture a packet from the wire (function execution)
if err := listener.NextPacketFn(func(payload []byte, totalLen uint32, pktType, ipLayerOffset byte) (err error) {
fmt.Printf("Received packet on enp1s0 (total len %d): %v (inbound: %v)\n",
totalLen, payload, pktType != capture.PacketOutgoing)
return
}); err != nil {
// Error handling
}

"github.com/fako1024/slimcap/capture/afpacket"
// Close the listener / the capture
if err := listener.Close(); err != nil {
// Error handling
}
```
#### **Perform zero-copy capture of a few packets on a network interface using the various options of the `capture.SourceZeroCopy` interface, using an optimal capture length (i.e. snaplen) to ensure any transport layer can be accessed for IPv4 & IPv6 packets and setting custom ring buffer size / number of blocks:**
```go
listener, err := afring.NewSource("enp1s0",
afring.CaptureLength(link.CaptureLengthMinimalIPv6Transport),
afring.BufferSize((1<<20), 4),
afring.Promiscuous(false),
)
if err != nil {
// Error handling
}

// Capture a raw packet (full payload) from the wire (zero-copy, no heap allocation)
payload, pktType, totalLen, err := listener.NextPayloadZeroCopy()
if err != nil {
// Error handling
}
fmt.Printf("Received payload on enp1s0 (total len %d): %v (inbound: %v)\n",
totalLen, payload, pktType != capture.PacketOutgoing)

// Capture a packet (IP layer only) from the wire (zero-copy, no heap allocation)
ipLayer, pktType, totalLen, err := listener.NextIPPacketZeroCopy()
if err != nil {
// Error handling
}
fmt.Printf("Received IP layer on enp1s0 (total len %d): %v (inbound: %v)\n",
totalLen, ipLayer, pktType != capture.PacketOutgoing)

func main() {

var (
devName string
maxPkts int
)

flag.StringVar(&devName, "d", "", "device / interface to capture on")
flag.IntVar(&maxPkts, "n", 10, "maximum number of packets to capture")
flag.Parse()
if devName == "" {
log.Fatal("no interface specified (-d)")
}

listener, err := afpacket.NewRingBufSource(devName,
afpacket.CaptureLength(64),
afpacket.BufferSize((1<<20), 4),
afpacket.Promiscuous(false),
)
if err != nil {
log.Fatalf("failed to start listener on `%s`: %s", devName, err)
}
defer func() {
if err := listener.Close(); err != nil {
log.Fatalf("failed to close listener on `%s`: %s", devName, err)
}
if err := listener.Free(); err != nil {
log.Fatalf("failed to free listener resources on `%s`: %s", devName, err)
}
}()
log.Printf("Listening on interface `%s`: %+v", listener.Link().Name, listener.Link().Interface)

log.Printf("Reading %d packets from wire (copy operation)...", maxPkts)
for i := 0; i < maxPkts; i++ {
p, err := listener.NextPacket(nil)
if err != nil {
log.Fatalf("error during capture (copy operation) on `%s`: %s", devName, err)
}
log.Printf("Received packet with Payload on `%s` (total len %d): %v (inbound: %v)", devName, p.TotalLen(), p.Payload(), p.Type() == 0)
}

log.Printf("Reading %d packets from wire (read into existing buffer)...", maxPkts)
p := listener.NewPacket()
for i := 0; i < maxPkts; i++ {
if p, err = listener.NextPacket(p); err != nil {
log.Fatalf("error during capture (read into existing buffer) on `%s`: %s", devName, err)
}
log.Printf("Received packet with Payload on `%s` (total len %d): %v (inbound: %v)", devName, p.TotalLen(), p.Payload(), p.Type() == 0)
}

log.Printf("Reading %d packets from wire (zero-copy function call)...", maxPkts)
for i := 0; i < maxPkts; i++ {
if err := listener.NextPacketFn(func(payload []byte, totalLen uint32, pktType, ipLayerOffset byte) (err error) {
log.Printf("Received packet with Payload on `%s` (total len %d): %v (inbound: %v)", devName, totalLen, payload, pktType == 0)
return
}); err != nil {
log.Fatalf("error during capture (zero-copy function call) on `%s`: %s", devName, err)
}
}
// Close the listener / the capture
if err := listener.Close(); err != nil {
// Error handling
}
```

> [!WARNING]
> In zero-copy mode, any and all interaction with the payload / IP Layer must be concluded prior to the next invocation of `Next...ZeroCopy()` since the calls provide direct access to the memory areas allocated by AF_PACKET (which may be overwritten by the next call)!

For further examples, please refer to the implementations in [examples](./examples). A production-level project that uses `slimcap` and showcases all its capabilities (including end-to-end testing using mock sources) is [goProbe](https://github.com/els0r/goProbe).

## Performance
The following benchmarks (c.f. [afring_mock_test.go](./capture/afpacket/afring/afring_mock_test.go)) show the relative difference in
general performance and memory allocation footprint of a single packet retrieval (obtained on a commodity Laptop using mock capture sources). For obvious reasons, zero-copy mode performs best and hence should be chosen in high-throughput scenarios:
```
goarch: amd64
pkg: github.com/fako1024/slimcap/capture/afpacket/afring
cpu: Intel(R) Core(TM) i7-10510U CPU @ 1.80GHz
BenchmarkCaptureMethods
BenchmarkCaptureMethods/NextPacket 226401712 74.60 ns/op 64 B/op 1 allocs/op
BenchmarkCaptureMethods/NextPacketInPlace 353661006 32.17 ns/op 0 B/op 0 allocs/op
BenchmarkCaptureMethods/NextPayload 176332635 63.81 ns/op 48 B/op 1 allocs/op
BenchmarkCaptureMethods/NextPayloadInPlace 516911223 21.65 ns/op 0 B/op 0 allocs/op
BenchmarkCaptureMethods/NextPayloadZeroCopy 535092314 19.67 ns/op 0 B/op 0 allocs/op
BenchmarkCaptureMethods/NextIPPacket 179753388 64.18 ns/op 48 B/op 1 allocs/op
BenchmarkCaptureMethods/NextIPPacketInPlace 381187490 28.33 ns/op 0 B/op 0 allocs/op
BenchmarkCaptureMethods/NextIPPacketZeroCopy 567278034 19.67 ns/op 0 B/op 0 allocs/op
BenchmarkCaptureMethods/NextPacketFn 559334258 20.44 ns/op 0 B/op 0 allocs/op
```

## Capture Mocks / Testing
In order to support extensive testing up to end-to-end level without having to rely on _actual_ network interface capture, both plain AF_PACKET and ring buffer captures are provided with mock-level sources implementing / wrapping their actual implementations. Hence, it is possible to simply exchange any invocation of an `afpacket` or `afring` source with their respective mock counterpart, e.g. by changing:
```go
listener, err := afring.NewSource("enp1s0",
// Options
)
```
to
```go
listener, err := afring.NewMockSource("enp1s0",
// Options
)
```
By either generating synthetic packet data or piping previously captured packets from a PCAP file these mock sources can then be used just like actual capture sources, e.g. for testing purposes. Some good examples on how to perform test using mocks can be found in [afpacket_mock_test.go](./capture/afpacket/afpacket/afpacket_mock_test.go) and [afring_mock_test.go](./capture/afpacket/afring/afring_mock_test.go), respectively.
Since the mock implementations incur a minor, yet significant performance overhead (even if not used), `slimcap` supports a build tag that allows disabling mocks completely (which in turn will also remove the aforementioned overhead):
```bash
go build -tags slimcap_nomock
```
Using this build tag for high performance production environments is recommended.

## Bug Reports & Feature Requests
Please use the [issue tracker](https://github.com/fako1024/slimcap/issues) for bugs and feature requests (or any other matter).

## License
See the [LICENSE](./LICENSE) file for usage conditions.
7 changes: 7 additions & 0 deletions capture/afpacket/afpacket/afpacket.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,13 @@
//go:build linux
// +build linux

/*
Package afpacket implements a capture.Source that allows reading network packets from
Linux network interfaces via the AF_PACKET mechanism. This implementation relies on performing
repeated `recvfrom()` calls to the allocated socket to fetch packets one by one. Consequently,
the performance / trhoughput is limited and it should only be used for educational / experimental
purposed. For production-level packet capture, use the `afring` package instead.
*/
package afpacket

import (
Expand Down
8 changes: 8 additions & 0 deletions capture/afpacket/afring/afring.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,14 @@
//go:build linux
// +build linux

/*
Package afring implements a capture.Source and a capture.SourceZeroCopy that allows reading
network packets from Linux network interfaces via the AF_PACKET / TPacket ring buffer mechanism.
This implementation relies on performing optimized `PPOLL()` syscalls to the MMAP'ed socket to
fetch blocks of packets. The ring buffer is configurable (depending on the expected throughput).
This capture method is optimally suited for production-level packet capture since it achieves
blazing-fast capture rates (in particular in zero-copy mode).
*/
package afring

import (
Expand Down
4 changes: 2 additions & 2 deletions capture/afpacket/afring/tpacket_mock.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,9 @@ import (

// / -> Block Header
func (t tPacketHeader) getStatus() uint32 {
return atomic.LoadUint32((*uint32)(unsafe.Pointer(&t.data[8])))
return atomic.LoadUint32((*uint32)(unsafe.Pointer(&t.data[8]))) // #nosec G103
}

func (t tPacketHeader) setStatus(status uint32) {
atomic.StoreUint32((*uint32)(unsafe.Pointer(&t.data[8])), status)
atomic.StoreUint32((*uint32)(unsafe.Pointer(&t.data[8])), status) // #nosec G103
}
5 changes: 5 additions & 0 deletions capture/afpacket/socket/socket.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
//go:build linux
// +build linux

/*
Package socket implements AF_PACKET sockets / file descriptors (both for `afring` and plain `afpacket`
modes). In addition, allocated sockets provide access to packet capture statistics for the underlying
network interface during capture.
*/
package socket

import (
Expand Down
16 changes: 13 additions & 3 deletions capture/capture.go
Original file line number Diff line number Diff line change
@@ -1,3 +1,13 @@
/*
Package capture provides the high level / central interface definitions for all slimcap capture
sources and core structures. Two Interfaces are supported:

- Source : The most general definition of methods any capture source must provide
- SourceZeroCopy : An extended interface definition adding capabilities for zero-copy operations

In addition to the capture source interfaces it provides the most basic implementation of a network
packet, including basic interaction methods (e.g. packet length, payload, type, ...).
*/
package capture

import (
Expand All @@ -20,7 +30,7 @@ var (
// ErrCaptureStopped denotes that the capture was stopped
ErrCaptureStopped = errors.New("capture was stopped")

// ErrCaptureUnblock denotes that the capture received an unblocking signal
// ErrCaptureUnblocked denotes that the capture received an unblocking signal
ErrCaptureUnblocked = errors.New("capture was released / unblocked")
)

Expand Down Expand Up @@ -177,15 +187,15 @@ func NewIPPacket(buf Packet, payload []byte, pktType PacketType, totalLen int, i

buf[0] = pktType
buf[1] = ipLayerOffset
*(*uint32)(unsafe.Pointer(&buf[2])) = uint32(totalLen)
*(*uint32)(unsafe.Pointer(&buf[2])) = uint32(totalLen) // #nosec G103
copy(buf[PacketHdrOffset:], payload)

return buf
}

// TotalLen returnsthe total packet length, including all headers
func (p *Packet) TotalLen() uint32 {
return *(*uint32)(unsafe.Pointer(&(*p)[2]))
return *(*uint32)(unsafe.Pointer(&(*p)[2])) // #nosec G103
}

// Len returns the actual data length of the packet payload as consumed from the wire
Expand Down
15 changes: 15 additions & 0 deletions capture/pcap/pcap.go
Original file line number Diff line number Diff line change
@@ -1,3 +1,10 @@
/*
Package pcap allows reading / parsing pcap files (compressed and uncompressed) and implement the
capture.Source interface. Consequently, an instance can act as network capture source, allowing
to replay network traffic previously captured elsewhere. In addition, the pcap package acts as main
facilitaty for testing (since it allows to mimic a live network traffic capture without the need
for privileged access and / or actual network interfaces).
*/
package pcap

import (
Expand Down Expand Up @@ -118,6 +125,10 @@ func (s *Source) NextPayload(pBuf []byte) ([]byte, capture.PacketType, uint32, e
if err != nil {
return nil, capture.PacketUnknown, 0, err
}
if pBuf != nil {
copy(pBuf, s.buf)
return pBuf, capture.PacketUnknown, uint32(pktHeader.OriginalLen), nil
}

return s.buf, capture.PacketUnknown, uint32(pktHeader.OriginalLen), nil
}
Expand All @@ -131,6 +142,10 @@ func (s *Source) NextIPPacket(pBuf capture.IPLayer) (capture.IPLayer, capture.Pa
if err != nil {
return nil, capture.PacketUnknown, 0, err
}
if pBuf != nil {
copy(pBuf, s.buf[s.ipLayerOffset:])
return pBuf, capture.PacketUnknown, uint32(pktHeader.OriginalLen), nil
}

return s.buf[s.ipLayerOffset:], capture.PacketUnknown, uint32(pktHeader.OriginalLen), nil
}
Expand Down
8 changes: 8 additions & 0 deletions event/event.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,14 @@
//go:build linux
// +build linux

/*
Package event provides access to system-level event file descriptors that act as semaphores and
notification mechanism to facilitate AF_PACKET network traffic capture. This is achieved either via
conventional recvfrom() operations (per-packet retrieval in afpacket/afpacket) or via PPOLL and
the kernel ring buffer (per-block retrieval in afpacket/afring).
In the latter case, a highly optimized assembler implementation is used (for AMD64 / ARM64 architectures)
in order to maximize throughput (by releasing resources as early as possible for the next poll operation).
*/
package event

import (
Expand Down
4 changes: 4 additions & 0 deletions examples/dump/main.go
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
/*
Package dump provides a simple packet dump tool that will simply consume up to a certain number
of network packets from the provided interface and then exit.
*/
package main

import (
Expand Down
4 changes: 4 additions & 0 deletions examples/pcap/main.go
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
/*
Package pcap provides a simple packet dump tool that will simply consume up to a certain number
of previously captured network packets from the provided pcap file, optionally log them and then exit.
*/
package main

import (
Expand Down
Loading