Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 3 additions & 17 deletions internal/bpf/decode_test.go
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
//go:build linux

// Copyright 2026 Optiqor contributors
// SPDX-License-Identifier: Apache-2.0

Expand Down Expand Up @@ -94,7 +96,7 @@ func TestDecodeTCPEvent(t *testing.T) {
SAddr: binary.BigEndian.Uint32([]byte{10, 0, 0, 1}),
DAddr: binary.BigEndian.Uint32([]byte{8, 8, 8, 8}),
SPort: 54321,
DPort: 443, // already in host byte order (BPF normalizes before writing)
DPort: 443,
EventType: TCPEventRTT,
RTTUs: 250,
}
Expand Down Expand Up @@ -489,20 +491,4 @@ func TestIsSyscallError(t *testing.T) {
}
}

func TestTCPEventPortByteOrder(t *testing.T) {
// Verify that ports in TCPEvent are host byte order.
// The BPF program applies bpf_ntohs() to dport before writing,
// so both fields should be readable directly without swapping.
e := TCPEvent{
SPort: 54321, // ephemeral source port, host order
DPort: 443, // HTTPS destination port, host order (not 47873)
}
if e.DPort == 0xBB01 { // 47873 = 443 byte-swapped
t.Error("DPort is in network byte order; expected host byte order after BPF normalization")
}
if e.DPort != 443 {
t.Errorf("DPort = %d, want 443", e.DPort)
}
}

var _ = net.IPv4(0, 0, 0, 0)
132 changes: 132 additions & 0 deletions internal/integration/ebpf_load_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
// Copyright 2026 Optiqor contributors
// SPDX-License-Identifier: Apache-2.0

//go:build integration

package integration

import (
"context"
"io"
"log/slog"
"net"
"testing"
"time"

"github.com/optiqor/kerno/internal/bpf"
"github.com/optiqor/kerno/internal/collector"
"github.com/testcontainers/testcontainers-go"
"github.com/testcontainers/testcontainers-go/wait"
)

func TestEBPFLoadAndEvents(t *testing.T) {
testcontainers.SkipIfProviderIsNotHealthy(t)

ctx, cancel := context.WithTimeout(context.Background(), 120*time.Second)
defer cancel()

logger := slog.New(slog.NewTextHandler(io.Discard, nil))

syscallLoader := bpf.NewSyscallLatencyLoader(logger)
syscallCloser, err := syscallLoader.Load()
if err != nil {
t.Skipf("skip syscall eBPF integration test: load syscall_latency: %v", err)
}
t.Cleanup(func() { _ = syscallCloser.Close() })

syscallCollector := collector.NewSyscallCollector(logger, syscallLoader)
if err := syscallCollector.Start(ctx); err != nil {
t.Fatalf("start syscall collector: %v", err)
}
t.Cleanup(syscallCollector.Stop)

tcpLoader := bpf.NewTCPMonitorLoader(logger)
tcpCloser, err := tcpLoader.Load()
if err != nil {
t.Skipf("skip TCP eBPF integration test: load tcp_monitor: %v", err)
}
t.Cleanup(func() { _ = tcpCloser.Close() })

tcpCollector := collector.NewTCPCollector(logger, tcpLoader)
if err := tcpCollector.Start(ctx); err != nil {
t.Fatalf("start tcp collector: %v", err)
}
t.Cleanup(tcpCollector.Stop)

syscallGen, err := testcontainers.GenericContainer(ctx, testcontainers.GenericContainerRequest{
ContainerRequest: testcontainers.ContainerRequest{
Image: "docker.io/library/alpine:3.19",
Cmd: []string{
"sh",
"-c",
"for i in $(seq 1 500); do ls -la /etc >/dev/null 2>&1; cat /etc/hostname >/dev/null 2>&1; done; echo 'syscall-gen-done'",
},
WaitingFor: wait.ForLog("syscall-gen-done").WithStartupTimeout(30 * time.Second),
AlwaysPullImage: false,
},
Started: true,
})
if err != nil {
t.Fatalf("start syscall generator container: %v", err)
}
t.Cleanup(func() {
termCtx, termCancel := context.WithTimeout(context.Background(), 15*time.Second)
defer termCancel()
_ = syscallGen.Terminate(termCtx)
})

ln, err := net.Listen("tcp", "127.0.0.1:0")
if err != nil {
t.Fatalf("listen on localhost: %v", err)
}
defer ln.Close()

tcpAddr := ln.Addr().String()
tcpReady := make(chan struct{})

go func() {
conn, err := ln.Accept()
if err != nil {
return
}
conn.Close()
}()

go func() {
time.Sleep(3 * time.Second)
conn, err := net.DialTimeout("tcp", tcpAddr, 5*time.Second)
if err != nil {
t.Logf("tcp dial: %v", err)
close(tcpReady)
return
}
conn.Close()
close(tcpReady)
}()

select {
case <-tcpReady:
case <-time.After(10 * time.Second):
t.Log("timeout waiting for TCP connection test")
}

time.Sleep(2 * time.Second)

syscallSnap, ok := syscallCollector.Snapshot().(*collector.SyscallSnapshot)
if !ok {
t.Fatalf("expected *SyscallSnapshot, got %T", syscallCollector.Snapshot())
}
if syscallSnap.TotalCount == 0 {
t.Fatalf("expected syscall collector to capture events, got TotalCount=0")
}
t.Logf("Syscall collector captured %d events across %d entries", syscallSnap.TotalCount, len(syscallSnap.Entries))

tcpSnap, ok := tcpCollector.Snapshot().(*collector.TCPSnapshot)
if !ok {
t.Fatalf("expected *TCPSnapshot, got %T", tcpCollector.Snapshot())
}
if tcpSnap.ActiveConnections == 0 && syscallSnap.TotalCount == 0 {
t.Fatalf("expected syscall or TCP collector to capture events, both empty")
}
t.Logf("TCP collector active connections: %d", tcpSnap.ActiveConnections)
}
Loading