Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
c4b1bb6
test(bpf): convert decode_test.go to table-driven tests
Anushreer22 Jun 3, 2026
4361142
feat(bpf): detect DNS resolution failures via UDP/53 eBPF (#22)
Anushreer22 Jun 8, 2026
8793161
feat(metrics): add kerno_dns_latency_nanoseconds and kerno_dns_failur…
Anushreer22 Jun 8, 2026
564cb61
fix(bpf): fix build tags, BOM, stub and paired rule for DNS monitor
Anushreer22 Jun 8, 2026
b9004c2
fix(doctor): wire DNS rules into Evaluate and fix gofmt/BOM issues
Anushreer22 Jun 8, 2026
ff9d35a
fix(doctor): replace unused t param with _ in DNS rules to satisfy un…
Anushreer22 Jun 8, 2026
86b0b19
fix(bpf): remove ebpf build tag from dns_monitor.go loader to match o…
Anushreer22 Jun 8, 2026
cce9362
fix(collector): remove ebpf build tag from dns.go collector
Anushreer22 Jun 8, 2026
5092480
fix(bpf): add dnsMonitorObjects stub to gen_stub.go for non-ebpf builds
Anushreer22 Jun 8, 2026
fdd842e
fix(bpf): fix malformed struct tags in dnsMonitorObjects stub
Anushreer22 Jun 8, 2026
b167337
fix(build): add linux and ebpf build tags to fix compilation errors
Anushreer22 Jun 9, 2026
50e89fc
fix(build): apply gofmt formatting
Anushreer22 Jun 9, 2026
7545184
fix(bpf): move Decode functions to decode.go so they compile without …
Anushreer22 Jun 9, 2026
89278ca
fix(bpf): add New*Loader stubs to gen_stub.go for non-ebpf builds
Anushreer22 Jun 9, 2026
fcee0f9
fix(build): change ebpf build tag to linux for collector and metrics …
Anushreer22 Jun 9, 2026
2978c65
fix(bpf): add loader type stubs to gen_stub.go for non-ebpf builds
Anushreer22 Jun 9, 2026
f019f6f
fix(bpf): fix loader stubs and build tags for integration tests
Anushreer22 Jun 9, 2026
1d0d505
fix(bpf): remove incorrect ebpf build tags from loader files
Anushreer22 Jun 9, 2026
6494081
fix(bpf): suppress unused lint warning on closerFunc
Anushreer22 Jun 9, 2026
83d0089
fix(bpf): add dns_event struct to kerno.h and fix closerFunc
Anushreer22 Jun 9, 2026
9f2746c
fix(bpf): rename _pad0 to _pad in dns_event struct to match C code
Anushreer22 Jun 9, 2026
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
2 changes: 2 additions & 0 deletions cmd/bpf-verify/main.go
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
//go:build ebpf

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

Expand Down
2 changes: 2 additions & 0 deletions cmd/kerno-mangen/main.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
2 changes: 2 additions & 0 deletions cmd/kerno/main.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
164 changes: 164 additions & 0 deletions internal/bpf/c/dns_monitor.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright 2026 Optiqor contributors.
//
// dns_monitor.c - Traces UDP/53 DNS sends and receives per pod/process.
//
// Hooks:
// tracepoint/syscalls/sys_enter_sendmsg -> DNS request events
// tracepoint/syscalls/sys_enter_recvmsg -> DNS response events
//
// Filter: destination (send) or source (recv) port == 53 only.
// Output: ring buffer of dns_event structs.

#include "headers/kerno.h"

#ifndef AF_INET
#define AF_INET 2
#endif

// Output ring buffer.
KERNO_RINGBUF(dns_events);

// In-flight request tracking: key = (pid << 16 | query_id), value = send timestamp_ns.
KERNO_HASH(dns_inflight, __u64, __u64, 4096);

// Force BTF emission so bpf2go can extract the struct.
const struct dns_event *_force_btf_dns_event __attribute__((used));

// Helper: read query_id (first 2 bytes of UDP payload = DNS transaction ID).
static __always_inline __u16 read_query_id(const struct msghdr *msg)
{
__u8 buf[2] = {0, 0};
struct iovec iov = {};

// Read the first iovec from userspace.
if (bpf_probe_read_user(&iov, sizeof(iov), BPF_CORE_READ(msg, msg_iter.iov)) != 0)
return 0;
if (iov.iov_len < 2)
return 0;

bpf_probe_read_user(buf, 2, iov.iov_base);
return ((__u16)buf[0] << 8) | buf[1];
}

// Helper: extract destination IPv4 address and port from sockaddr_in.
static __always_inline int read_dest(const struct msghdr *msg,
__u32 *daddr, __u16 *dport)
{
void *name_ptr = NULL;
__u32 name_len = 0;
struct sockaddr_in sin = {};

bpf_probe_read_kernel(&name_ptr, sizeof(name_ptr),
&msg->msg_name);
bpf_probe_read_kernel(&name_len, sizeof(name_len),
&msg->msg_namelen);

if (!name_ptr || name_len < sizeof(sin))
return -1;

bpf_probe_read_user(&sin, sizeof(sin), name_ptr);
if (sin.sin_family != AF_INET)
return -1;

*daddr = sin.sin_addr.s_addr;
*dport = __builtin_bswap16(sin.sin_port);
return 0;
}

// --- sys_enter_sendmsg -------------------------------------------------------

SEC("tracepoint/syscalls/sys_enter_sendmsg")
int tracepoint_sys_enter_sendmsg(struct trace_event_raw_sys_enter *ctx)
{
// ctx->args[1] is the msghdr pointer.
struct msghdr *msg = (struct msghdr *)(long)ctx->args[1];
if (!msg)
return 0;

__u32 daddr = 0;
__u16 dport = 0;
if (read_dest(msg, &daddr, &dport) != 0)
return 0;

// Filter: only DNS (port 53).
if (dport != 53)
return 0;

__u64 pid_tgid = bpf_get_current_pid_tgid();
__u16 qid = read_query_id(msg);

// Record send timestamp for latency calculation.
__u64 inflight_key = ((pid_tgid >> 32) << 16) | qid;
__u64 now = bpf_ktime_get_ns();
bpf_map_update_elem(&dns_inflight, &inflight_key, &now, BPF_ANY);

struct dns_event *e = bpf_ringbuf_reserve(&dns_events, sizeof(*e), 0);
if (!e)
return 0;

e->timestamp_ns = now;
e->cgroup_id = bpf_get_current_cgroup_id();
e->pid = pid_tgid >> 32;
e->saddr = 0; // source filled in userspace from socket
e->daddr = daddr;
e->sport = 0;
e->dport = dport;
e->query_id = qid;
e->event_type = DNS_EVENT_SEND;
e->_pad = 0;
bpf_get_current_comm(&e->comm, sizeof(e->comm));

bpf_ringbuf_submit(e, 0);
return 0;
}

// --- sys_enter_recvmsg -------------------------------------------------------

SEC("tracepoint/syscalls/sys_enter_recvmsg")
int tracepoint_sys_enter_recvmsg(struct trace_event_raw_sys_enter *ctx)
{
struct msghdr *msg = (struct msghdr *)(long)ctx->args[1];
if (!msg)
return 0;

__u32 saddr = 0;
__u16 sport = 0;
if (read_dest(msg, &saddr, &sport) != 0)
return 0;

// Only care about responses from port 53.
if (sport != 53)
return 0;

__u64 pid_tgid = bpf_get_current_pid_tgid();
__u16 qid = read_query_id(msg);

__u64 inflight_key = ((pid_tgid >> 32) << 16) | qid;
__u64 *send_ns = bpf_map_lookup_elem(&dns_inflight, &inflight_key);
__u64 now = bpf_ktime_get_ns();

if (send_ns)
bpf_map_delete_elem(&dns_inflight, &inflight_key);

struct dns_event *e = bpf_ringbuf_reserve(&dns_events, sizeof(*e), 0);
if (!e)
return 0;

e->timestamp_ns = send_ns ? *send_ns : now;
e->cgroup_id = bpf_get_current_cgroup_id();
e->pid = pid_tgid >> 32;
e->saddr = saddr;
e->daddr = 0;
e->sport = sport;
e->dport = 0;
e->query_id = qid;
e->event_type = DNS_EVENT_RECV;
e->_pad = 0;
bpf_get_current_comm(&e->comm, sizeof(e->comm));

bpf_ringbuf_submit(e, 0);
return 0;
}

char LICENSE[] SEC("license") = "Dual BSD/GPL";
25 changes: 25 additions & 0 deletions internal/bpf/c/headers/kerno.h
Original file line number Diff line number Diff line change
Expand Up @@ -163,4 +163,29 @@ struct file_event {
__type(value, val_type); \
} name SEC(".maps")

// --- DNS Monitor Event -------------------------------------------------------

#define EVENT_DNS_MONITOR 8

// DNS event subtypes.
#define DNS_EVENT_SEND 1
#define DNS_EVENT_RECV 2

struct dns_event {
__u64 timestamp_ns;
__u64 cgroup_id;
__u32 pid;
__u32 saddr;
__u32 daddr;
__u16 sport;
__u16 dport;
__u16 query_id;
__u8 event_type;
__u8 _pad;
char comm[TASK_COMM_LEN];
};

#endif // __KERNO_H__



14 changes: 14 additions & 0 deletions internal/bpf/closer.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
//go:build ebpf

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

package bpf

// closerFunc adapts a plain function to the io.Closer interface.
type closerFunc func()

func (f closerFunc) Close() error {
f()
return nil
}
58 changes: 58 additions & 0 deletions internal/bpf/decode.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
// Copyright 2026 Optiqor contributors
// SPDX-License-Identifier: Apache-2.0

package bpf

import (
"bytes"
"encoding/binary"
"fmt"
)

func DecodeSyscallEvent(data []byte) (*SyscallEvent, error) {
var event SyscallEvent
if err := binary.Read(bytes.NewReader(data), binary.LittleEndian, &event); err != nil {
return nil, fmt.Errorf("decoding syscall event: %w", err)
}
return &event, nil
}

func DecodeTCPEvent(data []byte) (*TCPEvent, error) {
var event TCPEvent
if err := binary.Read(bytes.NewReader(data), binary.LittleEndian, &event); err != nil {
return nil, fmt.Errorf("decoding tcp event: %w", err)
}
return &event, nil
}

func DecodeOOMEvent(data []byte) (*OOMEvent, error) {
var event OOMEvent
if err := binary.Read(bytes.NewReader(data), binary.LittleEndian, &event); err != nil {
return nil, fmt.Errorf("decoding oom event: %w", err)
}
return &event, nil
}

func DecodeDiskEvent(data []byte) (*DiskEvent, error) {
var event DiskEvent
if err := binary.Read(bytes.NewReader(data), binary.LittleEndian, &event); err != nil {
return nil, fmt.Errorf("decoding disk event: %w", err)
}
return &event, nil
}

func DecodeSchedEvent(data []byte) (*SchedEvent, error) {
var event SchedEvent
if err := binary.Read(bytes.NewReader(data), binary.LittleEndian, &event); err != nil {
return nil, fmt.Errorf("decoding sched event: %w", err)
}
return &event, nil
}

func DecodeFDEvent(data []byte) (*FDEvent, error) {
var event FDEvent
if err := binary.Read(bytes.NewReader(data), binary.LittleEndian, &event); err != nil {
return nil, fmt.Errorf("decoding fd event: %w", err)
}
return &event, nil
}
2 changes: 2 additions & 0 deletions internal/bpf/decode_fuzz_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
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)
9 changes: 2 additions & 7 deletions internal/bpf/disk_io.go
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
//go:build ebpf

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

Expand All @@ -21,7 +23,7 @@
// DiskIOLoader manages the disk_io eBPF program.
type DiskIOLoader struct {
logger *slog.Logger
objs *diskIOObjects

Check failure on line 26 in internal/bpf/disk_io.go

View workflow job for this annotation

GitHub Actions / Build

undefined: diskIOObjects
links []link.Link
reader *ringbuf.Reader
}
Expand All @@ -40,8 +42,8 @@

// Load implements Loader.
func (l *DiskIOLoader) Load() (io.Closer, error) {
l.objs = &diskIOObjects{}

Check failure on line 45 in internal/bpf/disk_io.go

View workflow job for this annotation

GitHub Actions / Build

undefined: diskIOObjects
if err := loadDiskIOObjects(l.objs, &ebpf.CollectionOptions{}); err != nil {

Check failure on line 46 in internal/bpf/disk_io.go

View workflow job for this annotation

GitHub Actions / Build

undefined: loadDiskIOObjects
return nil, fmt.Errorf("loading objects: %w", err)
}

Expand Down Expand Up @@ -125,10 +127,3 @@
}

// DecodeDiskEvent decodes a raw event into a typed DiskEvent.
func DecodeDiskEvent(data []byte) (*DiskEvent, error) {
var event DiskEvent
if err := binary.Read(bytes.NewReader(data), binary.LittleEndian, &event); err != nil {
return nil, fmt.Errorf("decoding disk event: %w", err)
}
return &event, nil
}
Loading
Loading