Skip to content

Commit 6cc55d5

Browse files
test: add test for bpf attach/detach
1 parent 50dd315 commit 6cc55d5

File tree

6 files changed

+566
-150
lines changed

6 files changed

+566
-150
lines changed

bpf-prog/block-iptables/cmd/block-iptables/main.go

Lines changed: 43 additions & 150 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,7 @@ import (
1010
"syscall"
1111
"time"
1212

13-
blockservice "github.com/Azure/azure-container-networking/bpf-prog/block-iptables/pkg/blockservice"
14-
"github.com/cilium/ebpf/link"
13+
"github.com/Azure/azure-container-networking/bpf-prog/block-iptables/pkg/bpfprogram"
1514
"github.com/cilium/ebpf/rlimit"
1615
"github.com/fsnotify/fsnotify"
1716
)
@@ -20,24 +19,18 @@ const (
2019
DefaultConfigFile = "/etc/cni/net.d/iptables-allow-list"
2120
)
2221

23-
// BPFProgram wraps the eBPF program and its links
24-
type BPFProgram struct {
25-
objs *blockservice.BlockIptablesObjects
26-
links []link.Link
27-
attached bool
22+
// BlockConfig holds configuration for the application
23+
type BlockConfig struct {
24+
ConfigFile string
25+
AttacherFactory bpfprogram.AttacherFactory
2826
}
2927

30-
// getHostNetnsInode gets the network namespace inode of the current process (host namespace)
31-
func getHostNetnsInode() (uint32, error) {
32-
var stat syscall.Stat_t
33-
err := syscall.Stat("/proc/self/ns/net", &stat)
34-
if err != nil {
35-
return 0, fmt.Errorf("failed to stat /proc/self/ns/net: %w", err)
28+
// NewDefaultBlockConfig creates a new BlockConfig with default values
29+
func NewDefaultBlockConfig() *BlockConfig {
30+
return &BlockConfig{
31+
ConfigFile: DefaultConfigFile,
32+
AttacherFactory: bpfprogram.NewProgram,
3633
}
37-
38-
inode := uint32(stat.Ino)
39-
log.Printf("Host network namespace inode: %d", inode)
40-
return inode, nil
4134
}
4235

4336
// isFileEmptyOrMissing checks if the config file exists and has content
@@ -62,113 +55,6 @@ func isFileEmptyOrMissing(filename string) int {
6255
return 0 // File exists and has content
6356
}
6457

65-
// attachBPFProgram attaches the BPF program to LSM hooks
66-
func (bp *BPFProgram) attachBPFProgram() error {
67-
if bp.attached {
68-
log.Println("BPF program already attached")
69-
return nil
70-
}
71-
72-
log.Println("Attaching BPF program...")
73-
74-
// Get the host network namespace inode
75-
hostNetnsInode, err := getHostNetnsInode()
76-
if err != nil {
77-
return fmt.Errorf("failed to get host network namespace inode: %w", err)
78-
}
79-
80-
// Load BPF objects with the host namespace inode set
81-
spec, err := blockservice.LoadBlockIptables()
82-
if err != nil {
83-
return fmt.Errorf("failed to load BPF spec: %w", err)
84-
}
85-
86-
// Set the host_netns_inode variable in the BPF program before loading
87-
// Note: The C program sets it to hostNetnsInode + 1, so we do the same
88-
if err := spec.RewriteConstants(map[string]interface{}{
89-
"host_netns_inode": hostNetnsInode,
90-
}); err != nil {
91-
return fmt.Errorf("failed to rewrite constants: %w", err)
92-
}
93-
94-
// Load the objects
95-
objs := &blockservice.BlockIptablesObjects{}
96-
if err := spec.LoadAndAssign(objs, nil); err != nil {
97-
return fmt.Errorf("failed to load BPF objects: %w", err)
98-
}
99-
bp.objs = objs
100-
101-
// Attach LSM programs
102-
var links []link.Link
103-
104-
// Attach socket_setsockopt LSM hook
105-
if bp.objs.IptablesLegacyBlock != nil {
106-
l, err := link.AttachLSM(link.LSMOptions{
107-
Program: bp.objs.IptablesLegacyBlock,
108-
})
109-
if err != nil {
110-
bp.objs.Close()
111-
return fmt.Errorf("failed to attach iptables_legacy_block LSM: %w", err)
112-
}
113-
links = append(links, l)
114-
}
115-
116-
// Attach netlink_send LSM hook
117-
if bp.objs.IptablesNftablesBlock != nil {
118-
l, err := link.AttachLSM(link.LSMOptions{
119-
Program: bp.objs.IptablesNftablesBlock,
120-
})
121-
if err != nil {
122-
// Clean up previous links
123-
for _, link := range links {
124-
link.Close()
125-
}
126-
bp.objs.Close()
127-
return fmt.Errorf("failed to attach block_nf_netlink LSM: %w", err)
128-
}
129-
links = append(links, l)
130-
}
131-
132-
bp.links = links
133-
bp.attached = true
134-
135-
log.Printf("BPF program attached successfully with host_netns_inode=%d", hostNetnsInode)
136-
return nil
137-
}
138-
139-
// detachBPFProgram detaches the BPF program
140-
func (bp *BPFProgram) detachBPFProgram() error {
141-
if !bp.attached {
142-
log.Println("BPF program already detached")
143-
return nil
144-
}
145-
146-
log.Println("Detaching BPF program...")
147-
148-
// Close all links
149-
for _, l := range bp.links {
150-
if err := l.Close(); err != nil {
151-
log.Printf("Warning: failed to close link: %v", err)
152-
}
153-
}
154-
bp.links = nil
155-
156-
// Close objects
157-
if bp.objs != nil {
158-
bp.objs.Close()
159-
bp.objs = nil
160-
}
161-
162-
bp.attached = false
163-
log.Println("BPF program detached successfully")
164-
return nil
165-
}
166-
167-
// Close cleans up all resources
168-
func (bp *BPFProgram) Close() {
169-
bp.detachBPFProgram()
170-
}
171-
17258
// setupFileWatcher sets up a file watcher for the config file
17359
func setupFileWatcher(configFile string) (*fsnotify.Watcher, error) {
17460
watcher, err := fsnotify.NewWatcher()
@@ -189,7 +75,7 @@ func setupFileWatcher(configFile string) (*fsnotify.Watcher, error) {
18975
}
19076

19177
// handleFileEvent processes file system events
192-
func handleFileEvent(event fsnotify.Event, configFile string, bp *BPFProgram) {
78+
func handleFileEvent(event fsnotify.Event, configFile string, bp bpfprogram.Attacher) {
19379
// Check if the event is for our config file
19480
if filepath.Base(event.Name) != filepath.Base(configFile) {
19581
return
@@ -205,48 +91,42 @@ func handleFileEvent(event fsnotify.Event, configFile string, bp *BPFProgram) {
20591
switch fileState {
20692
case 1: // File is empty
20793
log.Println("File is empty, attaching BPF program")
208-
if err := bp.attachBPFProgram(); err != nil {
94+
if err := bp.Attach(); err != nil {
20995
log.Printf("Failed to attach BPF program: %v", err)
21096
}
21197
case 0: // File has content
21298
log.Println("File has content, detaching BPF program")
213-
if err := bp.detachBPFProgram(); err != nil {
99+
if err := bp.Detach(); err != nil {
214100
log.Printf("Failed to detach BPF program: %v", err)
215101
}
216102
case -1: // File is missing
217103
log.Println("Config file was deleted, detaching BPF program")
218-
if err := bp.detachBPFProgram(); err != nil {
104+
if err := bp.Detach(); err != nil {
219105
log.Printf("Failed to detach BPF program: %v", err)
220106
}
221107
}
222108
}
223109

224-
func main() {
225-
configFile := DefaultConfigFile
226-
227-
// Parse command line arguments
228-
if len(os.Args) > 1 {
229-
configFile = os.Args[1]
230-
}
231-
232-
log.Printf("Using config file: %s", configFile)
110+
// run is the main application logic, separated for easier testing
111+
func run(config *BlockConfig) error {
112+
log.Printf("Using config file: %s", config.ConfigFile)
233113

234114
// Remove memory limit for eBPF
235115
if err := rlimit.RemoveMemlock(); err != nil {
236-
log.Fatalf("Failed to remove memlock rlimit: %v", err)
116+
return fmt.Errorf("failed to remove memlock rlimit: %w", err)
237117
}
238118

239-
// Initialize BPF program wrapper
240-
bp := &BPFProgram{}
119+
// Initialize BPF program attacher using the factory
120+
bp := config.AttacherFactory()
241121
defer bp.Close()
242122

243123
// Initial state check
244-
fileState := isFileEmptyOrMissing(configFile)
124+
fileState := isFileEmptyOrMissing(config.ConfigFile)
245125
switch fileState {
246126
case 1: // File is empty
247127
log.Println("File is empty, attaching BPF program")
248-
if err := bp.attachBPFProgram(); err != nil {
249-
log.Fatalf("Failed to attach BPF program: %v", err)
128+
if err := bp.Attach(); err != nil {
129+
return fmt.Errorf("failed to attach BPF program: %w", err)
250130
}
251131
case 0: // File has content
252132
log.Println("Config file has content, BPF program will remain detached")
@@ -255,9 +135,9 @@ func main() {
255135
}
256136

257137
// Setup file watcher
258-
watcher, err := setupFileWatcher(configFile)
138+
watcher, err := setupFileWatcher(config.ConfigFile)
259139
if err != nil {
260-
log.Fatalf("Failed to setup file watcher: %v", err)
140+
return fmt.Errorf("failed to setup file watcher: %w", err)
261141
}
262142
defer watcher.Close()
263143

@@ -276,25 +156,38 @@ func main() {
276156
case event, ok := <-watcher.Events:
277157
if !ok {
278158
log.Println("Watcher events channel closed")
279-
return
159+
return nil
280160
}
281-
handleFileEvent(event, configFile, bp)
161+
handleFileEvent(event, config.ConfigFile, bp)
282162

283163
case err, ok := <-watcher.Errors:
284164
if !ok {
285165
log.Println("Watcher errors channel closed")
286-
return
166+
return nil
287167
}
288168
log.Printf("Watcher error: %v", err)
289169

290170
case sig := <-sigChan:
291171
log.Printf("Received signal: %v", sig)
292172
cancel()
293-
return
173+
return nil
294174

295175
case <-ctx.Done():
296176
log.Println("Context cancelled, exiting")
297-
return
177+
return nil
298178
}
299179
}
300180
}
181+
182+
func main() {
183+
config := NewDefaultBlockConfig()
184+
185+
// Parse command line arguments
186+
if len(os.Args) > 1 {
187+
config.ConfigFile = os.Args[1]
188+
}
189+
190+
if err := run(config); err != nil {
191+
log.Fatalf("Application failed: %v", err)
192+
}
193+
}
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
package main
2+
3+
import (
4+
"os"
5+
"testing"
6+
7+
"github.com/Azure/azure-container-networking/bpf-prog/block-iptables/pkg/bpfprogram"
8+
"github.com/fsnotify/fsnotify"
9+
)
10+
11+
func TestHandleFileEventWithMock(t *testing.T) {
12+
// Create a mock Attacher
13+
mockAttacher := bpfprogram.NewMockProgram()
14+
15+
// Create a temporary config file for testing
16+
configFile := "/tmp/test-iptables-allow-list"
17+
18+
// Test cases
19+
testCases := []struct {
20+
name string
21+
setupFile func(string) error
22+
expectedAttach int
23+
expectedDetach int
24+
}{
25+
{
26+
name: "empty file triggers attach",
27+
setupFile: func(path string) error {
28+
// Create empty file
29+
file, err := os.Create(path)
30+
if err != nil {
31+
return err
32+
}
33+
return file.Close()
34+
},
35+
expectedAttach: 1,
36+
expectedDetach: 0,
37+
},
38+
{
39+
name: "file with content triggers detach",
40+
setupFile: func(path string) error {
41+
// Create file with content
42+
return os.WriteFile(path, []byte("some content"), 0o644)
43+
},
44+
expectedAttach: 0,
45+
expectedDetach: 1,
46+
},
47+
{
48+
name: "missing file triggers detach",
49+
setupFile: func(path string) error {
50+
// Remove file if it exists
51+
os.Remove(path)
52+
return nil
53+
},
54+
expectedAttach: 0,
55+
expectedDetach: 1,
56+
},
57+
}
58+
59+
for _, tc := range testCases {
60+
t.Run(tc.name, func(t *testing.T) {
61+
// Reset mock state
62+
mockAttacher.Reset()
63+
64+
// Setup file state
65+
if err := tc.setupFile(configFile); err != nil {
66+
t.Fatalf("Failed to setup file: %v", err)
67+
}
68+
defer os.Remove(configFile)
69+
70+
// Create a fake fsnotify event
71+
event := fsnotify.Event{
72+
Name: configFile,
73+
Op: fsnotify.Write,
74+
}
75+
76+
// Call the function under test
77+
handleFileEvent(event, configFile, mockAttacher)
78+
79+
// Verify expectations
80+
if mockAttacher.AttachCallCount() != tc.expectedAttach {
81+
t.Errorf("Expected %d attach calls, got %d", tc.expectedAttach, mockAttacher.AttachCallCount())
82+
}
83+
84+
if mockAttacher.DetachCallCount() != tc.expectedDetach {
85+
t.Errorf("Expected %d detach calls, got %d", tc.expectedDetach, mockAttacher.DetachCallCount())
86+
}
87+
})
88+
}
89+
}

0 commit comments

Comments
 (0)