Skip to content

Adding New Modules

Relevant source files

The following files were used as context for generating this wiki page:

This guide explains how to implement new capture modules in eCapture. It covers the complete process from defining the module structure to integrating it with the CLI and eBPF subsystems.

For information about implementing event parsers for captured data, see Event Processing and Parsers. For eBPF program development patterns, see eBPF Program Development.

Overview

eCapture's modular architecture allows developers to add new capture targets (libraries, applications, system calls) by implementing the IModule interface. Each module consists of:

  • A module implementation struct implementing IModule
  • A configuration struct implementing IConfig
  • A CLI command definition
  • eBPF programs for kernel-space instrumentation
  • Event structure definitions for captured data

The system provides base classes (Module, MTCProbe) that handle common functionality like eBPF map reading, event dispatching, and lifecycle management.

Module Architecture

Component Relationships

Sources: user/module/imodule.go:47-75, user/module/probe_openssl.go:83-106, cli/cmd/root.go:249-403

IModule Interface

The IModule interface defines the contract all modules must fulfill:

MethodPurpose
Init(context.Context, *zerolog.Logger, config.IConfig, io.Writer) errorInitialize module with context, logger, configuration, and event collector writer
Name() stringReturn module name identifier
Run() errorStart event monitoring and processing
Start() errorStart eBPF programs and attach probes
Stop() errorStop event monitoring
Close() errorCleanup and detach probes
SetChild(IModule)Set reference to child implementation (for base class)
Decode(*ebpf.Map, []byte) (event.IEventStruct, error)Decode raw eBPF event data
Events() []*ebpf.MapReturn list of eBPF maps to read events from
DecodeFun(*ebpf.Map) (event.IEventStruct, bool)Get decoder function for specific map
Dispatcher(event.IEventStruct)Handle decoded event

Sources: user/module/imodule.go:47-75

Implementation Steps

Step 1: Create Module Structure

Define a struct that embeds either Module (for basic modules) or MTCProbe (for TLS/network modules with TC support):

type MYourModuleProbe struct {
    Module                    // or MTCProbe for network modules
    bpfManager        *manager.Manager
    bpfManagerOptions manager.Options
    eventFuncMaps     map[*ebpf.Map]event.IEventStruct
    eventMaps         []*ebpf.Map
    
    // Module-specific fields
    targetPath        string
    someConfig        string
}

Key fields:

  • bpfManager: Manages eBPF program lifecycle
  • bpfManagerOptions: Configuration for eBPF manager (probes, maps, constants)
  • eventFuncMaps: Maps eBPF maps to their decoder functions
  • eventMaps: List of maps to read events from

Sources: user/module/probe_openssl.go:83-106

Step 2: Implement Init Method

The Init method initializes the module, detects target libraries/binaries, and prepares eBPF configuration:

func (m *MYourModuleProbe) Init(ctx context.Context, logger *zerolog.Logger, 
                                 conf config.IConfig, ecw io.Writer) error {
    // 1. Call parent Init
    err := m.Module.Init(ctx, logger, conf, ecw)
    if err != nil {
        return err
    }
    
    // 2. Set configuration and child reference
    m.conf = conf
    m.Module.SetChild(m)
    
    // 3. Initialize module-specific data structures
    m.eventMaps = make([]*ebpf.Map, 0, 2)
    m.eventFuncMaps = make(map[*ebpf.Map]event.IEventStruct)
    
    // 4. Detect target binary/library
    targetPath, err := m.detectTarget()
    if err != nil {
        return err
    }
    m.targetPath = targetPath
    
    // 5. Select appropriate eBPF bytecode based on target version
    err = m.selectBytecode(targetPath)
    if err != nil {
        return err
    }
    
    return nil
}

Sources: user/module/probe_openssl.go:109-176

Step 3: Implement Start Method

The Start method loads and attaches eBPF programs:

func (m *MYourModuleProbe) Start() error {
    // 1. Setup eBPF manager with probes and maps
    err := m.setupManagers()
    if err != nil {
        return err
    }
    
    // 2. Load bytecode from embedded assets
    bpfFileName := m.geteBPFName("user/bytecode/yourmodule_kern.o")
    byteBuf, err := assets.Asset(bpfFileName)
    if err != nil {
        return fmt.Errorf("couldn't find asset %w", err)
    }
    
    // 3. Initialize eBPF manager with bytecode
    if err = m.bpfManager.InitWithOptions(bytes.NewReader(byteBuf), 
                                          m.bpfManagerOptions); err != nil {
        return fmt.Errorf("couldn't init manager %w", err)
    }
    
    // 4. Start manager (attach probes)
    if err = m.bpfManager.Start(); err != nil {
        return fmt.Errorf("couldn't start manager %w", err)
    }
    
    // 5. Setup decode functions for event maps
    err = m.initDecodeFun()
    if err != nil {
        return err
    }
    
    return nil
}

setupManagers Implementation Pattern:

func (m *MYourModuleProbe) setupManagers() error {
    var probes []*manager.Probe
    
    // Define uprobes
    probes = append(probes, &manager.Probe{
        Section:     "uprobe/target_function",
        EbpfFuncName: "uprobe_target_function",
        AttachToFuncName: "target_function",
        BinaryPath:   m.targetPath,
    })
    
    // Define return probes
    probes = append(probes, &manager.Probe{
        Section:     "uretprobe/target_function",
        EbpfFuncName: "uretprobe_target_function",
        AttachToFuncName: "target_function",
        BinaryPath:   m.targetPath,
    })
    
    m.bpfManager = &manager.Manager{
        Probes: probes,
        Maps: []*manager.Map{
            {Name: "events"},
            {Name: "config_map"},
        },
    }
    
    m.bpfManagerOptions = manager.Options{
        DefaultKProbeMaxActive: 512,
        VerifierOptions: ebpf.CollectionOptions{
            Programs: ebpf.ProgramOptions{
                LogSize: 2097152,
            },
        },
        ConstantEditors: m.constantEditor(),
    }
    
    return nil
}

Sources: user/module/probe_openssl.go:280-350

Step 4: Implement Event Decoding

Setup Decode Functions

Map each eBPF event map to its decoder:

func (m *MYourModuleProbe) initDecodeFun() error {
    // Get reference to events map
    eventsMap, found, err := m.bpfManager.GetMap("events")
    if err != nil {
        return err
    }
    if !found {
        return errors.New("events map not found")
    }
    
    // Register decoder
    m.eventMaps = append(m.eventMaps, eventsMap)
    m.eventFuncMaps[eventsMap] = &event.YourEventStruct{}
    
    return nil
}

Implement DecodeFun and Events

func (m *MYourModuleProbe) DecodeFun(em *ebpf.Map) (event.IEventStruct, bool) {
    fun, found := m.eventFuncMaps[em]
    return fun, found
}

func (m *MYourModuleProbe) Events() []*ebpf.Map {
    return m.eventMaps
}

Sources: user/module/probe_openssl.go:336-348, user/module/probe_openssl.go:397-404

Step 5: Implement Dispatcher

Handle decoded events:

func (m *MYourModuleProbe) Dispatcher(eventStruct event.IEventStruct) {
    switch ev := eventStruct.(type) {
    case *event.YourDataEvent:
        m.handleDataEvent(ev)
    case *event.YourConfigEvent:
        m.handleConfigEvent(ev)
    default:
        m.logger.Warn().Str("eventType", fmt.Sprintf("%T", ev)).
            Msg("unknown event type")
    }
}

Sources: user/module/probe_openssl.go:741-762

Step 6: Create Configuration

Define a configuration struct implementing IConfig:

type YourModuleConfig struct {
    config.BaseConfig
    
    TargetPath   string `json:"target_path"`
    SomeOption   string `json:"some_option"`
    Model        string `json:"model"`  // text, pcap, keylog, etc.
}

func NewYourModuleConfig() *YourModuleConfig {
    return &YourModuleConfig{
        BaseConfig: config.BaseConfig{},
        Model:      config.TlsCaptureModelText,
    }
}

func (c *YourModuleConfig) Check() error {
    if c.TargetPath == "" {
        return errors.New("target path is required")
    }
    // Add validation logic
    return nil
}

Sources: user/config/iconfig.go:95-112

Step 7: Create CLI Command

Define a Cobra command in cli/cmd/:

// cli/cmd/yourmodule.go
package cmd

import (
    "github.com/spf13/cobra"
    "github.com/gojue/ecapture/user/config"
    "github.com/gojue/ecapture/user/module"
)

var ymc = config.NewYourModuleConfig()

var yourModuleCmd = &cobra.Command{
    Use:   "yourmodule",
    Short: "Brief description of what this module captures",
    Long: `Detailed description with examples:
ecapture yourmodule --target=/path/to/target
ecapture yourmodule -m pcap -w capture.pcapng
`,
    RunE: yourModuleCommandFunc,
}

func init() {
    yourModuleCmd.PersistentFlags().StringVar(&ymc.TargetPath, 
        "target", "", "Path to target binary")
    yourModuleCmd.PersistentFlags().StringVarP(&ymc.Model, 
        "model", "m", "text", "Capture model: text, pcap, keylog")
    rootCmd.AddCommand(yourModuleCmd)
}

func yourModuleCommandFunc(command *cobra.Command, args []string) error {
    return runModule(module.ModuleNameYourModule, ymc)
}

Sources: cli/cmd/tls.go:26-67, cli/cmd/gotls.go:26-58

Step 8: Register Module

Create factory function and register via init():

// user/module/probe_yourmodule.go

const (
    ModuleNameYourModule = "yourmodule"
)

func init() {
    RegisteFunc(NewYourModuleProbe)
}

func NewYourModuleProbe() IModule {
    mod := &MYourModuleProbe{}
    mod.name = ModuleNameYourModule
    mod.mType = ProbeTypeUprobe  // or ProbeTypeKprobe, ProbeTypeTC
    return mod
}

Sources: user/module/probe_openssl.go:785-794

Step 9: Implement Close Method

Cleanup resources:

func (m *MYourModuleProbe) Close() error {
    m.logger.Info().Msg("module close.")
    
    // Stop eBPF manager
    if err := m.bpfManager.Stop(manager.CleanAll); err != nil {
        return fmt.Errorf("couldn't stop manager %w", err)
    }
    
    // Close any open files or resources
    if m.someFile != nil {
        m.someFile.Close()
    }
    
    // Call parent Close
    return m.Module.Close()
}

Sources: user/module/probe_openssl.go:352-358

Module Lifecycle Flow

Sources: cli/cmd/root.go:336-398, user/module/imodule.go:236-262

Event Structure Definition

Events must implement the IEventStruct interface:

// user/event/yourmodule.go

type YourDataEvent struct {
    EventType uint8
    Pid       uint32
    Timestamp uint64
    DataLen   uint32
    Data      [4096]byte
}

func (e *YourDataEvent) Decode(payload []byte) error {
    buf := bytes.NewBuffer(payload)
    return binary.Read(buf, binary.LittleEndian, e)
}

func (e *YourDataEvent) String() string {
    return fmt.Sprintf("PID:%d, Data:%s", e.Pid, string(e.Data[:e.DataLen]))
}

func (e *YourDataEvent) StringHex() string {
    return fmt.Sprintf("PID:%d, Data:%x", e.Pid, e.Data[:e.DataLen])
}

func (e *YourDataEvent) Clone() IEventStruct {
    return &YourDataEvent{}
}

func (e *YourDataEvent) EventType() EventType {
    return TypeEventProcessor  // or TypeOutput, TypeModuleData
}

Sources: user/event/event_openssl.go (structure reference)

Complete Module Example

Module Initialization Sequence

Sources: user/module/probe_openssl.go:109-176

Manager Setup Pattern

Sources: user/module/probe_openssl.go:361-395

Common Patterns

Pattern 1: Version Detection and Bytecode Selection

Many modules need to detect library/binary versions and select appropriate bytecode:

func (m *MYourModuleProbe) detectTarget() (string, error) {
    // 1. Use config path if provided
    if m.conf.(*config.YourModuleConfig).TargetPath != "" {
        return m.conf.(*config.YourModuleConfig).TargetPath, nil
    }
    
    // 2. Search common paths
    paths := []string{
        "/usr/lib/libtarget.so",
        "/lib/x86_64-linux-gnu/libtarget.so",
    }
    
    for _, path := range paths {
        if _, err := os.Stat(path); err == nil {
            return path, nil
        }
    }
    
    return "", errors.New("target not found")
}

func (m *MYourModuleProbe) selectBytecode(targetPath string) error {
    // Parse version from binary
    version, err := parseVersion(targetPath)
    if err != nil {
        return err
    }
    
    // Map version to bytecode
    bytecodeMap := map[string]string{
        "1.0": "yourmodule_1_0_kern.o",
        "2.0": "yourmodule_2_0_kern.o",
    }
    
    bytecode, found := bytecodeMap[version]
    if !found {
        return fmt.Errorf("unsupported version: %s", version)
    }
    
    m.bytecodeFile = bytecode
    return nil
}

Sources: user/module/probe_openssl.go:178-278

Pattern 2: Constant Editors for eBPF Configuration

Pass configuration to eBPF programs via constant editors:

func (m *MYourModuleProbe) constantEditor() []manager.ConstantEditor {
    return []manager.ConstantEditor{
        {
            Name:  "target_pid",
            Value: uint64(m.conf.GetPid()),
        },
        {
            Name:  "target_uid",
            Value: uint64(m.conf.GetUid()),
        },
        {
            Name:  "enable_feature",
            Value: uint64(1),
        },
    }
}

These constants are referenced in eBPF code as:

c
const volatile u64 target_pid = 0;
const volatile u64 target_uid = 0;

Sources: user/module/probe_openssl.go:361-395

Pattern 3: Multiple eBPF Maps

Handle multiple event types from different maps:

func (m *MYourModuleProbe) initDecodeFun() error {
    // Data events
    dataMap, found, err := m.bpfManager.GetMap("data_events")
    if err != nil {
        return err
    }
    if found {
        m.eventMaps = append(m.eventMaps, dataMap)
        m.eventFuncMaps[dataMap] = &event.YourDataEvent{}
    }
    
    // Connection events
    connMap, found, err := m.bpfManager.GetMap("conn_events")
    if err != nil {
        return err
    }
    if found {
        m.eventMaps = append(m.eventMaps, connMap)
        m.eventFuncMaps[connMap] = &event.YourConnEvent{}
    }
    
    return nil
}

Sources: user/module/probe_openssl.go:336-348

Testing Considerations

Unit Testing

Test individual components:

func TestModuleInit(t *testing.T) {
    mod := NewYourModuleProbe()
    ctx := context.Background()
    logger := zerolog.New(os.Stdout)
    config := config.NewYourModuleConfig()
    
    err := mod.Init(ctx, &logger, config, os.Stdout)
    if err != nil {
        t.Errorf("Init failed: %v", err)
    }
}

Integration Testing

Test with actual eBPF programs:

  1. Build eBPF bytecode
  2. Initialize module with test config
  3. Start module
  4. Trigger target application events
  5. Verify captured events
  6. Clean up

Debugging

Enable debug logging:

bash
ecapture yourmodule --debug -p <pid>

Check eBPF verifier logs if programs fail to load.

Sources: user/module/imodule.go:111-171

Summary

To add a new module to eCapture:

  1. Define module struct embedding Module or MTCProbe
  2. Implement IModule interface methods: Init, Start, Close, Events, DecodeFun, Dispatcher
  3. Create configuration struct implementing IConfig
  4. Define CLI command using Cobra framework
  5. Write eBPF programs for instrumentation (see eBPF Program Development)
  6. Define event structures implementing IEventStruct
  7. Register module via init() function with factory method
  8. Test thoroughly with target applications

The module system provides extensive base functionality through the Module class, including eBPF map reading, event dispatching, protocol parsing integration, and lifecycle management. Most new modules only need to implement target-specific logic like version detection, bytecode selection, and event handling.

Sources: user/module/imodule.go:47-480, user/module/probe_openssl.go:83-794, cli/cmd/tls.go:26-67

Adding New Modules has loaded