ebpfstruct

package module
v0.2.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Apr 17, 2025 License: Apache-2.0 Imports: 11 Imported by: 0

README

ebpfstruct

Useful data structure for eBPF programming in Go. They conviniently wrap ebpf data structures in a thread-safe manner.

These data structures offering an abstract interface simplifies testing components.

Getting started

To get started using ebpfstruct please follow the 3 step presented below:

1. Get the package
go get github.com/alexandremahdhaoui/ebpfstruct
2. Import
package your_package

import "github.com/alexandremahdhaoui/ebpfstruct"

func yourFunc() {
    // [...]
    arr := ebpfstruct.NewArray[any](
	    a, b,
	    aLen, bLen,
	    activePointer,
    )
    // [...]
}
3. Learn the package

There is four way to learn how to use this package:

  • Learn by doing:

    • With the examples in the section below.
  • Learn by reading the documentation:

    • I keep documentation as close as possible from their source of truth.
    • Hence, you'll find relevant information close to the interface or struct you're studying.
    • If you need to learn even more, you can always dive deeper and check how the concrete implementation of an interface or their dependencies are implemented.
    • Please note that all interfaces are themselves briefly documented below.
  • Learn by asking for help:

    • Maybe something is missing from the documentation or a piece of information is unclear.
    • Feel free to open an issue and ask.
  • Learn by reading the code:

    • That's the most straightforward, start with an interface and check how it's implemented.
    • All files are similarly structured: they start with an interface declaration and its concrete implementation lays below.

Examples

This example is taken from https://github.com/alexandremahdhaoui/udplb

Introduction

You're writing a loadbalancer and needs to maintain the following data structures.

Let bpfBackendListT the ebpf-go representation of a BPF map of type array. This data structure contains a list of available backend for your load balancer program.

Let bpfLookupTableT the ebpf-go representation of another BPF map of type array. The data structure is a lookup table, that maps each entry to the index of an available backend in the map bpfBackendListT

The problem
  • If a packet is received while both data structures are not updated, then we will forward a packet to a potentially wrong backend.
  • If a large array is being udpated we do not want to delay packet processing or degrade performance e.g. because of spinlocks.
  • If an error occured while updating the second data structure (e.g. the lookup table), how can we revert the changes that affected the fist data structure (e.g. the backend list).
The solution

Excerpt of a comment in the code of the Array[T] concrete implementation:

// The idea is to internally use 2 BPF maps for each data structure and
// when updating 1 Array[T], we update the internal BPF map that is not 
// currently being used by the BPF program.
//
// The BPF program will check a variable that will let it know which map
// to read from.
//
// This is a sort of a blue/green deployment of the new data structure.
// This solution simplifies error handling as it ensures the whole data
// structure is atomically updated from the bpf program point of view.
//
// a & b are the internal maps, either one or the other is in the active
// state while the other one is in passive state.
// The userland program will update data structures in passive states and
// perform a switchover to "notify" the BPF program which map is in active
// state.
The example (finally)
func main() {
    objs := &bpfObjects{}
	if err = spec.LoadAndAssign(lb.objs, nil); err != nil {
        slog.Error(err.Error)
        os.Exit(1)
	}

    // [...]

	backendList, err := ebpfstruct.NewArray[*bpfBackendListT](
		objs.BackendListA,
		objs.BackendListB,
		objs.BackendListA_len,
		objs.BackendListB_len,
		objs.ActivePointer,
	)
    if err != nil {
        slog.Error(err.Error)
        os.Exit(1)
    }

	lookupTable, err := ebpfstruct.NewArray[uint32](
		objs.LookupTableA,
		objs.LookupTableB,
		objs.LookupTableA_len,
		objs.LookupTableB_len,
		objs.ActivePointer,
	)
    if err != nil {
        slog.Error(err.Error)
        os.Exit(1)
    }

    // [...]

    for retryCount := 0; retryCount < 3;{
        // You received an event that updates the status of a backend. 
        // One of the backend became unavailable and you need to update
        // both backendList and lookupTable data structures.
        newBackendList := <- eventCh
        newLookupTable := recomputeLup(newBackendList)

        // Updates the passive backend list.
        backendSwitchover, err := backendList.SetAndDeferSwitchover()
        if err != nil { // retry on failure
            // -> because we defer the switchover after all data structures
            //    are updated, we can simply retry on failure.
            eventCh <- newBackendList
            retryCount += 1
            slog.Error(err.Error)
            continue
        }

        // Updates the passive lookup table.
        lupSwitchover, err := lookupTable.SetAndDeferSwitchover()
        if err != nil { // retry on failure
            // -> because we defer the switchover after all data structures
            //    are updated, we can simply retry on failure.
            eventCh <- newBackendList
            retryCount += 1
            slog.Error(err.Error)
            continue
        }

        // Perform the switchover:
        // -> The switchover is done in sync for all involved data structures,
        //    as soon as the first deferable switchover function is called.
        // -> However, we MUST call all switchover function in order to update
        //    the internal state of all go data structures.
        backendSwitchover()
        lupSwitchover()
        retryCount = 0
    }

    slog.Error("program failed 3 times to update bpf data structures")
    os.Exit(1)
}

Interfaces

[//] # TODO: generate interfaces list from code.

Fakes

Fakes can be injected into components for testing purposes.

package your_test

import (
    "github.com/alexandremahdhaoui/ebpfstruct/pkg/fakebpfstruct"
    "github.com/stretchr/testify/assert"
) 

func TestSomething(t *testing.T) {
    // [...]

    expectedArrayInternalValue := [0, 1, 2, 3]
    arr := fakebpfstruct.NewArray[T]()
    arr.
        ORDERED().
        EXPECT("Set", errors.New("a random error to see if your component handles unexpected errors")).
        EXPECT(
            "SetAndDeferSwitchover", // expected method
            nil,                     // if the method returns an error, it will return this value.
        )

    yourComponent := NewComponentDependingOnArray(arr)

    err := yourComponent.RunWithRetry(expectedArrayInternalValue)
    assert.NoError(t, err)
    assert.Equal(t, arr.Array, expectedArrayInternalValue)

    // [...]
}

Disable expector with fakes

Please note that the expector can be disabled by calling the DISABLE_EXPECTOR() method.

package your_test

import (
    "github.com/alexandremahdhaoui/ebpfstruct/pkg/fakebpfstruct"
) 

func TestSomething(t *testing.T) {
    // [...]

    expectedArrayInternalValue := [0, 1, 2, 3]
    arr := fakebpfstruct.NewArray[T]()
    arr.DISABLE_EXPECTOR() // <-- Disables the expector.

    yourComponent := NewComponentDependingOnArray(arr)

    err := yourComponent.RunWithRetry(expectedArrayInternalValue)
    assert.NoError(t, err)
    assert.Equal(t, arr.Array, expectedArrayInternalValue)

    // [...]
}

Mocks

Mocks can also be injected into components for testing purposes.

package your_test

import "github.com/alexandremahdhaoui/ebpfstruct/pkg/mockebpfstruct"

func TestSomething(t *testing.T) {
    // ...
    arr := mockebpfstruct.NewArray[any](t *testing.T)
    arr.
        EXPECT().
        Set(/* expectations */).
        Times(1)
    yourComponent := NewComponentDependingOnArray(arr)
    // ...
}

Documentation

Overview

* Copyright 2025 Alexandre Mahdhaoui * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License.

* Copyright 2025 Alexandre Mahdhaoui * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License.

* Copyright 2025 Alexandre Mahdhaoui * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License.

* Copyright 2025 Alexandre Mahdhaoui * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License.

Index

Constants

This section is empty.

Variables

View Source
var (
	ErrEBPFObjectsMustNotBeNil = errors.New("ebpf objects must not be nil")
	ErrCreatingNewArray        = errors.New("creating new array")
)
View Source
var (
	ErrAnotherProcessAlreadySubscribed = errors.New("another process already subscribed")
	ErrSubscribingToFIFO               = errors.New("subscribing to fifo")
)
View Source
var ErrCreatingNewVariable = errors.New("creating new variable")

Functions

This section is empty.

Types

type Array

type Array[T any] interface {
	// Done returns a channel that's closed when work done on behalf of this
	// interface has been gracefully terminated.
	Done() <-chan struct{}

	// -- Set all values of the BPF map to the one of the input map.
	//	  - DELETE all entries in the *ebpf.Map that have index > newLen.
	//	  - UPDATE_BATCH all entries with index in the interval [0, newLen].
	// -- Set new length if changed:
	//	  - SET a.length.
	//	  - a.oldLen = newLen.
	Set(values []T) error

	// SetAndDeferSwitchover updates the passive internal map but does
	// not perform the switchover.
	//
	// It allows users to "pseudo-atomically" update multiple BPF data
	// structures from the bpf-program point of view, by sharing the same
	// "activePointer" bpf variable with multiple BPF data structures.
	//
	// SetAndDeferSwitchover returns a function that can be called once to
	// perform the switchover.
	// The returned function can only be called once.
	// Please note that the returned function will retry if errors are
	// encountered.
	//
	// The deferable switchover function must be called, even if the same
	// "activePointer" bpf variable is used for multiple data structures.
	// The deferable switchover function must be called because it updates
	// internal variables in userspace.
	SetAndDeferSwitchover(values []T) (func(), error)
}

Wraps bpf objects with a convenient interface for testing.

func NewArray

func NewArray[T any](
	a, b *ebpf.Map,
	aLen, bLen *ebpf.Variable,
	activePointer *ebpf.Variable,
	doneCh <-chan struct{},
) (Array[T], error)

doneCh is a channel used to notify the bpf data structures or bpf program has been closed and they can no longer be used.

type FIFO

type FIFO[T any] interface {
	// Done returns a channel that's closed when work done on behalf of this
	// interface has been gracefully terminated.
	//
	// While the channel from Subscribe() could be used to know if the FIFO
	// interface has been closed, using the channel from this interface is
	// more idiomatic.
	Done() <-chan struct{}

	// Subscribe returns a receiver channel of T or an error.
	Subscribe() (<-chan T, error)
}

FIFO[T] can be used to subscribe to structured notifications produced by a bpf program.

Generics constraints: - T must be a **struct**. - T must not be a pointer. - T must not be an interface.

Notes: - FIFO is thread-safe. - Subscribe() can be called only once.

func NewFIFO

func NewFIFO[T any](ringbufMap *ebpf.Map, doneCh <-chan struct{}) (FIFO[T], error)

Generics constraints: - T must be a **struct**. - T must not be a pointer. - T must not be an interface.

doneCh is a channel used to notify the bpf data structures or bpf program has been closed and they can no longer be used.

type Map

type Map[K comparable, V any] interface {
	// Done returns a channel that's closed when work done on behalf of this
	// interface has been gracefully terminated.
	Done() <-chan struct{}

	// Update in batch a set of entries in the ACTIVE map.
	// This method does not perform a switchover.
	// This method mutates the ACTIVE map.
	BatchUpdate(kv map[K]V) error

	// Delete in batch a set of keys from the ACTIVE map.
	// This method does not perform a switchover.
	// This method mutates the ACTIVE map.
	BatchDelete([]K) error

	// Set all values of the BPF map to the one of the input map.
	Set(newMap map[K]V) error

	// SetAndDeferSwitchover updates the passive internal map but does
	// not perform the switchover.
	//
	// It allows users to "pseudo-atomically" update multiple BPF data
	// structures from the bpf-program point of view, by sharing the same
	// "activePointer" bpf variable with multiple BPF data structures.
	//
	// SetAndDeferSwitchover returns a function that can be called once to
	// perform the switchover.
	// The returned function can only be called once.
	// Please note that the returned function will retry if errors are
	// encountered.
	//
	// The deferable switchover function must be called, even if the same
	// "activePointer" bpf variable is used for multiple data structures.
	// The deferable switchover function must be called because it updates
	// internal variables in userspace.
	SetAndDeferSwitchover(newMap map[K]V) (func(), error)
}

Wraps bpf objects with a convenient interface for testing.

func NewMap

func NewMap[K comparable, V any](
	a, b *ebpf.Map,
	aLen, bLen *ebpf.Variable,
	activePointer *ebpf.Variable,
	doneCh <-chan struct{},
) (Map[K, V], error)

doneCh is a channel used to notify the bpf data structures or bpf program has been closed and they can no longer be used.

type Variable

type Variable[T any] interface {
	// Done returns a channel that's closed when work done on behalf of this
	// interface has been gracefully terminated.
	Done() <-chan struct{}

	// Set the variable.
	Set(v T) error
}

This is only used in order to write tests. No fancy feature. Maybe in the future add support for:

  • Get()
  • caching & no-cache for Get().
  • Set with differable switchover in order to sync switchover with many bpf data structures.

func NewVariable

func NewVariable[T any](obj *ebpf.Variable, doneCh <-chan struct{}) (Variable[T], error)

doneCh is a channel used to notify the bpf data structures or bpf program has been closed and they can no longer be used.

Directories

Path Synopsis
internal
util
* Copyright 2025 Alexandre Mahdhaoui * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License.
* Copyright 2025 Alexandre Mahdhaoui * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License.
pkg
fakebpfstruct
* Copyright 2025 Alexandre Mahdhaoui * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License.
* Copyright 2025 Alexandre Mahdhaoui * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL