| name | ebpf-attach-hook |
| description | Implement eBPF program attachment logic for various hooks (XDP, TC/tcx, netkit, kprobe, tracepoint, cgroup) with proper error handling, cleanup, and link management. Includes Go code using cilium/ebpf library. Use when attaching CNF programs to kernel hooks. |
eBPF Attach Hook Skill
This skill generates Go code for attaching eBPF programs to various kernel hooks in CNF applications.
What This Skill Does
Generates code for:
- Attaching to XDP (eXpress Data Path)
- Attaching to TC/tcx (Traffic Control)
- Attaching to netkit devices
- Attaching to kprobes/kretprobes
- Attaching to tracepoints
- Attaching to cgroup hooks
- Proper cleanup and error handling
- Link management with defer patterns
When to Use
- Attaching CNF programs to network interfaces
- Hooking into kernel functions for tracing
- Setting up packet processing pipelines
- Implementing network policies
- Creating observability tools
- Building traffic control CNFs
Supported Hook Types
Network Hooks
- XDP: Earliest packet processing (driver level)
- TC/tcx: Traffic control (ingress/egress)
- netkit: BPF-programmable network device (primary/peer)
Tracing Hooks
- kprobe: Kernel function entry
- kretprobe: Kernel function return
- tracepoint: Static kernel tracepoints
Control Hooks
- cgroup: Socket operations, device access
- socket filter: Per-socket filtering
- SK_SKB: Socket buffer operations
Information to Gather
Ask the user:
- Hook Type: Which hook to use? (XDP, tcx, netkit, kprobe, etc.)
- Interface: Which interface (for network hooks)?
- Attach Point: Ingress/egress (for TC), primary/peer (for netkit)?
- Program Name: What is the eBPF program called?
- Cleanup: Need signal handling for graceful shutdown?
XDP Attachment
XDP Modes
const (
// Generic XDP (slowest, works everywhere)
XDPGenericMode = 1 << iota
// Driver XDP (requires driver support)
XDPDriverMode
// Offload XDP (requires NIC support)
XDPOffloadMode
)
Basic XDP Attachment
package main
import (
"log"
"net"
"os"
"os/signal"
"syscall"
"github.com/cilium/ebpf"
"github.com/cilium/ebpf/link"
)
func main() {
// Load eBPF program
spec, err := LoadMyProgram()
if err != nil {
log.Fatalf("loading spec: %v", err)
}
objs := &MyProgramObjects{}
if err := spec.LoadAndAssign(objs, nil); err != nil {
log.Fatalf("loading objects: %v", err)
}
defer objs.Close()
// Get interface
iface, err := net.InterfaceByName("eth0")
if err != nil {
log.Fatalf("finding interface: %v", err)
}
// Attach XDP program
l, err := link.AttachXDP(link.XDPOptions{
Program: objs.XdpProgram,
Interface: iface.Index,
Flags: link.XDPGenericMode, // or link.XDPDriverMode
})
if err != nil {
log.Fatalf("attaching XDP: %v", err)
}
defer l.Close()
log.Printf("XDP program attached to %s", iface.Name)
// Wait for signal
sig := make(chan os.Signal, 1)
signal.Notify(sig, syscall.SIGINT, syscall.SIGTERM)
<-sig
log.Println("Detaching XDP program...")
}
XDP with Auto-Mode Selection
func attachXDP(prog *ebpf.Program, ifaceName string) (link.Link, error) {
iface, err := net.InterfaceByName(ifaceName)
if err != nil {
return nil, fmt.Errorf("finding interface: %w", err)
}
// Try driver mode first, fall back to generic
l, err := link.AttachXDP(link.XDPOptions{
Program: prog,
Interface: iface.Index,
Flags: link.XDPDriverMode,
})
if err != nil {
log.Printf("Driver mode failed, trying generic mode: %v", err)
l, err = link.AttachXDP(link.XDPOptions{
Program: prog,
Interface: iface.Index,
Flags: link.XDPGenericMode,
})
if err != nil {
return nil, fmt.Errorf("attaching XDP: %w", err)
}
log.Println("Using generic XDP mode")
} else {
log.Println("Using driver XDP mode")
}
return l, nil
}
TC/tcx Attachment
tcx (Kernel 6.6+, Preferred)
package main
import (
"log"
"net"
"github.com/cilium/ebpf"
"github.com/cilium/ebpf/link"
)
func attachTCX(prog *ebpf.Program, ifaceName string, ingress bool) (link.Link, error) {
iface, err := net.InterfaceByName(ifaceName)
if err != nil {
return nil, fmt.Errorf("finding interface: %w", err)
}
attach := ebpf.AttachTCXIngress
if !ingress {
attach = ebpf.AttachTCXEgress
}
l, err := link.AttachTCX(link.TCXOptions{
Program: prog,
Attach: attach,
Interface: iface.Index,
})
if err != nil {
return nil, fmt.Errorf("attaching tcx: %w", err)
}
direction := "ingress"
if !ingress {
direction = "egress"
}
log.Printf("tcx program attached to %s %s", iface.Name, direction)
return l, nil
}
func main() {
// Load program
spec, _ := LoadMyProgram()
objs := &MyProgramObjects{}
spec.LoadAndAssign(objs, nil)
defer objs.Close()
// Attach to ingress
ingressLink, err := attachTCX(objs.TcxIngress, "eth0", true)
if err != nil {
log.Fatal(err)
}
defer ingressLink.Close()
// Attach to egress
egressLink, err := attachTCX(objs.TcxEgress, "eth0", false)
if err != nil {
log.Fatal(err)
}
defer egressLink.Close()
// ... wait for signal ...
}
Legacy TC (Kernel < 6.6)
func attachTC(prog *ebpf.Program, ifaceName string, ingress bool) (link.Link, error) {
iface, err := net.InterfaceByName(ifaceName)
if err != nil {
return nil, fmt.Errorf("finding interface: %w", err)
}
attach := ebpf.AttachTCIngress
if !ingress {
attach = ebpf.AttachTCEgress
}
l, err := link.AttachTC(link.TCOptions{
Program: prog,
Attach: attach,
Interface: iface.Index,
})
if err != nil {
return nil, fmt.Errorf("attaching TC: %w", err)
}
return l, nil
}
Auto-Detect tcx vs TC
func attachTrafficControl(prog *ebpf.Program, ifaceName string, ingress bool) (link.Link, error) {
iface, err := net.InterfaceByName(ifaceName)
if err != nil {
return nil, fmt.Errorf("finding interface: %w", err)
}
attach := ebpf.AttachTCXIngress
if !ingress {
attach = ebpf.AttachTCXEgress
}
// Try tcx first (kernel 6.6+)
l, err := link.AttachTCX(link.TCXOptions{
Program: prog,
Attach: attach,
Interface: iface.Index,
})
if err == nil {
log.Println("Using tcx (modern)")
return l, nil
}
// Fall back to legacy TC
log.Printf("tcx failed, using legacy TC: %v", err)
tcAttach := ebpf.AttachTCIngress
if !ingress {
tcAttach = ebpf.AttachTCEgress
}
l, err = link.AttachTC(link.TCOptions{
Program: prog,
Attach: tcAttach,
Interface: iface.Index,
})
if err != nil {
return nil, fmt.Errorf("attaching TC: %w", err)
}
log.Println("Using legacy TC")
return l, nil
}
netkit Attachment
package main
import (
"log"
"net"
"github.com/cilium/ebpf"
"github.com/cilium/ebpf/link"
)
func attachNetkit(primaryProg, peerProg *ebpf.Program, ifaceName string) (link.Link, link.Link, error) {
iface, err := net.InterfaceByName(ifaceName)
if err != nil {
return nil, nil, fmt.Errorf("finding interface: %w", err)
}
// Attach to primary
primaryLink, err := link.AttachNetkit(link.NetkitOptions{
Program: primaryProg,
Interface: iface.Index,
Attach: ebpf.AttachNetkitPrimary,
})
if err != nil {
return nil, nil, fmt.Errorf("attaching to primary: %w", err)
}
// Attach to peer
peerLink, err := link.AttachNetkit(link.NetkitOptions{
Program: peerProg,
Interface: iface.Index,
Attach: ebpf.AttachNetkitPeer,
})
if err != nil {
primaryLink.Close()
return nil, nil, fmt.Errorf("attaching to peer: %w", err)
}
log.Printf("netkit programs attached to %s (primary and peer)", iface.Name)
return primaryLink, peerLink, nil
}
func main() {
spec, _ := LoadMyProgram()
objs := &MyProgramObjects{}
spec.LoadAndAssign(objs, nil)
defer objs.Close()
primaryLink, peerLink, err := attachNetkit(
objs.NetkitPrimary,
objs.NetkitPeer,
"netkit0",
)
if err != nil {
log.Fatal(err)
}
defer primaryLink.Close()
defer peerLink.Close()
// ... wait for signal ...
}
kprobe/kretprobe Attachment
package main
import (
"log"
"github.com/cilium/ebpf/link"
)
func attachKprobe(prog *ebpf.Program, symbol string) (link.Link, error) {
// Attach kprobe to kernel function
l, err := link.Kprobe(symbol, prog, nil)
if err != nil {
return nil, fmt.Errorf("attaching kprobe to %s: %w", symbol, err)
}
log.Printf("kprobe attached to %s", symbol)
return l, nil
}
func attachKretprobe(prog *ebpf.Program, symbol string) (link.Link, error) {
// Attach kretprobe to kernel function return
l, err := link.Kretprobe(symbol, prog, nil)
if err != nil {
return nil, fmt.Errorf("attaching kretprobe to %s: %w", symbol, err)
}
log.Printf("kretprobe attached to %s", symbol)
return l, nil
}
func main() {
spec, _ := LoadMyProgram()
objs := &MyProgramObjects{}
spec.LoadAndAssign(objs, nil)
defer objs.Close()
// Attach to tcp_v4_connect entry
kprobeLink, err := attachKprobe(objs.TraceTcpConnect, "tcp_v4_connect")
if err != nil {
log.Fatal(err)
}
defer kprobeLink.Close()
// Attach to tcp_v4_connect return
kretprobeLink, err := attachKretprobe(objs.TraceTcpConnectReturn, "tcp_v4_connect")
if err != nil {
log.Fatal(err)
}
defer kretprobeLink.Close()
log.Println("Tracing TCP connections...")
// ... wait for signal ...
}
Tracepoint Attachment
func attachTracepoint(prog *ebpf.Program, group, name string) (link.Link, error) {
// Attach to tracepoint
l, err := link.Tracepoint(group, name, prog, nil)
if err != nil {
return nil, fmt.Errorf("attaching tracepoint %s:%s: %w", group, name, err)
}
log.Printf("tracepoint attached to %s:%s", group, name)
return l, nil
}
func main() {
spec, _ := LoadMyProgram()
objs := &MyProgramObjects{}
spec.LoadAndAssign(objs, nil)
defer objs.Close()
// Attach to syscalls:sys_enter_execve tracepoint
l, err := attachTracepoint(objs.TraceExecve, "syscalls", "sys_enter_execve")
if err != nil {
log.Fatal(err)
}
defer l.Close()
log.Println("Tracing execve syscalls...")
// ... wait for signal ...
}
cgroup Attachment
func attachCgroup(prog *ebpf.Program, cgroupPath string, attachType ebpf.AttachType) (link.Link, error) {
// Open cgroup directory
cgroupFd, err := os.Open(cgroupPath)
if err != nil {
return nil, fmt.Errorf("opening cgroup: %w", err)
}
defer cgroupFd.Close()
// Attach to cgroup
l, err := link.AttachCgroup(link.CgroupOptions{
Path: cgroupPath,
Attach: attachType,
Program: prog,
})
if err != nil {
return nil, fmt.Errorf("attaching to cgroup: %w", err)
}
log.Printf("Program attached to cgroup %s", cgroupPath)
return l, nil
}
func main() {
spec, _ := LoadMyProgram()
objs := &MyProgramObjects{}
spec.LoadAndAssign(objs, nil)
defer objs.Close()
// Attach to socket connect
l, err := attachCgroup(
objs.CgroupConnect,
"/sys/fs/cgroup/unified/my-cgroup",
ebpf.AttachCGroupInet4Connect,
)
if err != nil {
log.Fatal(err)
}
defer l.Close()
// ... wait for signal ...
}
Multi-Interface Attachment
func attachToMultipleInterfaces(prog *ebpf.Program, interfaces []string) ([]link.Link, error) {
var links []link.Link
for _, ifname := range interfaces {
iface, err := net.InterfaceByName(ifname)
if err != nil {
// Cleanup already attached
for _, l := range links {
l.Close()
}
return nil, fmt.Errorf("finding interface %s: %w", ifname, err)
}
l, err := link.AttachXDP(link.XDPOptions{
Program: prog,
Interface: iface.Index,
Flags: link.XDPGenericMode,
})
if err != nil {
// Cleanup already attached
for _, l := range links {
l.Close()
}
return nil, fmt.Errorf("attaching to %s: %w", ifname, err)
}
links = append(links, l)
log.Printf("Attached to %s", ifname)
}
return links, nil
}
func main() {
spec, _ := LoadMyProgram()
objs := &MyProgramObjects{}
spec.LoadAndAssign(objs, nil)
defer objs.Close()
interfaces := []string{"eth0", "eth1", "wlan0"}
links, err := attachToMultipleInterfaces(objs.XdpProgram, interfaces)
if err != nil {
log.Fatal(err)
}
// Cleanup all links
defer func() {
for _, l := range links {
l.Close()
}
}()
// ... wait for signal ...
}
Complete CNF Example with Signal Handling
package main
import (
"context"
"log"
"net"
"os"
"os/signal"
"syscall"
"github.com/cilium/ebpf"
"github.com/cilium/ebpf/link"
)
//go:generate go tool bpf2go MyCNF mycnf.c
func run(ctx context.Context) error {
// Load eBPF objects
spec, err := LoadMyCNF()
if err != nil {
return fmt.Errorf("loading spec: %w", err)
}
objs := &MyCNFObjects{}
if err := spec.LoadAndAssign(objs, nil); err != nil {
return fmt.Errorf("loading objects: %w", err)
}
defer objs.Close()
// Get interface
iface, err := net.InterfaceByName("eth0")
if err != nil {
return fmt.Errorf("finding interface: %w", err)
}
// Attach XDP
xdpLink, err := link.AttachXDP(link.XDPOptions{
Program: objs.XdpCnf,
Interface: iface.Index,
Flags: link.XDPGenericMode,
})
if err != nil {
return fmt.Errorf("attaching XDP: %w", err)
}
defer xdpLink.Close()
log.Printf("CNF attached to %s", iface.Name)
// Wait for cancellation
<-ctx.Done()
log.Println("Shutting down CNF...")
return nil
}
func main() {
// Setup signal handling
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
sig := make(chan os.Signal, 1)
signal.Notify(sig, syscall.SIGINT, syscall.SIGTERM)
go func() {
<-sig
log.Println("Received shutdown signal")
cancel()
}()
// Run CNF
if err := run(ctx); err != nil {
log.Fatalf("error: %v", err)
}
}
Best Practices
- Always use defer for cleanup:
defer link.Close() - Handle errors properly: Check all attachment errors
- Use context for graceful shutdown: Especially for long-running CNFs
- Log attachment details: Interface names, modes, etc.
- Try driver mode first: Fall back to generic if needed
- Clean up on partial failure: When attaching to multiple interfaces
- Use signal handling: Allow Ctrl+C to gracefully detach
- Prefer tcx over legacy TC: On kernel 6.6+
- Use netkit for BPF-programmable paths: Better than veth for eBPF
- Test attachment before deployment: Verify hooks work as expected
Error Handling Patterns
// Pattern 1: Cleanup on error
func attachWithCleanup() error {
var links []link.Link
defer func() {
for _, l := range links {
l.Close()
}
}()
// ... attachment code ...
return nil
}
// Pattern 2: Explicit cleanup
func attachExplicit() (link.Link, error) {
l, err := link.AttachXDP(...)
if err != nil {
return nil, err
}
// If we fail after this, clean up
if someCondition {
l.Close()
return nil, errors.New("failed")
}
return l, nil
}
Debugging Attachment Issues
// Check if program is attached
func checkAttachment(ifname string) {
// Use bpftool in bash: bpftool net show dev eth0
// Or use netlink to query XDP/TC status
}
// Verify link is valid
func verifyLink(l link.Link) bool {
info, err := l.Info()
if err != nil {
log.Printf("Link info error: %v", err)
return false
}
log.Printf("Link ID: %d, Type: %s", info.ID, info.Type)
return true
}