Skip to content
Open
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
49 changes: 49 additions & 0 deletions .github/workflows/test-ebpf.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
name: eBPF Program Tests
on:
push:
branches: [main]
paths:
- 'pkg/plugin/**/_cprog/**'
- 'pkg/plugin/ebpftest/**'
- 'pkg/plugin/packetparser/packetparser_ebpf_test.go'
pull_request:
branches: [main]
paths:
- 'pkg/plugin/**/_cprog/**'
- 'pkg/plugin/ebpftest/**'
- 'pkg/plugin/packetparser/packetparser_ebpf_test.go'
workflow_dispatch:

permissions:
contents: read

jobs:
ebpf-tests:
strategy:
matrix:
include:
- arch: amd64
runner: ubuntu-latest
- arch: arm64
runner: ubuntu-24.04-arm
runs-on: ${{ matrix.runner }}
steps:
- name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2

- uses: actions/setup-go@7a3fe6cf4cb3a834922a1244abfce67bcef6a0c5 # v6.2.0
with:
go-version-file: go.mod

- name: Install clang and llvm-strip
run: |
sudo apt-get update
sudo apt-get install -y clang llvm

- name: Compile eBPF programs
run: |
GOARCH=${{ matrix.arch }} go generate ./pkg/plugin/packetparser/

- name: Run eBPF program tests
run: |
sudo $(which go) test -tags=ebpf -v -count=1 ./pkg/plugin/packetparser/ ./pkg/plugin/ebpftest/
4 changes: 4 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -459,6 +459,10 @@ test: # Run unit tests.
go build -o test-summary ./test/utsummary/main.go
KUBEBUILDER_ASSETS="$(shell $(ENVTEST) use -p path)" go test -tags=unit,dashboard -skip=TestE2E* -coverprofile=coverage.out -v -json ./... | ./test-summary --progress --verbose

.PHONY: test-ebpf
test-ebpf: # Run eBPF program tests (requires root/CAP_BPF).
sudo $$(which go) test -tags=ebpf -v -count=1 ./pkg/plugin/packetparser/ ./pkg/plugin/ebpftest/

coverage: # Code coverage.
# go generate ./... && go test -tags=unit -coverprofile=coverage.out.tmp ./...
cat coverage.out | grep -Ev '_bpf\.go|_bpfel_x86\.go|_bpfel_arm64\.go|_generated\.go|mock_' > coveragenew.out
Expand Down
139 changes: 139 additions & 0 deletions pkg/plugin/ebpftest/helpers.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.

//go:build ebpf && linux

package ebpftest

import (
"bytes"
"encoding/binary"
"errors"
"net"
"os"
"testing"
"time"
"unsafe"

"github.com/cilium/ebpf"
"github.com/cilium/ebpf/perf"
"github.com/stretchr/testify/require"
)

// RequirePrivileged skips the test if the current process lacks BPF privileges.
func RequirePrivileged(t *testing.T) {
t.Helper()

// Attempt to create a trivial BPF map to check for CAP_BPF.
m, err := ebpf.NewMap(&ebpf.MapSpec{
Type: ebpf.Array,
KeySize: 4,
ValueSize: 4,
MaxEntries: 1,
})
if err != nil {
t.Skipf("skipping eBPF test: insufficient privileges: %v", err)
}
m.Close()
}

// RemoveMapPinning sets Pinning to PinNone on all maps in the collection spec.
// This is required because embedded objects may have LIBBPF_PIN_BY_NAME which
// would fail in test environments without /sys/fs/bpf access.
func RemoveMapPinning(spec *ebpf.CollectionSpec) {
for _, m := range spec.Maps {
m.Pinning = ebpf.PinNone
}
}

// ReadPerfEvent reads one perf record from the reader within the given timeout,
// decodes it into T using binary.Read (little-endian), and returns it.
// Returns (zero, false) if the deadline expires without an event.
func ReadPerfEvent[T any](t *testing.T, reader *perf.Reader, timeout time.Duration) (T, bool) {
t.Helper()

reader.SetDeadline(time.Now().Add(timeout))

record, err := reader.Read()
var zero T
if errors.Is(err, os.ErrDeadlineExceeded) {
return zero, false
}
require.NoError(t, err)
require.Zero(t, record.LostSamples, "perf reader lost samples")

var event T
err = binary.Read(bytes.NewReader(record.RawSample), binary.LittleEndian, &event)
require.NoError(t, err, "failed to decode perf event")

return event, true
}

// AssertNoPerfEvent asserts that no perf event is emitted within the given timeout.
func AssertNoPerfEvent(t *testing.T, reader *perf.Reader, timeout time.Duration) {
t.Helper()

reader.SetDeadline(time.Now().Add(timeout))

_, err := reader.Read()
if errors.Is(err, os.ErrDeadlineExceeded) {
return // expected: no event
}
if err != nil {
return // reader error is acceptable for "no event" assertion
}
t.Fatal("expected no perf event but got one")
}

// IPToNative converts an IP string to its uint32 representation as stored by
// eBPF programs. Programs store ip->saddr directly from the packet (network
// byte order), which on a LE machine is the raw 4 bytes interpreted as a LE uint32.
func IPToNative(ipStr string) uint32 {
ip := net.ParseIP(ipStr).To4()
return *(*uint32)(unsafe.Pointer(&ip[0]))
}

// PortToNetwork converts a host port number to the byte order used by eBPF
// programs. Programs store tcp->source / tcp->dest directly from the packet
// header (big-endian), then perf events are decoded as LE by binary.Read.
func PortToNetwork(port uint16) uint16 {
var buf [2]byte
binary.BigEndian.PutUint16(buf[:], port)
return binary.LittleEndian.Uint16(buf[:])
}

// LPMTrieKey represents the key for the retina_filter LPM trie map.
// This struct matches the layout generated by bpf2go for all plugins
// that use the shared retina_filter map.
type LPMTrieKey struct {
Prefixlen uint32
Data uint32
}

// PopulateFilterMap inserts IPs into a retina_filter LPM trie map so that
// the eBPF program's lookup() function matches them.
func PopulateFilterMap(t *testing.T, filterMap *ebpf.Map, ips ...net.IP) {
t.Helper()
for _, ip := range ips {
ipBytes := ip.To4()
require.NotNil(t, ipBytes, "expected IPv4 address")

key := LPMTrieKey{
Prefixlen: 32,
Data: *(*uint32)(unsafe.Pointer(&ipBytes[0])),
}
val := uint8(1)
err := filterMap.Put(key, val)
require.NoError(t, err)
}
}

// RunProgram executes an eBPF program with the given packet data and returns the retval.
func RunProgram(t *testing.T, prog *ebpf.Program, pkt []byte) uint32 {
t.Helper()
ret, err := prog.Run(&ebpf.RunOptions{
Data: pkt,
})
require.NoError(t, err)
return ret
}
Loading
Loading