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 ...
}
Tip: When you create interfaces programmatically (e.g., netkit.CreatePair), use the returned index directly instead of looking up by name. See EXAMPLES.md for the pattern.
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
}