mirror of
https://github.com/eunomia-bpf/bpf-developer-tutorial.git
synced 2026-10-30 04:06:26 +08:00
feat: add four production-shaped eBPF tutorial tools
This commit is contained in:
2
src/55-dns-egress/.gitignore
vendored
Normal file
2
src/55-dns-egress/.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
.output/
|
||||
dns_egress
|
||||
94
src/55-dns-egress/Makefile
Normal file
94
src/55-dns-egress/Makefile
Normal file
@@ -0,0 +1,94 @@
|
||||
# SPDX-License-Identifier: (LGPL-2.1 OR BSD-2-Clause)
|
||||
OUTPUT := .output
|
||||
CLANG ?= clang
|
||||
LIBBPF_SRC := $(abspath ../third_party/libbpf/src)
|
||||
BPFTOOL_SRC := $(abspath ../third_party/bpftool/src)
|
||||
LIBBPF_OBJ := $(abspath $(OUTPUT)/libbpf.a)
|
||||
BPFTOOL_OUTPUT ?= $(abspath $(OUTPUT)/bpftool)
|
||||
BPFTOOL ?= $(BPFTOOL_OUTPUT)/bootstrap/bpftool
|
||||
ARCH ?= $(shell uname -m | sed 's/x86_64/x86/' \
|
||||
| sed 's/arm.*/arm/' \
|
||||
| sed 's/aarch64/arm64/' \
|
||||
| sed 's/ppc64le/powerpc/' \
|
||||
| sed 's/mips.*/mips/' \
|
||||
| sed 's/riscv64/riscv/' \
|
||||
| sed 's/loongarch64/loongarch/')
|
||||
VMLINUX := ../third_party/vmlinux/$(ARCH)/vmlinux.h
|
||||
INCLUDES := -I$(OUTPUT) -I../third_party/libbpf/include/uapi -I$(dir $(VMLINUX)) -I.
|
||||
CFLAGS := -g -Wall -Wextra -D_GNU_SOURCE
|
||||
ALL_LDFLAGS := $(LDFLAGS) $(EXTRA_LDFLAGS)
|
||||
APPS = dns_egress
|
||||
|
||||
CLANG_BPF_SYS_INCLUDES ?= $(shell $(CLANG) -v -E - </dev/null 2>&1 \
|
||||
| sed -n '/<...> search starts here:/,/End of search list./{ s| \(/.*\)|-idirafter \1|p }')
|
||||
|
||||
ifeq ($(V),1)
|
||||
Q =
|
||||
msg =
|
||||
else
|
||||
Q = @
|
||||
msg = @printf ' %-8s %s%s\n' "$(1)" \
|
||||
"$(patsubst $(abspath $(OUTPUT))/%,%,$(2))" \
|
||||
"$(if $(3), $(3))";
|
||||
MAKEFLAGS += --no-print-directory
|
||||
endif
|
||||
|
||||
define allow-override
|
||||
$(if $(or $(findstring environment,$(origin $(1))),\
|
||||
$(findstring command line,$(origin $(1)))),,\
|
||||
$(eval $(1) = $(2)))
|
||||
endef
|
||||
|
||||
$(call allow-override,CC,$(CROSS_COMPILE)cc)
|
||||
|
||||
.PHONY: all clean test
|
||||
all: $(APPS)
|
||||
|
||||
clean:
|
||||
$(call msg,CLEAN)
|
||||
$(Q)rm -rf $(OUTPUT) $(APPS)
|
||||
|
||||
test: all
|
||||
@if [ "$$(id -u)" -ne 0 ]; then \
|
||||
echo "test requires root; run inside the documented KVM guest" >&2; \
|
||||
exit 1; \
|
||||
fi
|
||||
python3 tests/test_dns_egress.py ./dns_egress
|
||||
|
||||
$(OUTPUT) $(OUTPUT)/libbpf $(BPFTOOL_OUTPUT):
|
||||
$(call msg,MKDIR,$@)
|
||||
$(Q)mkdir -p $@
|
||||
|
||||
$(LIBBPF_OBJ): $(wildcard $(LIBBPF_SRC)/*.[ch] $(LIBBPF_SRC)/Makefile) | $(OUTPUT)/libbpf
|
||||
$(call msg,LIB,$@)
|
||||
$(Q)$(MAKE) -C $(LIBBPF_SRC) BUILD_STATIC_ONLY=1 \
|
||||
OBJDIR=$(dir $@)/libbpf DESTDIR=$(dir $@) \
|
||||
INCLUDEDIR= LIBDIR= UAPIDIR= install
|
||||
|
||||
$(BPFTOOL): | $(BPFTOOL_OUTPUT)
|
||||
$(call msg,BPFTOOL,$@)
|
||||
$(Q)$(MAKE) ARCH= CROSS_COMPILE= OUTPUT=$(BPFTOOL_OUTPUT)/ -C $(BPFTOOL_SRC) bootstrap
|
||||
|
||||
$(OUTPUT)/%.bpf.o: %.bpf.c $(LIBBPF_OBJ) $(wildcard %.h) $(VMLINUX) | $(OUTPUT) $(BPFTOOL)
|
||||
$(call msg,BPF,$@)
|
||||
$(Q)$(CLANG) -g -O2 -target bpf -D__TARGET_ARCH_$(ARCH) \
|
||||
$(INCLUDES) $(CLANG_BPF_SYS_INCLUDES) \
|
||||
-c $(filter %.c,$^) -o $(patsubst %.bpf.o,%.tmp.bpf.o,$@)
|
||||
$(Q)$(BPFTOOL) gen object $@ $(patsubst %.bpf.o,%.tmp.bpf.o,$@)
|
||||
|
||||
$(OUTPUT)/%.skel.h: $(OUTPUT)/%.bpf.o | $(OUTPUT) $(BPFTOOL)
|
||||
$(call msg,GEN-SKEL,$@)
|
||||
$(Q)$(BPFTOOL) gen skeleton $< > $@
|
||||
|
||||
$(patsubst %,$(OUTPUT)/%.o,$(APPS)): %.o: %.skel.h
|
||||
|
||||
$(OUTPUT)/%.o: %.c $(wildcard %.h) | $(OUTPUT)
|
||||
$(call msg,CC,$@)
|
||||
$(Q)$(CC) $(CFLAGS) $(INCLUDES) -c $(filter %.c,$^) -o $@
|
||||
|
||||
$(APPS): %: $(OUTPUT)/%.o $(LIBBPF_OBJ) | $(OUTPUT)
|
||||
$(call msg,BINARY,$@)
|
||||
$(Q)$(CC) $(CFLAGS) $^ $(ALL_LDFLAGS) -lelf -lz -o $@
|
||||
|
||||
.DELETE_ON_ERROR:
|
||||
.SECONDARY:
|
||||
931
src/55-dns-egress/README.md
Normal file
931
src/55-dns-egress/README.md
Normal file
@@ -0,0 +1,931 @@
|
||||
# eBPF Tutorial by Example: DNS-Aware Egress Policy Enforcement
|
||||
|
||||
How do you allow TCP connections on one protected port only to IP addresses that your application just resolved through DNS? This tutorial builds a cgroup-based egress filter that learns IPs from DNS responses, allows matching connections while the DNS TTL is valid, and rejects other destinations on that port. The kernel enforces the policy at connect time.
|
||||
|
||||
> Complete source code: <https://github.com/eunomia-bpf/bpf-developer-tutorial/tree/main/src/55-dns-egress>
|
||||
|
||||
## eBPF and cgroup Hooks
|
||||
|
||||
eBPF lets verified programs run at Linux kernel hooks and send selected state to user space. This tutorial uses cgroup BPF hooks to intercept network packets and connection attempts for one cgroup. The `cgroup_skb/egress` and `cgroup_skb/ingress` hooks observe DNS traffic, while `cgroup/connect4` intercepts IPv4 connect calls. LRU hash maps keep the most recently used query and address entries within fixed capacities, and the BPF ring buffer, introduced in Linux 5.8, carries policy events to user space. This implementation therefore requires Linux 5.8 or later.
|
||||
|
||||
## How the Implementation Works
|
||||
|
||||
The tool attaches three BPF programs to a cgroup. The egress program records outgoing DNS queries by saving the transaction ID, client port, and server IP as a pending query. Queries expire after 5 seconds if no matching response arrives. The ingress program learns IPs from DNS responses by verifying the response matches a pending query and extracting the A record IP with its TTL. For TCP connections to the configured protected port, the connect hook checks whether the destination IP exists in the allowed list and has not expired. Other destination ports proceed unchanged.
|
||||
|
||||
## Header File
|
||||
|
||||
```c
|
||||
/* SPDX-License-Identifier: (LGPL-2.1 OR BSD-2-Clause) */
|
||||
#ifndef __DNS_EGRESS_H
|
||||
#define __DNS_EGRESS_H
|
||||
|
||||
#define DNS_QNAME_MAX 64
|
||||
|
||||
enum dns_egress_event_type {
|
||||
DNS_LEARNED = 1,
|
||||
DNS_ALLOWED = 2,
|
||||
DNS_DENIED = 3,
|
||||
DNS_EXPIRED = 4,
|
||||
};
|
||||
|
||||
struct dns_header {
|
||||
unsigned short id;
|
||||
unsigned short flags;
|
||||
unsigned short questions;
|
||||
unsigned short answers;
|
||||
unsigned short authorities;
|
||||
unsigned short additionals;
|
||||
} __attribute__((packed));
|
||||
|
||||
struct dns_question {
|
||||
unsigned short type;
|
||||
unsigned short class;
|
||||
} __attribute__((packed));
|
||||
|
||||
struct dns_a_answer {
|
||||
unsigned short name;
|
||||
unsigned short type;
|
||||
unsigned short class;
|
||||
unsigned int ttl;
|
||||
unsigned short address_length;
|
||||
unsigned int address;
|
||||
} __attribute__((packed));
|
||||
|
||||
struct dns_egress_event {
|
||||
unsigned long long timestamp_ns;
|
||||
unsigned long long expires_ns;
|
||||
unsigned int type;
|
||||
unsigned int pid;
|
||||
unsigned int ip4;
|
||||
unsigned int ttl_seconds;
|
||||
};
|
||||
|
||||
#endif /* __DNS_EGRESS_H */
|
||||
```
|
||||
|
||||
The header defines DNS protocol structures used to parse packets. The `dns_header` structure matches the DNS message header format. The `dns_question` and `dns_a_answer` structures parse the query and answer sections. The `dns_egress_event` structure carries policy events (learned, allowed, denied, expired) to user space through the ring buffer.
|
||||
|
||||
## BPF Program
|
||||
|
||||
```c
|
||||
// SPDX-License-Identifier: GPL-2.0
|
||||
#include "vmlinux.h"
|
||||
#include <bpf/bpf_helpers.h>
|
||||
#include <bpf/bpf_endian.h>
|
||||
#include "dns_egress.h"
|
||||
|
||||
char LICENSE[] SEC("license") = "GPL";
|
||||
|
||||
#define IPPROTO_UDP 17
|
||||
#define IPPROTO_TCP 6
|
||||
#define IP_MF 0x2000
|
||||
#define IP_OFFSET 0x1fff
|
||||
#define DNS_QUERY_LIFETIME_NS (5ULL * 1000000000ULL)
|
||||
|
||||
const volatile __u32 target_tgid;
|
||||
const volatile __u32 dns_server_ip;
|
||||
const volatile __u16 dns_server_port = 53;
|
||||
const volatile __u16 protected_tcp_port = 443;
|
||||
const volatile __u32 configured_qname_length;
|
||||
const volatile unsigned char configured_qname[DNS_QNAME_MAX];
|
||||
|
||||
struct dns_state {
|
||||
__u64 expires_ns;
|
||||
__u32 ttl_seconds;
|
||||
__u32 pad;
|
||||
__u64 expired_reported;
|
||||
};
|
||||
|
||||
struct dns_query_key {
|
||||
__u32 server_ip;
|
||||
__u32 client_ip;
|
||||
__u16 client_port;
|
||||
__u16 transaction_id;
|
||||
};
|
||||
|
||||
struct dns_query_state {
|
||||
__u64 expires_ns;
|
||||
};
|
||||
|
||||
struct {
|
||||
__uint(type, BPF_MAP_TYPE_LRU_HASH);
|
||||
__uint(max_entries, 1024);
|
||||
__type(key, struct dns_query_key);
|
||||
__type(value, struct dns_query_state);
|
||||
} pending_queries SEC(".maps");
|
||||
|
||||
struct {
|
||||
__uint(type, BPF_MAP_TYPE_LRU_HASH);
|
||||
__uint(max_entries, 1024);
|
||||
__type(key, __u32);
|
||||
__type(value, struct dns_state);
|
||||
} allowed_ips SEC(".maps");
|
||||
|
||||
struct {
|
||||
__uint(type, BPF_MAP_TYPE_RINGBUF);
|
||||
__uint(max_entries, 256 * 1024);
|
||||
} events SEC(".maps");
|
||||
|
||||
static __always_inline void emit_event(__u32 type, __u32 ip4,
|
||||
__u32 ttl_seconds, __u64 expires_ns)
|
||||
{
|
||||
struct dns_egress_event *event;
|
||||
|
||||
event = bpf_ringbuf_reserve(&events, sizeof(*event), 0);
|
||||
if (!event)
|
||||
return;
|
||||
event->timestamp_ns = bpf_ktime_get_ns();
|
||||
event->expires_ns = expires_ns;
|
||||
event->type = type;
|
||||
event->pid = bpf_get_current_pid_tgid() >> 32;
|
||||
event->ip4 = ip4;
|
||||
event->ttl_seconds = ttl_seconds;
|
||||
bpf_ringbuf_submit(event, 0);
|
||||
}
|
||||
|
||||
static __noinline bool matches_qname(struct __sk_buff *skb, __u32 offset)
|
||||
{
|
||||
unsigned char byte;
|
||||
|
||||
if (!configured_qname_length || configured_qname_length > DNS_QNAME_MAX)
|
||||
return false;
|
||||
#pragma clang loop unroll(disable)
|
||||
for (int i = 0; i < DNS_QNAME_MAX; i++) {
|
||||
if (i >= configured_qname_length)
|
||||
break;
|
||||
if (bpf_skb_load_bytes(skb, offset + i, &byte, sizeof(byte)) ||
|
||||
byte != configured_qname[i])
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
SEC("cgroup_skb/egress")
|
||||
int record_dns_query(struct __sk_buff *skb)
|
||||
{
|
||||
struct dns_query_state state = {
|
||||
.expires_ns = bpf_ktime_get_ns() + DNS_QUERY_LIFETIME_NS,
|
||||
};
|
||||
struct dns_query_key key = {};
|
||||
struct dns_question question;
|
||||
struct dns_header header;
|
||||
struct udphdr udp;
|
||||
struct iphdr ip;
|
||||
__u32 ip_header_len;
|
||||
__u32 dns_offset;
|
||||
__u16 flags;
|
||||
|
||||
if (bpf_skb_load_bytes(skb, 0, &ip, sizeof(ip)))
|
||||
return 1;
|
||||
if (ip.version != 4 || ip.protocol != IPPROTO_UDP || ip.ihl < 5 ||
|
||||
(bpf_ntohs(ip.frag_off) & (IP_MF | IP_OFFSET)) ||
|
||||
ip.daddr != dns_server_ip)
|
||||
return 1;
|
||||
ip_header_len = ip.ihl * 4;
|
||||
if (bpf_skb_load_bytes(skb, ip_header_len, &udp, sizeof(udp)) ||
|
||||
bpf_ntohs(udp.dest) != dns_server_port)
|
||||
return 1;
|
||||
dns_offset = ip_header_len + sizeof(udp);
|
||||
if (bpf_skb_load_bytes(skb, dns_offset, &header, sizeof(header)))
|
||||
return 1;
|
||||
flags = bpf_ntohs(header.flags);
|
||||
if ((flags & 0xf800) || bpf_ntohs(header.questions) != 1)
|
||||
return 1;
|
||||
if (!matches_qname(skb, dns_offset + sizeof(header)))
|
||||
return 1;
|
||||
dns_offset += sizeof(header) + configured_qname_length;
|
||||
if (bpf_skb_load_bytes(skb, dns_offset, &question, sizeof(question)) ||
|
||||
bpf_ntohs(question.type) != 1 || bpf_ntohs(question.class) != 1)
|
||||
return 1;
|
||||
|
||||
key.server_ip = ip.daddr;
|
||||
key.client_ip = ip.saddr;
|
||||
key.client_port = udp.source;
|
||||
key.transaction_id = header.id;
|
||||
bpf_map_update_elem(&pending_queries, &key, &state, BPF_ANY);
|
||||
return 1;
|
||||
}
|
||||
|
||||
SEC("cgroup_skb/ingress")
|
||||
int learn_dns_answer(struct __sk_buff *skb)
|
||||
{
|
||||
struct dns_a_answer answer;
|
||||
struct dns_question question;
|
||||
struct dns_header header;
|
||||
struct dns_query_key query_key = {};
|
||||
struct dns_query_state *query;
|
||||
struct dns_state state = {};
|
||||
struct udphdr udp;
|
||||
struct iphdr ip;
|
||||
__u64 ttl_ns, expires;
|
||||
__u32 ip_header_len;
|
||||
__u32 dns_offset;
|
||||
__u32 key;
|
||||
__u32 ttl;
|
||||
|
||||
if (bpf_skb_load_bytes(skb, 0, &ip, sizeof(ip)))
|
||||
return 1;
|
||||
if (ip.version != 4 || ip.protocol != IPPROTO_UDP || ip.ihl < 5 ||
|
||||
(bpf_ntohs(ip.frag_off) & (IP_MF | IP_OFFSET)) ||
|
||||
ip.saddr != dns_server_ip)
|
||||
return 1;
|
||||
ip_header_len = ip.ihl * 4;
|
||||
if (bpf_skb_load_bytes(skb, ip_header_len, &udp, sizeof(udp)) ||
|
||||
bpf_ntohs(udp.source) != dns_server_port)
|
||||
return 1;
|
||||
dns_offset = ip_header_len + sizeof(udp);
|
||||
if (bpf_skb_load_bytes(skb, dns_offset, &header, sizeof(header)))
|
||||
return 1;
|
||||
query_key.server_ip = ip.saddr;
|
||||
query_key.client_ip = ip.daddr;
|
||||
query_key.client_port = udp.dest;
|
||||
query_key.transaction_id = header.id;
|
||||
query = bpf_map_lookup_elem(&pending_queries, &query_key);
|
||||
if (!query)
|
||||
return 1;
|
||||
if (bpf_ktime_get_ns() >= query->expires_ns) {
|
||||
bpf_map_delete_elem(&pending_queries, &query_key);
|
||||
return 1;
|
||||
}
|
||||
if ((bpf_ntohs(header.flags) & 0xf80f) != 0x8000 ||
|
||||
bpf_ntohs(header.questions) != 1 || !bpf_ntohs(header.answers))
|
||||
return 1;
|
||||
if (!matches_qname(skb, dns_offset + sizeof(header)))
|
||||
return 1;
|
||||
dns_offset += sizeof(header) + configured_qname_length;
|
||||
if (bpf_skb_load_bytes(skb, dns_offset, &question, sizeof(question)) ||
|
||||
bpf_ntohs(question.type) != 1 || bpf_ntohs(question.class) != 1)
|
||||
return 1;
|
||||
dns_offset += sizeof(question);
|
||||
if (bpf_skb_load_bytes(skb, dns_offset, &answer, sizeof(answer)) ||
|
||||
bpf_ntohs(answer.name) != 0xc00c || bpf_ntohs(answer.type) != 1 ||
|
||||
bpf_ntohs(answer.class) != 1 ||
|
||||
bpf_ntohs(answer.address_length) != 4)
|
||||
return 1;
|
||||
bpf_map_delete_elem(&pending_queries, &query_key);
|
||||
|
||||
key = answer.address;
|
||||
ttl = bpf_ntohl(answer.ttl);
|
||||
if (!ttl || ttl > 86400)
|
||||
return 1;
|
||||
ttl_ns = (__u64)ttl * 1000000000ULL;
|
||||
expires = bpf_ktime_get_ns() + ttl_ns;
|
||||
state.expires_ns = expires;
|
||||
state.ttl_seconds = ttl;
|
||||
if (bpf_map_update_elem(&allowed_ips, &key, &state, BPF_ANY))
|
||||
return 1;
|
||||
emit_event(DNS_LEARNED, key, ttl, expires);
|
||||
return 1;
|
||||
}
|
||||
|
||||
SEC("cgroup/connect4")
|
||||
int enforce_dns_policy(struct bpf_sock_addr *ctx)
|
||||
{
|
||||
struct dns_state *state;
|
||||
__u64 expires = 0;
|
||||
__u32 ip4;
|
||||
__u32 ttl = 0;
|
||||
|
||||
if ((target_tgid &&
|
||||
(__u32)(bpf_get_current_pid_tgid() >> 32) != target_tgid) ||
|
||||
ctx->protocol != IPPROTO_TCP ||
|
||||
bpf_ntohs((__u16)ctx->user_port) != protected_tcp_port)
|
||||
return 1;
|
||||
|
||||
ip4 = ctx->user_ip4;
|
||||
state = bpf_map_lookup_elem(&allowed_ips, &ip4);
|
||||
if (state) {
|
||||
expires = state->expires_ns;
|
||||
ttl = state->ttl_seconds;
|
||||
if (bpf_ktime_get_ns() < expires) {
|
||||
emit_event(DNS_ALLOWED, ip4, ttl, expires);
|
||||
return 1;
|
||||
}
|
||||
if (__sync_val_compare_and_swap(&state->expired_reported, 0, 1) == 0)
|
||||
emit_event(DNS_EXPIRED, ip4, ttl, expires);
|
||||
}
|
||||
emit_event(DNS_DENIED, ip4, ttl, expires);
|
||||
return 0;
|
||||
}
|
||||
```
|
||||
|
||||
The BPF program uses three maps. The `pending_queries` LRU hash tracks outgoing DNS queries using a composite key of server IP, client IP, client port, and transaction ID. The `allowed_ips` LRU hash stores learned IPs with their expiration timestamps. The `events` ring buffer sends policy decisions to user space.
|
||||
|
||||
The `record_dns_query` function runs on cgroup egress and captures DNS queries to the configured resolver. It validates the packet is IPv4 UDP to the correct server and port, checks the query name matches, and stores the query with a 5-second expiration.
|
||||
|
||||
The `learn_dns_answer` function runs on cgroup ingress and parses DNS responses. It looks up the matching pending query, validates the response format, extracts the IP address and TTL from the first A record, and stores the IP in the allowed list with the TTL-based expiration.
|
||||
|
||||
The `enforce_dns_policy` function runs when a process calls `connect()`. IPv4 TCP connections to the configured protected port are checked against the allowed list and its expiration timestamps. A successful lookup returns 1 to permit the connection; a missing or stale entry returns 0, which makes `connect()` fail with `EPERM`. Connections to other ports return 1 immediately.
|
||||
|
||||
## User Space Program
|
||||
|
||||
```c
|
||||
// SPDX-License-Identifier: (LGPL-2.1 OR BSD-2-Clause)
|
||||
#include <arpa/inet.h>
|
||||
#include <errno.h>
|
||||
#include <fcntl.h>
|
||||
#include <getopt.h>
|
||||
#include <netinet/in.h>
|
||||
#include <signal.h>
|
||||
#include <stdbool.h>
|
||||
#include <stddef.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <sys/socket.h>
|
||||
#include <time.h>
|
||||
#include <unistd.h>
|
||||
#include <bpf/libbpf.h>
|
||||
#include "dns_egress.h"
|
||||
#include "dns_egress.skel.h"
|
||||
|
||||
#define DEMO_DNS_PORT 15353
|
||||
#define DEMO_TCP_PORT 19090
|
||||
#define DNS_ID 0x4b1d
|
||||
|
||||
struct options {
|
||||
const char *cgroup_path;
|
||||
const char *domain;
|
||||
const char *dns_server;
|
||||
unsigned int port;
|
||||
unsigned int dns_port;
|
||||
unsigned int duration_seconds;
|
||||
bool demo;
|
||||
};
|
||||
|
||||
static int event_counts[5];
|
||||
static volatile sig_atomic_t stop;
|
||||
|
||||
static void handle_signal(int signal_number)
|
||||
{
|
||||
(void)signal_number;
|
||||
stop = 1;
|
||||
}
|
||||
|
||||
static unsigned long long monotonic_ns(void)
|
||||
{
|
||||
struct timespec now;
|
||||
|
||||
clock_gettime(CLOCK_MONOTONIC, &now);
|
||||
return (unsigned long long)now.tv_sec * 1000000000ULL + now.tv_nsec;
|
||||
}
|
||||
|
||||
static int handle_event(void *ctx, void *data, size_t size)
|
||||
{
|
||||
const struct dns_egress_event *event = data;
|
||||
char address[INET_ADDRSTRLEN];
|
||||
const char *name;
|
||||
|
||||
(void)ctx;
|
||||
if (size != sizeof(*event) || event->type > DNS_EXPIRED)
|
||||
return 0;
|
||||
event_counts[event->type]++;
|
||||
inet_ntop(AF_INET, &event->ip4, address, sizeof(address));
|
||||
name = event->type == DNS_LEARNED ? "learned" :
|
||||
event->type == DNS_ALLOWED ? "allowed" :
|
||||
event->type == DNS_DENIED ? "denied" : "expired";
|
||||
printf("event=%s pid=%u ip=%s ttl=%u\n", name, event->pid, address,
|
||||
event->ttl_seconds);
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int parse_uint(const char *text, unsigned int maximum,
|
||||
unsigned int *value)
|
||||
{
|
||||
char *end = NULL;
|
||||
unsigned long parsed;
|
||||
|
||||
errno = 0;
|
||||
parsed = strtoul(text, &end, 10);
|
||||
if (errno || !*text || *end || parsed > maximum)
|
||||
return -1;
|
||||
*value = parsed;
|
||||
return 0;
|
||||
}
|
||||
|
||||
static void usage(const char *program)
|
||||
{
|
||||
printf("Usage: %s --cgroup PATH --domain NAME --dns-server IPV4 [--port PORT] [--dns-port PORT] [--duration SEC]\n"
|
||||
" %s --demo\n", program, program);
|
||||
}
|
||||
|
||||
static int parse_options(int argc, char **argv, struct options *options)
|
||||
{
|
||||
static const struct option long_options[] = {
|
||||
{ "cgroup", required_argument, NULL, 'c' },
|
||||
{ "domain", required_argument, NULL, 'n' },
|
||||
{ "dns-server", required_argument, NULL, 'r' },
|
||||
{ "port", required_argument, NULL, 'p' },
|
||||
{ "dns-port", required_argument, NULL, 's' },
|
||||
{ "duration", required_argument, NULL, 'd' },
|
||||
{ "demo", no_argument, NULL, 'D' },
|
||||
{ "help", no_argument, NULL, 'h' },
|
||||
{},
|
||||
};
|
||||
int option;
|
||||
|
||||
while ((option = getopt_long(argc, argv, "c:n:r:p:s:d:Dh", long_options,
|
||||
NULL)) != -1) {
|
||||
switch (option) {
|
||||
case 'c': options->cgroup_path = optarg; break;
|
||||
case 'n': options->domain = optarg; break;
|
||||
case 'r': options->dns_server = optarg; break;
|
||||
case 'p':
|
||||
if (parse_uint(optarg, 65535, &options->port) ||
|
||||
!options->port)
|
||||
return -1;
|
||||
break;
|
||||
case 's':
|
||||
if (parse_uint(optarg, 65535, &options->dns_port) ||
|
||||
!options->dns_port)
|
||||
return -1;
|
||||
break;
|
||||
case 'd':
|
||||
if (parse_uint(optarg, 86400,
|
||||
&options->duration_seconds))
|
||||
return -1;
|
||||
break;
|
||||
case 'D': options->demo = true; break;
|
||||
case 'h': usage(argv[0]); exit(0);
|
||||
default: return -1;
|
||||
}
|
||||
}
|
||||
if (optind != argc)
|
||||
return -1;
|
||||
if (options->demo) {
|
||||
if (options->cgroup_path || options->domain || options->dns_server)
|
||||
return -1;
|
||||
options->cgroup_path = "/sys/fs/cgroup";
|
||||
options->domain = "lab.test";
|
||||
options->dns_server = "127.0.0.1";
|
||||
options->port = DEMO_TCP_PORT;
|
||||
options->dns_port = DEMO_DNS_PORT;
|
||||
return 0;
|
||||
}
|
||||
return options->cgroup_path && options->domain && options->dns_server ?
|
||||
0 : -1;
|
||||
}
|
||||
|
||||
static int encode_qname(const char *domain, unsigned char output[DNS_QNAME_MAX],
|
||||
unsigned int *output_length)
|
||||
{
|
||||
const char *label = domain;
|
||||
unsigned int used = 0;
|
||||
|
||||
if (!*domain)
|
||||
return -1;
|
||||
while (*label) {
|
||||
const char *dot = strchr(label, '.');
|
||||
size_t length = dot ? (size_t)(dot - label) : strlen(label);
|
||||
|
||||
if (!length || length > 63 || used + length + 2 > DNS_QNAME_MAX)
|
||||
return -1;
|
||||
output[used++] = length;
|
||||
memcpy(output + used, label, length);
|
||||
used += length;
|
||||
if (!dot)
|
||||
break;
|
||||
label = dot + 1;
|
||||
if (!*label)
|
||||
break;
|
||||
}
|
||||
output[used++] = 0;
|
||||
*output_length = used;
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int bind_udp(struct sockaddr_in *address)
|
||||
{
|
||||
socklen_t length = sizeof(*address);
|
||||
int fd;
|
||||
|
||||
fd = socket(AF_INET, SOCK_DGRAM | SOCK_CLOEXEC, 0);
|
||||
if (fd < 0)
|
||||
return -1;
|
||||
if (bind(fd, (struct sockaddr *)address, sizeof(*address)) ||
|
||||
getsockname(fd, (struct sockaddr *)address, &length)) {
|
||||
close(fd);
|
||||
return -1;
|
||||
}
|
||||
return fd;
|
||||
}
|
||||
|
||||
static int begin_dns_exchange(int server, int client,
|
||||
struct sockaddr_in *server_address,
|
||||
const unsigned char *qname,
|
||||
unsigned int qname_length,
|
||||
unsigned char message[512],
|
||||
size_t *query_length,
|
||||
struct sockaddr_in *client_address)
|
||||
{
|
||||
struct dns_question question = {
|
||||
.type = htons(1),
|
||||
.class = htons(1),
|
||||
};
|
||||
struct dns_header *header = (void *)message;
|
||||
socklen_t address_length = sizeof(*client_address);
|
||||
ssize_t received;
|
||||
|
||||
memset(message, 0, 512);
|
||||
header->id = htons(DNS_ID);
|
||||
header->flags = htons(0x0100);
|
||||
header->questions = htons(1);
|
||||
memcpy(message + sizeof(*header), qname, qname_length);
|
||||
memcpy(message + sizeof(*header) + qname_length, &question,
|
||||
sizeof(question));
|
||||
*query_length = sizeof(*header) + qname_length + sizeof(question);
|
||||
if (sendto(client, message, *query_length, 0,
|
||||
(struct sockaddr *)server_address, sizeof(*server_address)) !=
|
||||
(ssize_t)*query_length)
|
||||
return -1;
|
||||
received = recvfrom(server, message, 512, 0,
|
||||
(struct sockaddr *)client_address, &address_length);
|
||||
return received == (ssize_t)*query_length ? 0 : -1;
|
||||
}
|
||||
|
||||
static int send_dns_answer(int server, int client,
|
||||
struct sockaddr_in *client_address,
|
||||
unsigned char message[512], size_t query_length,
|
||||
unsigned short transaction_id,
|
||||
unsigned int ttl_seconds)
|
||||
{
|
||||
struct dns_a_answer answer = {
|
||||
.name = htons(0xc00c),
|
||||
.type = htons(1),
|
||||
.class = htons(1),
|
||||
.ttl = htonl(ttl_seconds),
|
||||
.address_length = htons(4),
|
||||
};
|
||||
struct dns_header *header = (void *)message;
|
||||
|
||||
if (query_length + sizeof(answer) > 512)
|
||||
return -1;
|
||||
header->id = htons(transaction_id);
|
||||
header->flags = htons(0x8180);
|
||||
header->answers = htons(1);
|
||||
inet_pton(AF_INET, "127.0.0.1", &answer.address);
|
||||
memcpy(message + query_length, &answer, sizeof(answer));
|
||||
if (sendto(server, message, query_length + sizeof(answer), 0,
|
||||
(struct sockaddr *)client_address, sizeof(*client_address)) !=
|
||||
(ssize_t)(query_length + sizeof(answer)))
|
||||
return -1;
|
||||
return recv(client, message, 512, 0) ==
|
||||
(ssize_t)(query_length + sizeof(answer)) ? 0 : -1;
|
||||
}
|
||||
|
||||
static int send_unsolicited_dns(int server, int client,
|
||||
const unsigned char *qname,
|
||||
unsigned int qname_length)
|
||||
{
|
||||
unsigned char message[512] = {};
|
||||
struct sockaddr_in client_address;
|
||||
struct dns_a_answer answer = {
|
||||
.name = htons(0xc00c),
|
||||
.type = htons(1),
|
||||
.class = htons(1),
|
||||
.ttl = htonl(30),
|
||||
.address_length = htons(4),
|
||||
};
|
||||
struct dns_question question = {
|
||||
.type = htons(1),
|
||||
.class = htons(1),
|
||||
};
|
||||
struct dns_header *header = (void *)message;
|
||||
socklen_t address_length = sizeof(client_address);
|
||||
size_t message_length;
|
||||
|
||||
if (getsockname(client, (struct sockaddr *)&client_address,
|
||||
&address_length))
|
||||
return -1;
|
||||
header->id = htons(DNS_ID + 1);
|
||||
header->flags = htons(0x8180);
|
||||
header->questions = htons(1);
|
||||
header->answers = htons(1);
|
||||
memcpy(message + sizeof(*header), qname, qname_length);
|
||||
memcpy(message + sizeof(*header) + qname_length, &question,
|
||||
sizeof(question));
|
||||
message_length = sizeof(*header) + qname_length + sizeof(question);
|
||||
inet_pton(AF_INET, "127.0.0.1", &answer.address);
|
||||
memcpy(message + message_length, &answer, sizeof(answer));
|
||||
message_length += sizeof(answer);
|
||||
if (sendto(server, message, message_length, 0,
|
||||
(struct sockaddr *)&client_address, address_length) !=
|
||||
(ssize_t)message_length)
|
||||
return -1;
|
||||
return recv(client, message, sizeof(message), 0) ==
|
||||
(ssize_t)message_length ? 0 : -1;
|
||||
}
|
||||
|
||||
static int create_tcp_listener(unsigned int port)
|
||||
{
|
||||
struct sockaddr_in address = {
|
||||
.sin_family = AF_INET,
|
||||
.sin_addr.s_addr = htonl(INADDR_LOOPBACK),
|
||||
.sin_port = htons(port),
|
||||
};
|
||||
int one = 1;
|
||||
int fd;
|
||||
|
||||
fd = socket(AF_INET, SOCK_STREAM | SOCK_CLOEXEC, 0);
|
||||
if (fd < 0)
|
||||
return -1;
|
||||
setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &one, sizeof(one));
|
||||
if (bind(fd, (struct sockaddr *)&address, sizeof(address)) ||
|
||||
listen(fd, 4)) {
|
||||
close(fd);
|
||||
return -1;
|
||||
}
|
||||
return fd;
|
||||
}
|
||||
|
||||
static int connect_tcp(unsigned int port)
|
||||
{
|
||||
struct sockaddr_in address = {
|
||||
.sin_family = AF_INET,
|
||||
.sin_addr.s_addr = htonl(INADDR_LOOPBACK),
|
||||
.sin_port = htons(port),
|
||||
};
|
||||
int saved_errno;
|
||||
int fd;
|
||||
|
||||
fd = socket(AF_INET, SOCK_STREAM | SOCK_CLOEXEC, 0);
|
||||
if (fd < 0)
|
||||
return -1;
|
||||
if (!connect(fd, (struct sockaddr *)&address, sizeof(address)))
|
||||
return fd;
|
||||
saved_errno = errno;
|
||||
close(fd);
|
||||
errno = saved_errno;
|
||||
return -1;
|
||||
}
|
||||
|
||||
static int complete_tcp(int listener, int client)
|
||||
{
|
||||
int accepted = accept4(listener, NULL, NULL, SOCK_CLOEXEC);
|
||||
char byte = 'x';
|
||||
|
||||
if (accepted < 0)
|
||||
return -1;
|
||||
if (write(accepted, &byte, 1) != 1 || read(client, &byte, 1) != 1) {
|
||||
close(accepted);
|
||||
return -1;
|
||||
}
|
||||
close(accepted);
|
||||
close(client);
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int run_demo(struct ring_buffer *ring, const struct options *options,
|
||||
const unsigned char *qname, unsigned int qname_length)
|
||||
{
|
||||
struct sockaddr_in server_address = {
|
||||
.sin_family = AF_INET,
|
||||
.sin_addr.s_addr = htonl(INADDR_LOOPBACK),
|
||||
.sin_port = htons(options->dns_port),
|
||||
};
|
||||
struct sockaddr_in client_address = {
|
||||
.sin_family = AF_INET,
|
||||
.sin_addr.s_addr = htonl(INADDR_LOOPBACK),
|
||||
};
|
||||
struct timespec wait_time = { .tv_sec = 1, .tv_nsec = 300000000 };
|
||||
struct sockaddr_in response_client_address;
|
||||
unsigned char dns_message[512];
|
||||
size_t query_length;
|
||||
int dns_server = -1, dns_client = -1, listener = -1, client = -1;
|
||||
int err = -1;
|
||||
|
||||
dns_server = bind_udp(&server_address);
|
||||
dns_client = bind_udp(&client_address);
|
||||
listener = create_tcp_listener(options->port);
|
||||
if (dns_server < 0 || dns_client < 0 || listener < 0)
|
||||
goto cleanup;
|
||||
|
||||
errno = 0;
|
||||
client = connect_tcp(options->port);
|
||||
if (client >= 0 || errno != EPERM)
|
||||
goto cleanup;
|
||||
ring_buffer__poll(ring, 100);
|
||||
printf("demo step=before-dns result=blocked\n");
|
||||
|
||||
if (send_unsolicited_dns(dns_server, dns_client, qname, qname_length))
|
||||
goto cleanup;
|
||||
ring_buffer__poll(ring, 100);
|
||||
errno = 0;
|
||||
client = connect_tcp(options->port);
|
||||
if (client >= 0 || errno != EPERM)
|
||||
goto cleanup;
|
||||
ring_buffer__poll(ring, 100);
|
||||
printf("demo step=unsolicited-response result=blocked\n");
|
||||
|
||||
if (begin_dns_exchange(dns_server, dns_client, &server_address, qname,
|
||||
qname_length, dns_message, &query_length,
|
||||
&response_client_address) ||
|
||||
send_dns_answer(dns_server, dns_client, &response_client_address,
|
||||
dns_message, query_length, DNS_ID + 1, 30))
|
||||
goto cleanup;
|
||||
ring_buffer__poll(ring, 100);
|
||||
errno = 0;
|
||||
client = connect_tcp(options->port);
|
||||
if (client >= 0 || errno != EPERM)
|
||||
goto cleanup;
|
||||
ring_buffer__poll(ring, 100);
|
||||
printf("demo step=wrong-transaction-id result=blocked\n");
|
||||
|
||||
if (send_dns_answer(dns_server, dns_client, &response_client_address,
|
||||
dns_message, query_length, DNS_ID, 1))
|
||||
goto cleanup;
|
||||
ring_buffer__poll(ring, 100);
|
||||
client = connect_tcp(options->port);
|
||||
if (client < 0 || complete_tcp(listener, client))
|
||||
goto cleanup;
|
||||
client = -1;
|
||||
ring_buffer__poll(ring, 100);
|
||||
printf("demo step=live-answer result=allowed\n");
|
||||
|
||||
nanosleep(&wait_time, NULL);
|
||||
ring_buffer__poll(ring, 100);
|
||||
errno = 0;
|
||||
client = connect_tcp(options->port);
|
||||
if (client >= 0 || errno != EPERM)
|
||||
goto cleanup;
|
||||
ring_buffer__poll(ring, 100);
|
||||
printf("demo step=expired-answer result=blocked\n");
|
||||
|
||||
if (event_counts[DNS_LEARNED] != 1 || event_counts[DNS_ALLOWED] != 1 ||
|
||||
event_counts[DNS_DENIED] != 4 || event_counts[DNS_EXPIRED] != 1)
|
||||
goto cleanup;
|
||||
err = 0;
|
||||
|
||||
cleanup:
|
||||
if (client >= 0) close(client);
|
||||
if (listener >= 0) close(listener);
|
||||
if (dns_client >= 0) close(dns_client);
|
||||
if (dns_server >= 0) close(dns_server);
|
||||
return err;
|
||||
}
|
||||
|
||||
int main(int argc, char **argv)
|
||||
{
|
||||
struct options options = { .port = 443, .dns_port = 53 };
|
||||
struct dns_egress_bpf *skel = NULL;
|
||||
struct bpf_link *query_link = NULL, *ingress_link = NULL;
|
||||
struct bpf_link *connect_link = NULL;
|
||||
struct ring_buffer *ring = NULL;
|
||||
struct in_addr dns_server = {};
|
||||
unsigned char qname[DNS_QNAME_MAX] = {};
|
||||
unsigned long long deadline = 0;
|
||||
unsigned int qname_length = 0;
|
||||
int cgroup_fd = -1;
|
||||
int err = 1;
|
||||
|
||||
setvbuf(stdout, NULL, _IONBF, 0);
|
||||
if (parse_options(argc, argv, &options) ||
|
||||
encode_qname(options.domain, qname, &qname_length) ||
|
||||
inet_pton(AF_INET, options.dns_server, &dns_server) != 1) {
|
||||
usage(argv[0]);
|
||||
return 2;
|
||||
}
|
||||
cgroup_fd = open(options.cgroup_path,
|
||||
O_RDONLY | O_DIRECTORY | O_CLOEXEC);
|
||||
if (cgroup_fd < 0) {
|
||||
fprintf(stderr, "failed to open cgroup %s: %s\n",
|
||||
options.cgroup_path, strerror(errno));
|
||||
goto cleanup;
|
||||
}
|
||||
|
||||
skel = dns_egress_bpf__open();
|
||||
if (!skel)
|
||||
goto cleanup;
|
||||
skel->rodata->target_tgid = options.demo ? getpid() : 0;
|
||||
skel->rodata->dns_server_ip = dns_server.s_addr;
|
||||
skel->rodata->dns_server_port = options.dns_port;
|
||||
skel->rodata->protected_tcp_port = options.port;
|
||||
skel->rodata->configured_qname_length = qname_length;
|
||||
memcpy((void *)skel->rodata->configured_qname, qname, qname_length);
|
||||
if (dns_egress_bpf__load(skel)) {
|
||||
fprintf(stderr, "failed to load DNS egress BPF programs\n");
|
||||
goto cleanup;
|
||||
}
|
||||
query_link = bpf_program__attach_cgroup(skel->progs.record_dns_query,
|
||||
cgroup_fd);
|
||||
ingress_link = bpf_program__attach_cgroup(skel->progs.learn_dns_answer,
|
||||
cgroup_fd);
|
||||
connect_link = bpf_program__attach_cgroup(skel->progs.enforce_dns_policy,
|
||||
cgroup_fd);
|
||||
if (libbpf_get_error(query_link) || libbpf_get_error(ingress_link) ||
|
||||
libbpf_get_error(connect_link)) {
|
||||
fprintf(stderr, "failed to attach programs to cgroup %s\n",
|
||||
options.cgroup_path);
|
||||
query_link = libbpf_get_error(query_link) ? NULL : query_link;
|
||||
ingress_link = libbpf_get_error(ingress_link) ? NULL : ingress_link;
|
||||
connect_link = libbpf_get_error(connect_link) ? NULL : connect_link;
|
||||
goto cleanup;
|
||||
}
|
||||
ring = ring_buffer__new(bpf_map__fd(skel->maps.events), handle_event,
|
||||
NULL, NULL);
|
||||
if (!ring)
|
||||
goto cleanup;
|
||||
|
||||
printf("dns-egress attached cgroup=%s domain=%s resolver=%s tcp_port=%u dns_port=%u\n",
|
||||
options.cgroup_path, options.domain, options.dns_server,
|
||||
options.port, options.dns_port);
|
||||
if (options.demo) {
|
||||
if (run_demo(ring, &options, qname, qname_length))
|
||||
goto cleanup;
|
||||
} else {
|
||||
signal(SIGINT, handle_signal);
|
||||
signal(SIGTERM, handle_signal);
|
||||
if (options.duration_seconds)
|
||||
deadline = monotonic_ns() +
|
||||
(unsigned long long)options.duration_seconds *
|
||||
1000000000ULL;
|
||||
while (!stop && (!deadline || monotonic_ns() < deadline)) {
|
||||
int poll_result = ring_buffer__poll(ring, 100);
|
||||
|
||||
if (poll_result < 0 && poll_result != -EINTR) {
|
||||
fprintf(stderr, "ring buffer poll failed: %d\n",
|
||||
poll_result);
|
||||
goto cleanup;
|
||||
}
|
||||
}
|
||||
}
|
||||
err = 0;
|
||||
|
||||
cleanup:
|
||||
ring_buffer__free(ring);
|
||||
bpf_link__destroy(connect_link);
|
||||
bpf_link__destroy(ingress_link);
|
||||
bpf_link__destroy(query_link);
|
||||
if (cgroup_fd >= 0) close(cgroup_fd);
|
||||
dns_egress_bpf__destroy(skel);
|
||||
return err;
|
||||
}
|
||||
```
|
||||
|
||||
The user space program loads and attaches the BPF programs to a cgroup. It encodes the domain name in DNS label format, configures the BPF programs with the target resolver and protected port, and polls the ring buffer for policy events.
|
||||
|
||||
The demo mode runs a self-contained test: it creates local UDP and TCP sockets, simulates DNS resolution, and verifies the policy works correctly. It tests connection blocking before DNS, rejection of unsolicited responses, rejection of wrong transaction IDs, connection allowing after valid DNS, and connection blocking after TTL expiry.
|
||||
|
||||
## Compilation and Execution
|
||||
|
||||
Build the example:
|
||||
|
||||
```bash
|
||||
cd src/55-dns-egress
|
||||
make
|
||||
```
|
||||
|
||||
Run with the required parameters:
|
||||
|
||||
```bash
|
||||
sudo ./dns_egress --cgroup /sys/fs/cgroup/my-service --domain api.example.com --dns-server 127.0.0.53 --port 443
|
||||
```
|
||||
|
||||
Or run the built-in demo:
|
||||
|
||||
```bash
|
||||
sudo ./dns_egress --demo
|
||||
```
|
||||
|
||||
Example demo output:
|
||||
|
||||
```text
|
||||
dns-egress attached cgroup=/sys/fs/cgroup domain=lab.test resolver=127.0.0.1 tcp_port=19090 dns_port=15353
|
||||
event=denied pid=1246 ip=127.0.0.1 ttl=0
|
||||
demo step=before-dns result=blocked
|
||||
event=denied pid=1246 ip=127.0.0.1 ttl=0
|
||||
demo step=unsolicited-response result=blocked
|
||||
event=denied pid=1246 ip=127.0.0.1 ttl=0
|
||||
demo step=wrong-transaction-id result=blocked
|
||||
event=learned pid=1246 ip=127.0.0.1 ttl=1
|
||||
event=allowed pid=1246 ip=127.0.0.1 ttl=1
|
||||
demo step=live-answer result=allowed
|
||||
event=expired pid=1246 ip=127.0.0.1 ttl=1
|
||||
event=denied pid=1246 ip=127.0.0.1 ttl=1
|
||||
demo step=expired-answer result=blocked
|
||||
```
|
||||
|
||||
## Requirements
|
||||
|
||||
| Requirement | Details |
|
||||
|-------------|---------|
|
||||
| Kernel | Linux 5.8+ (BPF ring buffer) |
|
||||
| Config | `CONFIG_BPF_SYSCALL`, `CONFIG_CGROUP_BPF`, `CONFIG_DEBUG_INFO_BTF` |
|
||||
| Privileges | Root |
|
||||
| cgroup | cgroup v2 mounted |
|
||||
|
||||
## Scope and Limitations
|
||||
|
||||
This implementation protects one configured TCP destination port and leaves other ports unchanged. It supports one exact query name configured at startup, IPv4 UDP DNS only, and parses direct first A record answers. Pending queries expire after 5 seconds. Allowed IPs use a 1024-entry LRU hash with DNS TTL. It does not handle CNAME chains, multiple answer layouts, TCP DNS, IPv6, DNS-over-HTTPS, or DNS-over-TLS.
|
||||
|
||||
## Summary
|
||||
|
||||
By correlating each DNS response with a recent query and carrying its TTL into connect-time policy, this example turns one domain name into a short-lived kernel allowlist. The three cgroup hooks separate DNS observation from connection enforcement, while ring buffer events make every decision visible in user space.
|
||||
|
||||
> If you'd like to dive deeper into eBPF, check out our tutorial repository at <https://github.com/eunomia-bpf/bpf-developer-tutorial> or visit our website at <https://eunomia.dev/tutorials/>.
|
||||
|
||||
## References
|
||||
|
||||
- [BPF ring buffer documentation](https://docs.kernel.org/6.6/bpf/ringbuf.html)
|
||||
- [BPF hash map documentation](https://docs.kernel.org/bpf/map_hash.html)
|
||||
- [cgroup BPF commit](https://github.com/torvalds/linux/commit/d74bad4e74ee)
|
||||
931
src/55-dns-egress/README.zh.md
Normal file
931
src/55-dns-egress/README.zh.md
Normal file
@@ -0,0 +1,931 @@
|
||||
# eBPF 实战教程:DNS 感知的出口策略执行
|
||||
|
||||
如何让一个受保护端口只连接到应用程序刚通过 DNS 解析的 IP 地址?本教程构建一个基于 cgroup 的出口过滤器,从 DNS 响应中学习 IP 地址,在 DNS TTL 有效期间允许匹配的 TCP 连接,并拒绝这个端口上的其他目的地址,策略在 connect 时由内核执行。
|
||||
|
||||
> 完整源代码:<https://github.com/eunomia-bpf/bpf-developer-tutorial/tree/main/src/55-dns-egress>
|
||||
|
||||
## eBPF 与 cgroup 钩子
|
||||
|
||||
eBPF 让经过验证器检查的程序运行在 Linux 内核 hook 上,并把选出的状态发送到用户态。本教程使用 cgroup BPF hook 拦截一个 cgroup 的网络报文和连接请求,`cgroup_skb/egress` 与 `cgroup_skb/ingress` 观察 DNS 流量,`cgroup/connect4` 拦截 IPv4 connect 调用。LRU 哈希映射让查询和地址条目保持在固定容量内,Linux 5.8 引入的 BPF ring buffer 则把策略事件交给用户态,因此本例需要 Linux 5.8 或更高版本。
|
||||
|
||||
## 实现原理
|
||||
|
||||
该工具将三个 BPF 程序附加到 cgroup。出口程序把事务 ID、客户端端口和服务器 IP 保存为待处理查询,用于记录出站 DNS 请求,5 秒内没有匹配响应的查询会过期。入口程序验证响应与待处理查询的关联,再提取 A 记录 IP 和 TTL。对于发往受保护端口的 TCP 连接,connect hook 检查目标 IP 是否在允许列表中并且仍然有效,其他目的端口保持原有行为。
|
||||
|
||||
## 头文件
|
||||
|
||||
```c
|
||||
/* SPDX-License-Identifier: (LGPL-2.1 OR BSD-2-Clause) */
|
||||
#ifndef __DNS_EGRESS_H
|
||||
#define __DNS_EGRESS_H
|
||||
|
||||
#define DNS_QNAME_MAX 64
|
||||
|
||||
enum dns_egress_event_type {
|
||||
DNS_LEARNED = 1,
|
||||
DNS_ALLOWED = 2,
|
||||
DNS_DENIED = 3,
|
||||
DNS_EXPIRED = 4,
|
||||
};
|
||||
|
||||
struct dns_header {
|
||||
unsigned short id;
|
||||
unsigned short flags;
|
||||
unsigned short questions;
|
||||
unsigned short answers;
|
||||
unsigned short authorities;
|
||||
unsigned short additionals;
|
||||
} __attribute__((packed));
|
||||
|
||||
struct dns_question {
|
||||
unsigned short type;
|
||||
unsigned short class;
|
||||
} __attribute__((packed));
|
||||
|
||||
struct dns_a_answer {
|
||||
unsigned short name;
|
||||
unsigned short type;
|
||||
unsigned short class;
|
||||
unsigned int ttl;
|
||||
unsigned short address_length;
|
||||
unsigned int address;
|
||||
} __attribute__((packed));
|
||||
|
||||
struct dns_egress_event {
|
||||
unsigned long long timestamp_ns;
|
||||
unsigned long long expires_ns;
|
||||
unsigned int type;
|
||||
unsigned int pid;
|
||||
unsigned int ip4;
|
||||
unsigned int ttl_seconds;
|
||||
};
|
||||
|
||||
#endif /* __DNS_EGRESS_H */
|
||||
```
|
||||
|
||||
头文件定义了用于解析数据包的 DNS 协议结构。`dns_header` 结构匹配 DNS 消息头格式。`dns_question` 和 `dns_a_answer` 结构解析查询和应答部分。`dns_egress_event` 结构通过 ring buffer 将策略事件(learned、allowed、denied、expired)传递给用户空间。
|
||||
|
||||
## BPF 程序
|
||||
|
||||
```c
|
||||
// SPDX-License-Identifier: GPL-2.0
|
||||
#include "vmlinux.h"
|
||||
#include <bpf/bpf_helpers.h>
|
||||
#include <bpf/bpf_endian.h>
|
||||
#include "dns_egress.h"
|
||||
|
||||
char LICENSE[] SEC("license") = "GPL";
|
||||
|
||||
#define IPPROTO_UDP 17
|
||||
#define IPPROTO_TCP 6
|
||||
#define IP_MF 0x2000
|
||||
#define IP_OFFSET 0x1fff
|
||||
#define DNS_QUERY_LIFETIME_NS (5ULL * 1000000000ULL)
|
||||
|
||||
const volatile __u32 target_tgid;
|
||||
const volatile __u32 dns_server_ip;
|
||||
const volatile __u16 dns_server_port = 53;
|
||||
const volatile __u16 protected_tcp_port = 443;
|
||||
const volatile __u32 configured_qname_length;
|
||||
const volatile unsigned char configured_qname[DNS_QNAME_MAX];
|
||||
|
||||
struct dns_state {
|
||||
__u64 expires_ns;
|
||||
__u32 ttl_seconds;
|
||||
__u32 pad;
|
||||
__u64 expired_reported;
|
||||
};
|
||||
|
||||
struct dns_query_key {
|
||||
__u32 server_ip;
|
||||
__u32 client_ip;
|
||||
__u16 client_port;
|
||||
__u16 transaction_id;
|
||||
};
|
||||
|
||||
struct dns_query_state {
|
||||
__u64 expires_ns;
|
||||
};
|
||||
|
||||
struct {
|
||||
__uint(type, BPF_MAP_TYPE_LRU_HASH);
|
||||
__uint(max_entries, 1024);
|
||||
__type(key, struct dns_query_key);
|
||||
__type(value, struct dns_query_state);
|
||||
} pending_queries SEC(".maps");
|
||||
|
||||
struct {
|
||||
__uint(type, BPF_MAP_TYPE_LRU_HASH);
|
||||
__uint(max_entries, 1024);
|
||||
__type(key, __u32);
|
||||
__type(value, struct dns_state);
|
||||
} allowed_ips SEC(".maps");
|
||||
|
||||
struct {
|
||||
__uint(type, BPF_MAP_TYPE_RINGBUF);
|
||||
__uint(max_entries, 256 * 1024);
|
||||
} events SEC(".maps");
|
||||
|
||||
static __always_inline void emit_event(__u32 type, __u32 ip4,
|
||||
__u32 ttl_seconds, __u64 expires_ns)
|
||||
{
|
||||
struct dns_egress_event *event;
|
||||
|
||||
event = bpf_ringbuf_reserve(&events, sizeof(*event), 0);
|
||||
if (!event)
|
||||
return;
|
||||
event->timestamp_ns = bpf_ktime_get_ns();
|
||||
event->expires_ns = expires_ns;
|
||||
event->type = type;
|
||||
event->pid = bpf_get_current_pid_tgid() >> 32;
|
||||
event->ip4 = ip4;
|
||||
event->ttl_seconds = ttl_seconds;
|
||||
bpf_ringbuf_submit(event, 0);
|
||||
}
|
||||
|
||||
static __noinline bool matches_qname(struct __sk_buff *skb, __u32 offset)
|
||||
{
|
||||
unsigned char byte;
|
||||
|
||||
if (!configured_qname_length || configured_qname_length > DNS_QNAME_MAX)
|
||||
return false;
|
||||
#pragma clang loop unroll(disable)
|
||||
for (int i = 0; i < DNS_QNAME_MAX; i++) {
|
||||
if (i >= configured_qname_length)
|
||||
break;
|
||||
if (bpf_skb_load_bytes(skb, offset + i, &byte, sizeof(byte)) ||
|
||||
byte != configured_qname[i])
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
SEC("cgroup_skb/egress")
|
||||
int record_dns_query(struct __sk_buff *skb)
|
||||
{
|
||||
struct dns_query_state state = {
|
||||
.expires_ns = bpf_ktime_get_ns() + DNS_QUERY_LIFETIME_NS,
|
||||
};
|
||||
struct dns_query_key key = {};
|
||||
struct dns_question question;
|
||||
struct dns_header header;
|
||||
struct udphdr udp;
|
||||
struct iphdr ip;
|
||||
__u32 ip_header_len;
|
||||
__u32 dns_offset;
|
||||
__u16 flags;
|
||||
|
||||
if (bpf_skb_load_bytes(skb, 0, &ip, sizeof(ip)))
|
||||
return 1;
|
||||
if (ip.version != 4 || ip.protocol != IPPROTO_UDP || ip.ihl < 5 ||
|
||||
(bpf_ntohs(ip.frag_off) & (IP_MF | IP_OFFSET)) ||
|
||||
ip.daddr != dns_server_ip)
|
||||
return 1;
|
||||
ip_header_len = ip.ihl * 4;
|
||||
if (bpf_skb_load_bytes(skb, ip_header_len, &udp, sizeof(udp)) ||
|
||||
bpf_ntohs(udp.dest) != dns_server_port)
|
||||
return 1;
|
||||
dns_offset = ip_header_len + sizeof(udp);
|
||||
if (bpf_skb_load_bytes(skb, dns_offset, &header, sizeof(header)))
|
||||
return 1;
|
||||
flags = bpf_ntohs(header.flags);
|
||||
if ((flags & 0xf800) || bpf_ntohs(header.questions) != 1)
|
||||
return 1;
|
||||
if (!matches_qname(skb, dns_offset + sizeof(header)))
|
||||
return 1;
|
||||
dns_offset += sizeof(header) + configured_qname_length;
|
||||
if (bpf_skb_load_bytes(skb, dns_offset, &question, sizeof(question)) ||
|
||||
bpf_ntohs(question.type) != 1 || bpf_ntohs(question.class) != 1)
|
||||
return 1;
|
||||
|
||||
key.server_ip = ip.daddr;
|
||||
key.client_ip = ip.saddr;
|
||||
key.client_port = udp.source;
|
||||
key.transaction_id = header.id;
|
||||
bpf_map_update_elem(&pending_queries, &key, &state, BPF_ANY);
|
||||
return 1;
|
||||
}
|
||||
|
||||
SEC("cgroup_skb/ingress")
|
||||
int learn_dns_answer(struct __sk_buff *skb)
|
||||
{
|
||||
struct dns_a_answer answer;
|
||||
struct dns_question question;
|
||||
struct dns_header header;
|
||||
struct dns_query_key query_key = {};
|
||||
struct dns_query_state *query;
|
||||
struct dns_state state = {};
|
||||
struct udphdr udp;
|
||||
struct iphdr ip;
|
||||
__u64 ttl_ns, expires;
|
||||
__u32 ip_header_len;
|
||||
__u32 dns_offset;
|
||||
__u32 key;
|
||||
__u32 ttl;
|
||||
|
||||
if (bpf_skb_load_bytes(skb, 0, &ip, sizeof(ip)))
|
||||
return 1;
|
||||
if (ip.version != 4 || ip.protocol != IPPROTO_UDP || ip.ihl < 5 ||
|
||||
(bpf_ntohs(ip.frag_off) & (IP_MF | IP_OFFSET)) ||
|
||||
ip.saddr != dns_server_ip)
|
||||
return 1;
|
||||
ip_header_len = ip.ihl * 4;
|
||||
if (bpf_skb_load_bytes(skb, ip_header_len, &udp, sizeof(udp)) ||
|
||||
bpf_ntohs(udp.source) != dns_server_port)
|
||||
return 1;
|
||||
dns_offset = ip_header_len + sizeof(udp);
|
||||
if (bpf_skb_load_bytes(skb, dns_offset, &header, sizeof(header)))
|
||||
return 1;
|
||||
query_key.server_ip = ip.saddr;
|
||||
query_key.client_ip = ip.daddr;
|
||||
query_key.client_port = udp.dest;
|
||||
query_key.transaction_id = header.id;
|
||||
query = bpf_map_lookup_elem(&pending_queries, &query_key);
|
||||
if (!query)
|
||||
return 1;
|
||||
if (bpf_ktime_get_ns() >= query->expires_ns) {
|
||||
bpf_map_delete_elem(&pending_queries, &query_key);
|
||||
return 1;
|
||||
}
|
||||
if ((bpf_ntohs(header.flags) & 0xf80f) != 0x8000 ||
|
||||
bpf_ntohs(header.questions) != 1 || !bpf_ntohs(header.answers))
|
||||
return 1;
|
||||
if (!matches_qname(skb, dns_offset + sizeof(header)))
|
||||
return 1;
|
||||
dns_offset += sizeof(header) + configured_qname_length;
|
||||
if (bpf_skb_load_bytes(skb, dns_offset, &question, sizeof(question)) ||
|
||||
bpf_ntohs(question.type) != 1 || bpf_ntohs(question.class) != 1)
|
||||
return 1;
|
||||
dns_offset += sizeof(question);
|
||||
if (bpf_skb_load_bytes(skb, dns_offset, &answer, sizeof(answer)) ||
|
||||
bpf_ntohs(answer.name) != 0xc00c || bpf_ntohs(answer.type) != 1 ||
|
||||
bpf_ntohs(answer.class) != 1 ||
|
||||
bpf_ntohs(answer.address_length) != 4)
|
||||
return 1;
|
||||
bpf_map_delete_elem(&pending_queries, &query_key);
|
||||
|
||||
key = answer.address;
|
||||
ttl = bpf_ntohl(answer.ttl);
|
||||
if (!ttl || ttl > 86400)
|
||||
return 1;
|
||||
ttl_ns = (__u64)ttl * 1000000000ULL;
|
||||
expires = bpf_ktime_get_ns() + ttl_ns;
|
||||
state.expires_ns = expires;
|
||||
state.ttl_seconds = ttl;
|
||||
if (bpf_map_update_elem(&allowed_ips, &key, &state, BPF_ANY))
|
||||
return 1;
|
||||
emit_event(DNS_LEARNED, key, ttl, expires);
|
||||
return 1;
|
||||
}
|
||||
|
||||
SEC("cgroup/connect4")
|
||||
int enforce_dns_policy(struct bpf_sock_addr *ctx)
|
||||
{
|
||||
struct dns_state *state;
|
||||
__u64 expires = 0;
|
||||
__u32 ip4;
|
||||
__u32 ttl = 0;
|
||||
|
||||
if ((target_tgid &&
|
||||
(__u32)(bpf_get_current_pid_tgid() >> 32) != target_tgid) ||
|
||||
ctx->protocol != IPPROTO_TCP ||
|
||||
bpf_ntohs((__u16)ctx->user_port) != protected_tcp_port)
|
||||
return 1;
|
||||
|
||||
ip4 = ctx->user_ip4;
|
||||
state = bpf_map_lookup_elem(&allowed_ips, &ip4);
|
||||
if (state) {
|
||||
expires = state->expires_ns;
|
||||
ttl = state->ttl_seconds;
|
||||
if (bpf_ktime_get_ns() < expires) {
|
||||
emit_event(DNS_ALLOWED, ip4, ttl, expires);
|
||||
return 1;
|
||||
}
|
||||
if (__sync_val_compare_and_swap(&state->expired_reported, 0, 1) == 0)
|
||||
emit_event(DNS_EXPIRED, ip4, ttl, expires);
|
||||
}
|
||||
emit_event(DNS_DENIED, ip4, ttl, expires);
|
||||
return 0;
|
||||
}
|
||||
```
|
||||
|
||||
BPF 程序使用三个映射。`pending_queries` LRU 哈希使用服务器 IP、客户端 IP、客户端端口和事务 ID 的组合键跟踪出站 DNS 查询。`allowed_ips` LRU 哈希存储学习到的 IP 及其过期时间戳。`events` ring buffer 将策略决策发送到用户空间。
|
||||
|
||||
`record_dns_query` 函数在 cgroup 出口运行,捕获发往配置的解析器的 DNS 查询。它验证数据包是发往正确服务器和端口的 IPv4 UDP,检查查询名称是否匹配,并存储带有 5 秒过期时间的查询。
|
||||
|
||||
`learn_dns_answer` 函数在 cgroup 入口运行,解析 DNS 响应。它查找匹配的待处理查询,验证响应格式,从第一个 A 记录提取 IP 地址和 TTL,并将 IP 存储到允许列表中,设置基于 TTL 的过期时间。
|
||||
|
||||
`enforce_dns_policy` 函数在进程调用 `connect()` 时运行,发往受保护端口的 IPv4 TCP 连接会查询允许列表和过期时间。命中的条目返回 1 继续连接,缺失或过期的条目返回 0,让 `connect()` 以 `EPERM` 失败,其他端口则直接返回 1。
|
||||
|
||||
## 用户空间程序
|
||||
|
||||
```c
|
||||
// SPDX-License-Identifier: (LGPL-2.1 OR BSD-2-Clause)
|
||||
#include <arpa/inet.h>
|
||||
#include <errno.h>
|
||||
#include <fcntl.h>
|
||||
#include <getopt.h>
|
||||
#include <netinet/in.h>
|
||||
#include <signal.h>
|
||||
#include <stdbool.h>
|
||||
#include <stddef.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <sys/socket.h>
|
||||
#include <time.h>
|
||||
#include <unistd.h>
|
||||
#include <bpf/libbpf.h>
|
||||
#include "dns_egress.h"
|
||||
#include "dns_egress.skel.h"
|
||||
|
||||
#define DEMO_DNS_PORT 15353
|
||||
#define DEMO_TCP_PORT 19090
|
||||
#define DNS_ID 0x4b1d
|
||||
|
||||
struct options {
|
||||
const char *cgroup_path;
|
||||
const char *domain;
|
||||
const char *dns_server;
|
||||
unsigned int port;
|
||||
unsigned int dns_port;
|
||||
unsigned int duration_seconds;
|
||||
bool demo;
|
||||
};
|
||||
|
||||
static int event_counts[5];
|
||||
static volatile sig_atomic_t stop;
|
||||
|
||||
static void handle_signal(int signal_number)
|
||||
{
|
||||
(void)signal_number;
|
||||
stop = 1;
|
||||
}
|
||||
|
||||
static unsigned long long monotonic_ns(void)
|
||||
{
|
||||
struct timespec now;
|
||||
|
||||
clock_gettime(CLOCK_MONOTONIC, &now);
|
||||
return (unsigned long long)now.tv_sec * 1000000000ULL + now.tv_nsec;
|
||||
}
|
||||
|
||||
static int handle_event(void *ctx, void *data, size_t size)
|
||||
{
|
||||
const struct dns_egress_event *event = data;
|
||||
char address[INET_ADDRSTRLEN];
|
||||
const char *name;
|
||||
|
||||
(void)ctx;
|
||||
if (size != sizeof(*event) || event->type > DNS_EXPIRED)
|
||||
return 0;
|
||||
event_counts[event->type]++;
|
||||
inet_ntop(AF_INET, &event->ip4, address, sizeof(address));
|
||||
name = event->type == DNS_LEARNED ? "learned" :
|
||||
event->type == DNS_ALLOWED ? "allowed" :
|
||||
event->type == DNS_DENIED ? "denied" : "expired";
|
||||
printf("event=%s pid=%u ip=%s ttl=%u\n", name, event->pid, address,
|
||||
event->ttl_seconds);
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int parse_uint(const char *text, unsigned int maximum,
|
||||
unsigned int *value)
|
||||
{
|
||||
char *end = NULL;
|
||||
unsigned long parsed;
|
||||
|
||||
errno = 0;
|
||||
parsed = strtoul(text, &end, 10);
|
||||
if (errno || !*text || *end || parsed > maximum)
|
||||
return -1;
|
||||
*value = parsed;
|
||||
return 0;
|
||||
}
|
||||
|
||||
static void usage(const char *program)
|
||||
{
|
||||
printf("Usage: %s --cgroup PATH --domain NAME --dns-server IPV4 [--port PORT] [--dns-port PORT] [--duration SEC]\n"
|
||||
" %s --demo\n", program, program);
|
||||
}
|
||||
|
||||
static int parse_options(int argc, char **argv, struct options *options)
|
||||
{
|
||||
static const struct option long_options[] = {
|
||||
{ "cgroup", required_argument, NULL, 'c' },
|
||||
{ "domain", required_argument, NULL, 'n' },
|
||||
{ "dns-server", required_argument, NULL, 'r' },
|
||||
{ "port", required_argument, NULL, 'p' },
|
||||
{ "dns-port", required_argument, NULL, 's' },
|
||||
{ "duration", required_argument, NULL, 'd' },
|
||||
{ "demo", no_argument, NULL, 'D' },
|
||||
{ "help", no_argument, NULL, 'h' },
|
||||
{},
|
||||
};
|
||||
int option;
|
||||
|
||||
while ((option = getopt_long(argc, argv, "c:n:r:p:s:d:Dh", long_options,
|
||||
NULL)) != -1) {
|
||||
switch (option) {
|
||||
case 'c': options->cgroup_path = optarg; break;
|
||||
case 'n': options->domain = optarg; break;
|
||||
case 'r': options->dns_server = optarg; break;
|
||||
case 'p':
|
||||
if (parse_uint(optarg, 65535, &options->port) ||
|
||||
!options->port)
|
||||
return -1;
|
||||
break;
|
||||
case 's':
|
||||
if (parse_uint(optarg, 65535, &options->dns_port) ||
|
||||
!options->dns_port)
|
||||
return -1;
|
||||
break;
|
||||
case 'd':
|
||||
if (parse_uint(optarg, 86400,
|
||||
&options->duration_seconds))
|
||||
return -1;
|
||||
break;
|
||||
case 'D': options->demo = true; break;
|
||||
case 'h': usage(argv[0]); exit(0);
|
||||
default: return -1;
|
||||
}
|
||||
}
|
||||
if (optind != argc)
|
||||
return -1;
|
||||
if (options->demo) {
|
||||
if (options->cgroup_path || options->domain || options->dns_server)
|
||||
return -1;
|
||||
options->cgroup_path = "/sys/fs/cgroup";
|
||||
options->domain = "lab.test";
|
||||
options->dns_server = "127.0.0.1";
|
||||
options->port = DEMO_TCP_PORT;
|
||||
options->dns_port = DEMO_DNS_PORT;
|
||||
return 0;
|
||||
}
|
||||
return options->cgroup_path && options->domain && options->dns_server ?
|
||||
0 : -1;
|
||||
}
|
||||
|
||||
static int encode_qname(const char *domain, unsigned char output[DNS_QNAME_MAX],
|
||||
unsigned int *output_length)
|
||||
{
|
||||
const char *label = domain;
|
||||
unsigned int used = 0;
|
||||
|
||||
if (!*domain)
|
||||
return -1;
|
||||
while (*label) {
|
||||
const char *dot = strchr(label, '.');
|
||||
size_t length = dot ? (size_t)(dot - label) : strlen(label);
|
||||
|
||||
if (!length || length > 63 || used + length + 2 > DNS_QNAME_MAX)
|
||||
return -1;
|
||||
output[used++] = length;
|
||||
memcpy(output + used, label, length);
|
||||
used += length;
|
||||
if (!dot)
|
||||
break;
|
||||
label = dot + 1;
|
||||
if (!*label)
|
||||
break;
|
||||
}
|
||||
output[used++] = 0;
|
||||
*output_length = used;
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int bind_udp(struct sockaddr_in *address)
|
||||
{
|
||||
socklen_t length = sizeof(*address);
|
||||
int fd;
|
||||
|
||||
fd = socket(AF_INET, SOCK_DGRAM | SOCK_CLOEXEC, 0);
|
||||
if (fd < 0)
|
||||
return -1;
|
||||
if (bind(fd, (struct sockaddr *)address, sizeof(*address)) ||
|
||||
getsockname(fd, (struct sockaddr *)address, &length)) {
|
||||
close(fd);
|
||||
return -1;
|
||||
}
|
||||
return fd;
|
||||
}
|
||||
|
||||
static int begin_dns_exchange(int server, int client,
|
||||
struct sockaddr_in *server_address,
|
||||
const unsigned char *qname,
|
||||
unsigned int qname_length,
|
||||
unsigned char message[512],
|
||||
size_t *query_length,
|
||||
struct sockaddr_in *client_address)
|
||||
{
|
||||
struct dns_question question = {
|
||||
.type = htons(1),
|
||||
.class = htons(1),
|
||||
};
|
||||
struct dns_header *header = (void *)message;
|
||||
socklen_t address_length = sizeof(*client_address);
|
||||
ssize_t received;
|
||||
|
||||
memset(message, 0, 512);
|
||||
header->id = htons(DNS_ID);
|
||||
header->flags = htons(0x0100);
|
||||
header->questions = htons(1);
|
||||
memcpy(message + sizeof(*header), qname, qname_length);
|
||||
memcpy(message + sizeof(*header) + qname_length, &question,
|
||||
sizeof(question));
|
||||
*query_length = sizeof(*header) + qname_length + sizeof(question);
|
||||
if (sendto(client, message, *query_length, 0,
|
||||
(struct sockaddr *)server_address, sizeof(*server_address)) !=
|
||||
(ssize_t)*query_length)
|
||||
return -1;
|
||||
received = recvfrom(server, message, 512, 0,
|
||||
(struct sockaddr *)client_address, &address_length);
|
||||
return received == (ssize_t)*query_length ? 0 : -1;
|
||||
}
|
||||
|
||||
static int send_dns_answer(int server, int client,
|
||||
struct sockaddr_in *client_address,
|
||||
unsigned char message[512], size_t query_length,
|
||||
unsigned short transaction_id,
|
||||
unsigned int ttl_seconds)
|
||||
{
|
||||
struct dns_a_answer answer = {
|
||||
.name = htons(0xc00c),
|
||||
.type = htons(1),
|
||||
.class = htons(1),
|
||||
.ttl = htonl(ttl_seconds),
|
||||
.address_length = htons(4),
|
||||
};
|
||||
struct dns_header *header = (void *)message;
|
||||
|
||||
if (query_length + sizeof(answer) > 512)
|
||||
return -1;
|
||||
header->id = htons(transaction_id);
|
||||
header->flags = htons(0x8180);
|
||||
header->answers = htons(1);
|
||||
inet_pton(AF_INET, "127.0.0.1", &answer.address);
|
||||
memcpy(message + query_length, &answer, sizeof(answer));
|
||||
if (sendto(server, message, query_length + sizeof(answer), 0,
|
||||
(struct sockaddr *)client_address, sizeof(*client_address)) !=
|
||||
(ssize_t)(query_length + sizeof(answer)))
|
||||
return -1;
|
||||
return recv(client, message, 512, 0) ==
|
||||
(ssize_t)(query_length + sizeof(answer)) ? 0 : -1;
|
||||
}
|
||||
|
||||
static int send_unsolicited_dns(int server, int client,
|
||||
const unsigned char *qname,
|
||||
unsigned int qname_length)
|
||||
{
|
||||
unsigned char message[512] = {};
|
||||
struct sockaddr_in client_address;
|
||||
struct dns_a_answer answer = {
|
||||
.name = htons(0xc00c),
|
||||
.type = htons(1),
|
||||
.class = htons(1),
|
||||
.ttl = htonl(30),
|
||||
.address_length = htons(4),
|
||||
};
|
||||
struct dns_question question = {
|
||||
.type = htons(1),
|
||||
.class = htons(1),
|
||||
};
|
||||
struct dns_header *header = (void *)message;
|
||||
socklen_t address_length = sizeof(client_address);
|
||||
size_t message_length;
|
||||
|
||||
if (getsockname(client, (struct sockaddr *)&client_address,
|
||||
&address_length))
|
||||
return -1;
|
||||
header->id = htons(DNS_ID + 1);
|
||||
header->flags = htons(0x8180);
|
||||
header->questions = htons(1);
|
||||
header->answers = htons(1);
|
||||
memcpy(message + sizeof(*header), qname, qname_length);
|
||||
memcpy(message + sizeof(*header) + qname_length, &question,
|
||||
sizeof(question));
|
||||
message_length = sizeof(*header) + qname_length + sizeof(question);
|
||||
inet_pton(AF_INET, "127.0.0.1", &answer.address);
|
||||
memcpy(message + message_length, &answer, sizeof(answer));
|
||||
message_length += sizeof(answer);
|
||||
if (sendto(server, message, message_length, 0,
|
||||
(struct sockaddr *)&client_address, address_length) !=
|
||||
(ssize_t)message_length)
|
||||
return -1;
|
||||
return recv(client, message, sizeof(message), 0) ==
|
||||
(ssize_t)message_length ? 0 : -1;
|
||||
}
|
||||
|
||||
static int create_tcp_listener(unsigned int port)
|
||||
{
|
||||
struct sockaddr_in address = {
|
||||
.sin_family = AF_INET,
|
||||
.sin_addr.s_addr = htonl(INADDR_LOOPBACK),
|
||||
.sin_port = htons(port),
|
||||
};
|
||||
int one = 1;
|
||||
int fd;
|
||||
|
||||
fd = socket(AF_INET, SOCK_STREAM | SOCK_CLOEXEC, 0);
|
||||
if (fd < 0)
|
||||
return -1;
|
||||
setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &one, sizeof(one));
|
||||
if (bind(fd, (struct sockaddr *)&address, sizeof(address)) ||
|
||||
listen(fd, 4)) {
|
||||
close(fd);
|
||||
return -1;
|
||||
}
|
||||
return fd;
|
||||
}
|
||||
|
||||
static int connect_tcp(unsigned int port)
|
||||
{
|
||||
struct sockaddr_in address = {
|
||||
.sin_family = AF_INET,
|
||||
.sin_addr.s_addr = htonl(INADDR_LOOPBACK),
|
||||
.sin_port = htons(port),
|
||||
};
|
||||
int saved_errno;
|
||||
int fd;
|
||||
|
||||
fd = socket(AF_INET, SOCK_STREAM | SOCK_CLOEXEC, 0);
|
||||
if (fd < 0)
|
||||
return -1;
|
||||
if (!connect(fd, (struct sockaddr *)&address, sizeof(address)))
|
||||
return fd;
|
||||
saved_errno = errno;
|
||||
close(fd);
|
||||
errno = saved_errno;
|
||||
return -1;
|
||||
}
|
||||
|
||||
static int complete_tcp(int listener, int client)
|
||||
{
|
||||
int accepted = accept4(listener, NULL, NULL, SOCK_CLOEXEC);
|
||||
char byte = 'x';
|
||||
|
||||
if (accepted < 0)
|
||||
return -1;
|
||||
if (write(accepted, &byte, 1) != 1 || read(client, &byte, 1) != 1) {
|
||||
close(accepted);
|
||||
return -1;
|
||||
}
|
||||
close(accepted);
|
||||
close(client);
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int run_demo(struct ring_buffer *ring, const struct options *options,
|
||||
const unsigned char *qname, unsigned int qname_length)
|
||||
{
|
||||
struct sockaddr_in server_address = {
|
||||
.sin_family = AF_INET,
|
||||
.sin_addr.s_addr = htonl(INADDR_LOOPBACK),
|
||||
.sin_port = htons(options->dns_port),
|
||||
};
|
||||
struct sockaddr_in client_address = {
|
||||
.sin_family = AF_INET,
|
||||
.sin_addr.s_addr = htonl(INADDR_LOOPBACK),
|
||||
};
|
||||
struct timespec wait_time = { .tv_sec = 1, .tv_nsec = 300000000 };
|
||||
struct sockaddr_in response_client_address;
|
||||
unsigned char dns_message[512];
|
||||
size_t query_length;
|
||||
int dns_server = -1, dns_client = -1, listener = -1, client = -1;
|
||||
int err = -1;
|
||||
|
||||
dns_server = bind_udp(&server_address);
|
||||
dns_client = bind_udp(&client_address);
|
||||
listener = create_tcp_listener(options->port);
|
||||
if (dns_server < 0 || dns_client < 0 || listener < 0)
|
||||
goto cleanup;
|
||||
|
||||
errno = 0;
|
||||
client = connect_tcp(options->port);
|
||||
if (client >= 0 || errno != EPERM)
|
||||
goto cleanup;
|
||||
ring_buffer__poll(ring, 100);
|
||||
printf("demo step=before-dns result=blocked\n");
|
||||
|
||||
if (send_unsolicited_dns(dns_server, dns_client, qname, qname_length))
|
||||
goto cleanup;
|
||||
ring_buffer__poll(ring, 100);
|
||||
errno = 0;
|
||||
client = connect_tcp(options->port);
|
||||
if (client >= 0 || errno != EPERM)
|
||||
goto cleanup;
|
||||
ring_buffer__poll(ring, 100);
|
||||
printf("demo step=unsolicited-response result=blocked\n");
|
||||
|
||||
if (begin_dns_exchange(dns_server, dns_client, &server_address, qname,
|
||||
qname_length, dns_message, &query_length,
|
||||
&response_client_address) ||
|
||||
send_dns_answer(dns_server, dns_client, &response_client_address,
|
||||
dns_message, query_length, DNS_ID + 1, 30))
|
||||
goto cleanup;
|
||||
ring_buffer__poll(ring, 100);
|
||||
errno = 0;
|
||||
client = connect_tcp(options->port);
|
||||
if (client >= 0 || errno != EPERM)
|
||||
goto cleanup;
|
||||
ring_buffer__poll(ring, 100);
|
||||
printf("demo step=wrong-transaction-id result=blocked\n");
|
||||
|
||||
if (send_dns_answer(dns_server, dns_client, &response_client_address,
|
||||
dns_message, query_length, DNS_ID, 1))
|
||||
goto cleanup;
|
||||
ring_buffer__poll(ring, 100);
|
||||
client = connect_tcp(options->port);
|
||||
if (client < 0 || complete_tcp(listener, client))
|
||||
goto cleanup;
|
||||
client = -1;
|
||||
ring_buffer__poll(ring, 100);
|
||||
printf("demo step=live-answer result=allowed\n");
|
||||
|
||||
nanosleep(&wait_time, NULL);
|
||||
ring_buffer__poll(ring, 100);
|
||||
errno = 0;
|
||||
client = connect_tcp(options->port);
|
||||
if (client >= 0 || errno != EPERM)
|
||||
goto cleanup;
|
||||
ring_buffer__poll(ring, 100);
|
||||
printf("demo step=expired-answer result=blocked\n");
|
||||
|
||||
if (event_counts[DNS_LEARNED] != 1 || event_counts[DNS_ALLOWED] != 1 ||
|
||||
event_counts[DNS_DENIED] != 4 || event_counts[DNS_EXPIRED] != 1)
|
||||
goto cleanup;
|
||||
err = 0;
|
||||
|
||||
cleanup:
|
||||
if (client >= 0) close(client);
|
||||
if (listener >= 0) close(listener);
|
||||
if (dns_client >= 0) close(dns_client);
|
||||
if (dns_server >= 0) close(dns_server);
|
||||
return err;
|
||||
}
|
||||
|
||||
int main(int argc, char **argv)
|
||||
{
|
||||
struct options options = { .port = 443, .dns_port = 53 };
|
||||
struct dns_egress_bpf *skel = NULL;
|
||||
struct bpf_link *query_link = NULL, *ingress_link = NULL;
|
||||
struct bpf_link *connect_link = NULL;
|
||||
struct ring_buffer *ring = NULL;
|
||||
struct in_addr dns_server = {};
|
||||
unsigned char qname[DNS_QNAME_MAX] = {};
|
||||
unsigned long long deadline = 0;
|
||||
unsigned int qname_length = 0;
|
||||
int cgroup_fd = -1;
|
||||
int err = 1;
|
||||
|
||||
setvbuf(stdout, NULL, _IONBF, 0);
|
||||
if (parse_options(argc, argv, &options) ||
|
||||
encode_qname(options.domain, qname, &qname_length) ||
|
||||
inet_pton(AF_INET, options.dns_server, &dns_server) != 1) {
|
||||
usage(argv[0]);
|
||||
return 2;
|
||||
}
|
||||
cgroup_fd = open(options.cgroup_path,
|
||||
O_RDONLY | O_DIRECTORY | O_CLOEXEC);
|
||||
if (cgroup_fd < 0) {
|
||||
fprintf(stderr, "failed to open cgroup %s: %s\n",
|
||||
options.cgroup_path, strerror(errno));
|
||||
goto cleanup;
|
||||
}
|
||||
|
||||
skel = dns_egress_bpf__open();
|
||||
if (!skel)
|
||||
goto cleanup;
|
||||
skel->rodata->target_tgid = options.demo ? getpid() : 0;
|
||||
skel->rodata->dns_server_ip = dns_server.s_addr;
|
||||
skel->rodata->dns_server_port = options.dns_port;
|
||||
skel->rodata->protected_tcp_port = options.port;
|
||||
skel->rodata->configured_qname_length = qname_length;
|
||||
memcpy((void *)skel->rodata->configured_qname, qname, qname_length);
|
||||
if (dns_egress_bpf__load(skel)) {
|
||||
fprintf(stderr, "failed to load DNS egress BPF programs\n");
|
||||
goto cleanup;
|
||||
}
|
||||
query_link = bpf_program__attach_cgroup(skel->progs.record_dns_query,
|
||||
cgroup_fd);
|
||||
ingress_link = bpf_program__attach_cgroup(skel->progs.learn_dns_answer,
|
||||
cgroup_fd);
|
||||
connect_link = bpf_program__attach_cgroup(skel->progs.enforce_dns_policy,
|
||||
cgroup_fd);
|
||||
if (libbpf_get_error(query_link) || libbpf_get_error(ingress_link) ||
|
||||
libbpf_get_error(connect_link)) {
|
||||
fprintf(stderr, "failed to attach programs to cgroup %s\n",
|
||||
options.cgroup_path);
|
||||
query_link = libbpf_get_error(query_link) ? NULL : query_link;
|
||||
ingress_link = libbpf_get_error(ingress_link) ? NULL : ingress_link;
|
||||
connect_link = libbpf_get_error(connect_link) ? NULL : connect_link;
|
||||
goto cleanup;
|
||||
}
|
||||
ring = ring_buffer__new(bpf_map__fd(skel->maps.events), handle_event,
|
||||
NULL, NULL);
|
||||
if (!ring)
|
||||
goto cleanup;
|
||||
|
||||
printf("dns-egress attached cgroup=%s domain=%s resolver=%s tcp_port=%u dns_port=%u\n",
|
||||
options.cgroup_path, options.domain, options.dns_server,
|
||||
options.port, options.dns_port);
|
||||
if (options.demo) {
|
||||
if (run_demo(ring, &options, qname, qname_length))
|
||||
goto cleanup;
|
||||
} else {
|
||||
signal(SIGINT, handle_signal);
|
||||
signal(SIGTERM, handle_signal);
|
||||
if (options.duration_seconds)
|
||||
deadline = monotonic_ns() +
|
||||
(unsigned long long)options.duration_seconds *
|
||||
1000000000ULL;
|
||||
while (!stop && (!deadline || monotonic_ns() < deadline)) {
|
||||
int poll_result = ring_buffer__poll(ring, 100);
|
||||
|
||||
if (poll_result < 0 && poll_result != -EINTR) {
|
||||
fprintf(stderr, "ring buffer poll failed: %d\n",
|
||||
poll_result);
|
||||
goto cleanup;
|
||||
}
|
||||
}
|
||||
}
|
||||
err = 0;
|
||||
|
||||
cleanup:
|
||||
ring_buffer__free(ring);
|
||||
bpf_link__destroy(connect_link);
|
||||
bpf_link__destroy(ingress_link);
|
||||
bpf_link__destroy(query_link);
|
||||
if (cgroup_fd >= 0) close(cgroup_fd);
|
||||
dns_egress_bpf__destroy(skel);
|
||||
return err;
|
||||
}
|
||||
```
|
||||
|
||||
用户空间程序加载并将 BPF 程序附加到 cgroup。它将域名编码为 DNS 标签格式,配置 BPF 程序的目标解析器和受保护端口,并轮询 ring buffer 获取策略事件。
|
||||
|
||||
演示模式运行一个独立测试:它创建本地 UDP 和 TCP 套接字,模拟 DNS 解析,并验证策略是否正确工作。它测试 DNS 之前的连接阻止、拒绝未经请求的响应、拒绝错误的事务 ID、有效 DNS 后允许连接,以及 TTL 过期后阻止连接。
|
||||
|
||||
## 编译和执行
|
||||
|
||||
构建示例:
|
||||
|
||||
```bash
|
||||
cd src/55-dns-egress
|
||||
make
|
||||
```
|
||||
|
||||
使用必需参数运行:
|
||||
|
||||
```bash
|
||||
sudo ./dns_egress --cgroup /sys/fs/cgroup/my-service --domain api.example.com --dns-server 127.0.0.53 --port 443
|
||||
```
|
||||
|
||||
或运行内置演示:
|
||||
|
||||
```bash
|
||||
sudo ./dns_egress --demo
|
||||
```
|
||||
|
||||
示例演示输出:
|
||||
|
||||
```text
|
||||
dns-egress attached cgroup=/sys/fs/cgroup domain=lab.test resolver=127.0.0.1 tcp_port=19090 dns_port=15353
|
||||
event=denied pid=1246 ip=127.0.0.1 ttl=0
|
||||
demo step=before-dns result=blocked
|
||||
event=denied pid=1246 ip=127.0.0.1 ttl=0
|
||||
demo step=unsolicited-response result=blocked
|
||||
event=denied pid=1246 ip=127.0.0.1 ttl=0
|
||||
demo step=wrong-transaction-id result=blocked
|
||||
event=learned pid=1246 ip=127.0.0.1 ttl=1
|
||||
event=allowed pid=1246 ip=127.0.0.1 ttl=1
|
||||
demo step=live-answer result=allowed
|
||||
event=expired pid=1246 ip=127.0.0.1 ttl=1
|
||||
event=denied pid=1246 ip=127.0.0.1 ttl=1
|
||||
demo step=expired-answer result=blocked
|
||||
```
|
||||
|
||||
## 环境要求
|
||||
|
||||
| 要求 | 详情 |
|
||||
|------|------|
|
||||
| 内核 | Linux 5.8+(BPF ring buffer) |
|
||||
| 配置 | `CONFIG_BPF_SYSCALL`、`CONFIG_CGROUP_BPF`、`CONFIG_DEBUG_INFO_BTF` |
|
||||
| 权限 | Root |
|
||||
| cgroup | cgroup v2 已挂载 |
|
||||
|
||||
## 范围和限制
|
||||
|
||||
此实现保护一个配置好的 TCP 目的端口,其他端口保持不变。它支持启动时配置的一个精确查询名称,仅支持 IPv4 UDP DNS,并解析直接的第一个 A 记录应答。待处理查询在 5 秒后过期,允许的 IP 使用带有 DNS TTL 的 1024 条目 LRU 哈希。它不处理 CNAME 链、多应答布局、TCP DNS、IPv6、DNS-over-HTTPS 或 DNS-over-TLS。
|
||||
|
||||
## 总结
|
||||
|
||||
这个例子把 DNS 响应与最近的查询关联起来,再把 TTL 带到 connect 阶段的策略判断中,从而把一个域名变成内核中的短期允许列表。三个 cgroup hook 分别负责 DNS 观察和连接执行,ring buffer 事件则让用户态能够看到每一次策略决定。
|
||||
|
||||
> 如果你想深入了解 eBPF,请查看我们的教程代码仓库 <https://github.com/eunomia-bpf/bpf-developer-tutorial> 或访问我们的网站 <https://eunomia.dev/tutorials/>。
|
||||
|
||||
## 参考资料
|
||||
|
||||
- [BPF ring buffer 文档](https://docs.kernel.org/6.6/bpf/ringbuf.html)
|
||||
- [BPF hash map 文档](https://docs.kernel.org/bpf/map_hash.html)
|
||||
- [cgroup BPF commit](https://github.com/torvalds/linux/commit/d74bad4e74ee)
|
||||
239
src/55-dns-egress/dns_egress.bpf.c
Normal file
239
src/55-dns-egress/dns_egress.bpf.c
Normal file
@@ -0,0 +1,239 @@
|
||||
// SPDX-License-Identifier: GPL-2.0
|
||||
#include "vmlinux.h"
|
||||
#include <bpf/bpf_helpers.h>
|
||||
#include <bpf/bpf_endian.h>
|
||||
#include "dns_egress.h"
|
||||
|
||||
char LICENSE[] SEC("license") = "GPL";
|
||||
|
||||
#define IPPROTO_UDP 17
|
||||
#define IPPROTO_TCP 6
|
||||
#define IP_MF 0x2000
|
||||
#define IP_OFFSET 0x1fff
|
||||
#define DNS_QUERY_LIFETIME_NS (5ULL * 1000000000ULL)
|
||||
|
||||
const volatile __u32 target_tgid;
|
||||
const volatile __u32 dns_server_ip;
|
||||
const volatile __u16 dns_server_port = 53;
|
||||
const volatile __u16 protected_tcp_port = 443;
|
||||
const volatile __u32 configured_qname_length;
|
||||
const volatile unsigned char configured_qname[DNS_QNAME_MAX];
|
||||
|
||||
struct dns_state {
|
||||
__u64 expires_ns;
|
||||
__u32 ttl_seconds;
|
||||
__u32 pad;
|
||||
__u64 expired_reported;
|
||||
};
|
||||
|
||||
struct dns_query_key {
|
||||
__u32 server_ip;
|
||||
__u32 client_ip;
|
||||
__u16 client_port;
|
||||
__u16 transaction_id;
|
||||
};
|
||||
|
||||
struct dns_query_state {
|
||||
__u64 expires_ns;
|
||||
};
|
||||
|
||||
struct {
|
||||
__uint(type, BPF_MAP_TYPE_LRU_HASH);
|
||||
__uint(max_entries, 1024);
|
||||
__type(key, struct dns_query_key);
|
||||
__type(value, struct dns_query_state);
|
||||
} pending_queries SEC(".maps");
|
||||
|
||||
struct {
|
||||
__uint(type, BPF_MAP_TYPE_LRU_HASH);
|
||||
__uint(max_entries, 1024);
|
||||
__type(key, __u32);
|
||||
__type(value, struct dns_state);
|
||||
} allowed_ips SEC(".maps");
|
||||
|
||||
struct {
|
||||
__uint(type, BPF_MAP_TYPE_RINGBUF);
|
||||
__uint(max_entries, 256 * 1024);
|
||||
} events SEC(".maps");
|
||||
|
||||
static __always_inline void emit_event(__u32 type, __u32 ip4,
|
||||
__u32 ttl_seconds, __u64 expires_ns)
|
||||
{
|
||||
struct dns_egress_event *event;
|
||||
|
||||
event = bpf_ringbuf_reserve(&events, sizeof(*event), 0);
|
||||
if (!event)
|
||||
return;
|
||||
event->timestamp_ns = bpf_ktime_get_ns();
|
||||
event->expires_ns = expires_ns;
|
||||
event->type = type;
|
||||
event->pid = bpf_get_current_pid_tgid() >> 32;
|
||||
event->ip4 = ip4;
|
||||
event->ttl_seconds = ttl_seconds;
|
||||
bpf_ringbuf_submit(event, 0);
|
||||
}
|
||||
|
||||
static __noinline bool matches_qname(struct __sk_buff *skb, __u32 offset)
|
||||
{
|
||||
unsigned char byte;
|
||||
|
||||
if (!configured_qname_length || configured_qname_length > DNS_QNAME_MAX)
|
||||
return false;
|
||||
#pragma clang loop unroll(disable)
|
||||
for (int i = 0; i < DNS_QNAME_MAX; i++) {
|
||||
if (i >= configured_qname_length)
|
||||
break;
|
||||
if (bpf_skb_load_bytes(skb, offset + i, &byte, sizeof(byte)) ||
|
||||
byte != configured_qname[i])
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
SEC("cgroup_skb/egress")
|
||||
int record_dns_query(struct __sk_buff *skb)
|
||||
{
|
||||
struct dns_query_state state = {
|
||||
.expires_ns = bpf_ktime_get_ns() + DNS_QUERY_LIFETIME_NS,
|
||||
};
|
||||
struct dns_query_key key = {};
|
||||
struct dns_question question;
|
||||
struct dns_header header;
|
||||
struct udphdr udp;
|
||||
struct iphdr ip;
|
||||
__u32 ip_header_len;
|
||||
__u32 dns_offset;
|
||||
__u16 flags;
|
||||
|
||||
if (bpf_skb_load_bytes(skb, 0, &ip, sizeof(ip)))
|
||||
return 1;
|
||||
if (ip.version != 4 || ip.protocol != IPPROTO_UDP || ip.ihl < 5 ||
|
||||
(bpf_ntohs(ip.frag_off) & (IP_MF | IP_OFFSET)) ||
|
||||
ip.daddr != dns_server_ip)
|
||||
return 1;
|
||||
ip_header_len = ip.ihl * 4;
|
||||
if (bpf_skb_load_bytes(skb, ip_header_len, &udp, sizeof(udp)) ||
|
||||
bpf_ntohs(udp.dest) != dns_server_port)
|
||||
return 1;
|
||||
dns_offset = ip_header_len + sizeof(udp);
|
||||
if (bpf_skb_load_bytes(skb, dns_offset, &header, sizeof(header)))
|
||||
return 1;
|
||||
flags = bpf_ntohs(header.flags);
|
||||
if ((flags & 0xf800) || bpf_ntohs(header.questions) != 1)
|
||||
return 1;
|
||||
if (!matches_qname(skb, dns_offset + sizeof(header)))
|
||||
return 1;
|
||||
dns_offset += sizeof(header) + configured_qname_length;
|
||||
if (bpf_skb_load_bytes(skb, dns_offset, &question, sizeof(question)) ||
|
||||
bpf_ntohs(question.type) != 1 || bpf_ntohs(question.class) != 1)
|
||||
return 1;
|
||||
|
||||
key.server_ip = ip.daddr;
|
||||
key.client_ip = ip.saddr;
|
||||
key.client_port = udp.source;
|
||||
key.transaction_id = header.id;
|
||||
bpf_map_update_elem(&pending_queries, &key, &state, BPF_ANY);
|
||||
return 1;
|
||||
}
|
||||
|
||||
SEC("cgroup_skb/ingress")
|
||||
int learn_dns_answer(struct __sk_buff *skb)
|
||||
{
|
||||
struct dns_a_answer answer;
|
||||
struct dns_question question;
|
||||
struct dns_header header;
|
||||
struct dns_query_key query_key = {};
|
||||
struct dns_query_state *query;
|
||||
struct dns_state state = {};
|
||||
struct udphdr udp;
|
||||
struct iphdr ip;
|
||||
__u64 ttl_ns, expires;
|
||||
__u32 ip_header_len;
|
||||
__u32 dns_offset;
|
||||
__u32 key;
|
||||
__u32 ttl;
|
||||
|
||||
if (bpf_skb_load_bytes(skb, 0, &ip, sizeof(ip)))
|
||||
return 1;
|
||||
if (ip.version != 4 || ip.protocol != IPPROTO_UDP || ip.ihl < 5 ||
|
||||
(bpf_ntohs(ip.frag_off) & (IP_MF | IP_OFFSET)) ||
|
||||
ip.saddr != dns_server_ip)
|
||||
return 1;
|
||||
ip_header_len = ip.ihl * 4;
|
||||
if (bpf_skb_load_bytes(skb, ip_header_len, &udp, sizeof(udp)) ||
|
||||
bpf_ntohs(udp.source) != dns_server_port)
|
||||
return 1;
|
||||
dns_offset = ip_header_len + sizeof(udp);
|
||||
if (bpf_skb_load_bytes(skb, dns_offset, &header, sizeof(header)))
|
||||
return 1;
|
||||
query_key.server_ip = ip.saddr;
|
||||
query_key.client_ip = ip.daddr;
|
||||
query_key.client_port = udp.dest;
|
||||
query_key.transaction_id = header.id;
|
||||
query = bpf_map_lookup_elem(&pending_queries, &query_key);
|
||||
if (!query)
|
||||
return 1;
|
||||
if (bpf_ktime_get_ns() >= query->expires_ns) {
|
||||
bpf_map_delete_elem(&pending_queries, &query_key);
|
||||
return 1;
|
||||
}
|
||||
if ((bpf_ntohs(header.flags) & 0xf80f) != 0x8000 ||
|
||||
bpf_ntohs(header.questions) != 1 || !bpf_ntohs(header.answers))
|
||||
return 1;
|
||||
if (!matches_qname(skb, dns_offset + sizeof(header)))
|
||||
return 1;
|
||||
dns_offset += sizeof(header) + configured_qname_length;
|
||||
if (bpf_skb_load_bytes(skb, dns_offset, &question, sizeof(question)) ||
|
||||
bpf_ntohs(question.type) != 1 || bpf_ntohs(question.class) != 1)
|
||||
return 1;
|
||||
dns_offset += sizeof(question);
|
||||
if (bpf_skb_load_bytes(skb, dns_offset, &answer, sizeof(answer)) ||
|
||||
bpf_ntohs(answer.name) != 0xc00c || bpf_ntohs(answer.type) != 1 ||
|
||||
bpf_ntohs(answer.class) != 1 ||
|
||||
bpf_ntohs(answer.address_length) != 4)
|
||||
return 1;
|
||||
bpf_map_delete_elem(&pending_queries, &query_key);
|
||||
|
||||
key = answer.address;
|
||||
ttl = bpf_ntohl(answer.ttl);
|
||||
if (!ttl || ttl > 86400)
|
||||
return 1;
|
||||
ttl_ns = (__u64)ttl * 1000000000ULL;
|
||||
expires = bpf_ktime_get_ns() + ttl_ns;
|
||||
state.expires_ns = expires;
|
||||
state.ttl_seconds = ttl;
|
||||
if (bpf_map_update_elem(&allowed_ips, &key, &state, BPF_ANY))
|
||||
return 1;
|
||||
emit_event(DNS_LEARNED, key, ttl, expires);
|
||||
return 1;
|
||||
}
|
||||
|
||||
SEC("cgroup/connect4")
|
||||
int enforce_dns_policy(struct bpf_sock_addr *ctx)
|
||||
{
|
||||
struct dns_state *state;
|
||||
__u64 expires = 0;
|
||||
__u32 ip4;
|
||||
__u32 ttl = 0;
|
||||
|
||||
if ((target_tgid &&
|
||||
(__u32)(bpf_get_current_pid_tgid() >> 32) != target_tgid) ||
|
||||
ctx->protocol != IPPROTO_TCP ||
|
||||
bpf_ntohs((__u16)ctx->user_port) != protected_tcp_port)
|
||||
return 1;
|
||||
|
||||
ip4 = ctx->user_ip4;
|
||||
state = bpf_map_lookup_elem(&allowed_ips, &ip4);
|
||||
if (state) {
|
||||
expires = state->expires_ns;
|
||||
ttl = state->ttl_seconds;
|
||||
if (bpf_ktime_get_ns() < expires) {
|
||||
emit_event(DNS_ALLOWED, ip4, ttl, expires);
|
||||
return 1;
|
||||
}
|
||||
if (__sync_val_compare_and_swap(&state->expired_reported, 0, 1) == 0)
|
||||
emit_event(DNS_EXPIRED, ip4, ttl, expires);
|
||||
}
|
||||
emit_event(DNS_DENIED, ip4, ttl, expires);
|
||||
return 0;
|
||||
}
|
||||
540
src/55-dns-egress/dns_egress.c
Normal file
540
src/55-dns-egress/dns_egress.c
Normal file
@@ -0,0 +1,540 @@
|
||||
// SPDX-License-Identifier: (LGPL-2.1 OR BSD-2-Clause)
|
||||
#include <arpa/inet.h>
|
||||
#include <errno.h>
|
||||
#include <fcntl.h>
|
||||
#include <getopt.h>
|
||||
#include <netinet/in.h>
|
||||
#include <signal.h>
|
||||
#include <stdbool.h>
|
||||
#include <stddef.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <sys/socket.h>
|
||||
#include <time.h>
|
||||
#include <unistd.h>
|
||||
#include <bpf/libbpf.h>
|
||||
#include "dns_egress.h"
|
||||
#include "dns_egress.skel.h"
|
||||
|
||||
#define DEMO_DNS_PORT 15353
|
||||
#define DEMO_TCP_PORT 19090
|
||||
#define DNS_ID 0x4b1d
|
||||
|
||||
struct options {
|
||||
const char *cgroup_path;
|
||||
const char *domain;
|
||||
const char *dns_server;
|
||||
unsigned int port;
|
||||
unsigned int dns_port;
|
||||
unsigned int duration_seconds;
|
||||
bool demo;
|
||||
};
|
||||
|
||||
static int event_counts[5];
|
||||
static volatile sig_atomic_t stop;
|
||||
|
||||
static void handle_signal(int signal_number)
|
||||
{
|
||||
(void)signal_number;
|
||||
stop = 1;
|
||||
}
|
||||
|
||||
static unsigned long long monotonic_ns(void)
|
||||
{
|
||||
struct timespec now;
|
||||
|
||||
clock_gettime(CLOCK_MONOTONIC, &now);
|
||||
return (unsigned long long)now.tv_sec * 1000000000ULL + now.tv_nsec;
|
||||
}
|
||||
|
||||
static int handle_event(void *ctx, void *data, size_t size)
|
||||
{
|
||||
const struct dns_egress_event *event = data;
|
||||
char address[INET_ADDRSTRLEN];
|
||||
const char *name;
|
||||
|
||||
(void)ctx;
|
||||
if (size != sizeof(*event) || event->type > DNS_EXPIRED)
|
||||
return 0;
|
||||
event_counts[event->type]++;
|
||||
inet_ntop(AF_INET, &event->ip4, address, sizeof(address));
|
||||
name = event->type == DNS_LEARNED ? "learned" :
|
||||
event->type == DNS_ALLOWED ? "allowed" :
|
||||
event->type == DNS_DENIED ? "denied" : "expired";
|
||||
printf("event=%s pid=%u ip=%s ttl=%u\n", name, event->pid, address,
|
||||
event->ttl_seconds);
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int parse_uint(const char *text, unsigned int maximum,
|
||||
unsigned int *value)
|
||||
{
|
||||
char *end = NULL;
|
||||
unsigned long parsed;
|
||||
|
||||
errno = 0;
|
||||
parsed = strtoul(text, &end, 10);
|
||||
if (errno || !*text || *end || parsed > maximum)
|
||||
return -1;
|
||||
*value = parsed;
|
||||
return 0;
|
||||
}
|
||||
|
||||
static void usage(const char *program)
|
||||
{
|
||||
printf("Usage: %s --cgroup PATH --domain NAME --dns-server IPV4 [--port PORT] [--dns-port PORT] [--duration SEC]\n"
|
||||
" %s --demo\n", program, program);
|
||||
}
|
||||
|
||||
static int parse_options(int argc, char **argv, struct options *options)
|
||||
{
|
||||
static const struct option long_options[] = {
|
||||
{ "cgroup", required_argument, NULL, 'c' },
|
||||
{ "domain", required_argument, NULL, 'n' },
|
||||
{ "dns-server", required_argument, NULL, 'r' },
|
||||
{ "port", required_argument, NULL, 'p' },
|
||||
{ "dns-port", required_argument, NULL, 's' },
|
||||
{ "duration", required_argument, NULL, 'd' },
|
||||
{ "demo", no_argument, NULL, 'D' },
|
||||
{ "help", no_argument, NULL, 'h' },
|
||||
{},
|
||||
};
|
||||
int option;
|
||||
|
||||
while ((option = getopt_long(argc, argv, "c:n:r:p:s:d:Dh", long_options,
|
||||
NULL)) != -1) {
|
||||
switch (option) {
|
||||
case 'c': options->cgroup_path = optarg; break;
|
||||
case 'n': options->domain = optarg; break;
|
||||
case 'r': options->dns_server = optarg; break;
|
||||
case 'p':
|
||||
if (parse_uint(optarg, 65535, &options->port) ||
|
||||
!options->port)
|
||||
return -1;
|
||||
break;
|
||||
case 's':
|
||||
if (parse_uint(optarg, 65535, &options->dns_port) ||
|
||||
!options->dns_port)
|
||||
return -1;
|
||||
break;
|
||||
case 'd':
|
||||
if (parse_uint(optarg, 86400,
|
||||
&options->duration_seconds))
|
||||
return -1;
|
||||
break;
|
||||
case 'D': options->demo = true; break;
|
||||
case 'h': usage(argv[0]); exit(0);
|
||||
default: return -1;
|
||||
}
|
||||
}
|
||||
if (optind != argc)
|
||||
return -1;
|
||||
if (options->demo) {
|
||||
if (options->cgroup_path || options->domain || options->dns_server)
|
||||
return -1;
|
||||
options->cgroup_path = "/sys/fs/cgroup";
|
||||
options->domain = "lab.test";
|
||||
options->dns_server = "127.0.0.1";
|
||||
options->port = DEMO_TCP_PORT;
|
||||
options->dns_port = DEMO_DNS_PORT;
|
||||
return 0;
|
||||
}
|
||||
return options->cgroup_path && options->domain && options->dns_server ?
|
||||
0 : -1;
|
||||
}
|
||||
|
||||
static int encode_qname(const char *domain, unsigned char output[DNS_QNAME_MAX],
|
||||
unsigned int *output_length)
|
||||
{
|
||||
const char *label = domain;
|
||||
unsigned int used = 0;
|
||||
|
||||
if (!*domain)
|
||||
return -1;
|
||||
while (*label) {
|
||||
const char *dot = strchr(label, '.');
|
||||
size_t length = dot ? (size_t)(dot - label) : strlen(label);
|
||||
|
||||
if (!length || length > 63 || used + length + 2 > DNS_QNAME_MAX)
|
||||
return -1;
|
||||
output[used++] = length;
|
||||
memcpy(output + used, label, length);
|
||||
used += length;
|
||||
if (!dot)
|
||||
break;
|
||||
label = dot + 1;
|
||||
if (!*label)
|
||||
break;
|
||||
}
|
||||
output[used++] = 0;
|
||||
*output_length = used;
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int bind_udp(struct sockaddr_in *address)
|
||||
{
|
||||
socklen_t length = sizeof(*address);
|
||||
int fd;
|
||||
|
||||
fd = socket(AF_INET, SOCK_DGRAM | SOCK_CLOEXEC, 0);
|
||||
if (fd < 0)
|
||||
return -1;
|
||||
if (bind(fd, (struct sockaddr *)address, sizeof(*address)) ||
|
||||
getsockname(fd, (struct sockaddr *)address, &length)) {
|
||||
close(fd);
|
||||
return -1;
|
||||
}
|
||||
return fd;
|
||||
}
|
||||
|
||||
static int begin_dns_exchange(int server, int client,
|
||||
struct sockaddr_in *server_address,
|
||||
const unsigned char *qname,
|
||||
unsigned int qname_length,
|
||||
unsigned char message[512],
|
||||
size_t *query_length,
|
||||
struct sockaddr_in *client_address)
|
||||
{
|
||||
struct dns_question question = {
|
||||
.type = htons(1),
|
||||
.class = htons(1),
|
||||
};
|
||||
struct dns_header *header = (void *)message;
|
||||
socklen_t address_length = sizeof(*client_address);
|
||||
ssize_t received;
|
||||
|
||||
memset(message, 0, 512);
|
||||
header->id = htons(DNS_ID);
|
||||
header->flags = htons(0x0100);
|
||||
header->questions = htons(1);
|
||||
memcpy(message + sizeof(*header), qname, qname_length);
|
||||
memcpy(message + sizeof(*header) + qname_length, &question,
|
||||
sizeof(question));
|
||||
*query_length = sizeof(*header) + qname_length + sizeof(question);
|
||||
if (sendto(client, message, *query_length, 0,
|
||||
(struct sockaddr *)server_address, sizeof(*server_address)) !=
|
||||
(ssize_t)*query_length)
|
||||
return -1;
|
||||
received = recvfrom(server, message, 512, 0,
|
||||
(struct sockaddr *)client_address, &address_length);
|
||||
return received == (ssize_t)*query_length ? 0 : -1;
|
||||
}
|
||||
|
||||
static int send_dns_answer(int server, int client,
|
||||
struct sockaddr_in *client_address,
|
||||
unsigned char message[512], size_t query_length,
|
||||
unsigned short transaction_id,
|
||||
unsigned int ttl_seconds)
|
||||
{
|
||||
struct dns_a_answer answer = {
|
||||
.name = htons(0xc00c),
|
||||
.type = htons(1),
|
||||
.class = htons(1),
|
||||
.ttl = htonl(ttl_seconds),
|
||||
.address_length = htons(4),
|
||||
};
|
||||
struct dns_header *header = (void *)message;
|
||||
|
||||
if (query_length + sizeof(answer) > 512)
|
||||
return -1;
|
||||
header->id = htons(transaction_id);
|
||||
header->flags = htons(0x8180);
|
||||
header->answers = htons(1);
|
||||
inet_pton(AF_INET, "127.0.0.1", &answer.address);
|
||||
memcpy(message + query_length, &answer, sizeof(answer));
|
||||
if (sendto(server, message, query_length + sizeof(answer), 0,
|
||||
(struct sockaddr *)client_address, sizeof(*client_address)) !=
|
||||
(ssize_t)(query_length + sizeof(answer)))
|
||||
return -1;
|
||||
return recv(client, message, 512, 0) ==
|
||||
(ssize_t)(query_length + sizeof(answer)) ? 0 : -1;
|
||||
}
|
||||
|
||||
static int send_unsolicited_dns(int server, int client,
|
||||
const unsigned char *qname,
|
||||
unsigned int qname_length)
|
||||
{
|
||||
unsigned char message[512] = {};
|
||||
struct sockaddr_in client_address;
|
||||
struct dns_a_answer answer = {
|
||||
.name = htons(0xc00c),
|
||||
.type = htons(1),
|
||||
.class = htons(1),
|
||||
.ttl = htonl(30),
|
||||
.address_length = htons(4),
|
||||
};
|
||||
struct dns_question question = {
|
||||
.type = htons(1),
|
||||
.class = htons(1),
|
||||
};
|
||||
struct dns_header *header = (void *)message;
|
||||
socklen_t address_length = sizeof(client_address);
|
||||
size_t message_length;
|
||||
|
||||
if (getsockname(client, (struct sockaddr *)&client_address,
|
||||
&address_length))
|
||||
return -1;
|
||||
header->id = htons(DNS_ID + 1);
|
||||
header->flags = htons(0x8180);
|
||||
header->questions = htons(1);
|
||||
header->answers = htons(1);
|
||||
memcpy(message + sizeof(*header), qname, qname_length);
|
||||
memcpy(message + sizeof(*header) + qname_length, &question,
|
||||
sizeof(question));
|
||||
message_length = sizeof(*header) + qname_length + sizeof(question);
|
||||
inet_pton(AF_INET, "127.0.0.1", &answer.address);
|
||||
memcpy(message + message_length, &answer, sizeof(answer));
|
||||
message_length += sizeof(answer);
|
||||
if (sendto(server, message, message_length, 0,
|
||||
(struct sockaddr *)&client_address, address_length) !=
|
||||
(ssize_t)message_length)
|
||||
return -1;
|
||||
return recv(client, message, sizeof(message), 0) ==
|
||||
(ssize_t)message_length ? 0 : -1;
|
||||
}
|
||||
|
||||
static int create_tcp_listener(unsigned int port)
|
||||
{
|
||||
struct sockaddr_in address = {
|
||||
.sin_family = AF_INET,
|
||||
.sin_addr.s_addr = htonl(INADDR_LOOPBACK),
|
||||
.sin_port = htons(port),
|
||||
};
|
||||
int one = 1;
|
||||
int fd;
|
||||
|
||||
fd = socket(AF_INET, SOCK_STREAM | SOCK_CLOEXEC, 0);
|
||||
if (fd < 0)
|
||||
return -1;
|
||||
setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &one, sizeof(one));
|
||||
if (bind(fd, (struct sockaddr *)&address, sizeof(address)) ||
|
||||
listen(fd, 4)) {
|
||||
close(fd);
|
||||
return -1;
|
||||
}
|
||||
return fd;
|
||||
}
|
||||
|
||||
static int connect_tcp(unsigned int port)
|
||||
{
|
||||
struct sockaddr_in address = {
|
||||
.sin_family = AF_INET,
|
||||
.sin_addr.s_addr = htonl(INADDR_LOOPBACK),
|
||||
.sin_port = htons(port),
|
||||
};
|
||||
int saved_errno;
|
||||
int fd;
|
||||
|
||||
fd = socket(AF_INET, SOCK_STREAM | SOCK_CLOEXEC, 0);
|
||||
if (fd < 0)
|
||||
return -1;
|
||||
if (!connect(fd, (struct sockaddr *)&address, sizeof(address)))
|
||||
return fd;
|
||||
saved_errno = errno;
|
||||
close(fd);
|
||||
errno = saved_errno;
|
||||
return -1;
|
||||
}
|
||||
|
||||
static int complete_tcp(int listener, int client)
|
||||
{
|
||||
int accepted = accept4(listener, NULL, NULL, SOCK_CLOEXEC);
|
||||
char byte = 'x';
|
||||
|
||||
if (accepted < 0)
|
||||
return -1;
|
||||
if (write(accepted, &byte, 1) != 1 || read(client, &byte, 1) != 1) {
|
||||
close(accepted);
|
||||
return -1;
|
||||
}
|
||||
close(accepted);
|
||||
close(client);
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int run_demo(struct ring_buffer *ring, const struct options *options,
|
||||
const unsigned char *qname, unsigned int qname_length)
|
||||
{
|
||||
struct sockaddr_in server_address = {
|
||||
.sin_family = AF_INET,
|
||||
.sin_addr.s_addr = htonl(INADDR_LOOPBACK),
|
||||
.sin_port = htons(options->dns_port),
|
||||
};
|
||||
struct sockaddr_in client_address = {
|
||||
.sin_family = AF_INET,
|
||||
.sin_addr.s_addr = htonl(INADDR_LOOPBACK),
|
||||
};
|
||||
struct timespec wait_time = { .tv_sec = 1, .tv_nsec = 300000000 };
|
||||
struct sockaddr_in response_client_address;
|
||||
unsigned char dns_message[512];
|
||||
size_t query_length;
|
||||
int dns_server = -1, dns_client = -1, listener = -1, client = -1;
|
||||
int err = -1;
|
||||
|
||||
dns_server = bind_udp(&server_address);
|
||||
dns_client = bind_udp(&client_address);
|
||||
listener = create_tcp_listener(options->port);
|
||||
if (dns_server < 0 || dns_client < 0 || listener < 0)
|
||||
goto cleanup;
|
||||
|
||||
errno = 0;
|
||||
client = connect_tcp(options->port);
|
||||
if (client >= 0 || errno != EPERM)
|
||||
goto cleanup;
|
||||
ring_buffer__poll(ring, 100);
|
||||
printf("demo step=before-dns result=blocked\n");
|
||||
|
||||
if (send_unsolicited_dns(dns_server, dns_client, qname, qname_length))
|
||||
goto cleanup;
|
||||
ring_buffer__poll(ring, 100);
|
||||
errno = 0;
|
||||
client = connect_tcp(options->port);
|
||||
if (client >= 0 || errno != EPERM)
|
||||
goto cleanup;
|
||||
ring_buffer__poll(ring, 100);
|
||||
printf("demo step=unsolicited-response result=blocked\n");
|
||||
|
||||
if (begin_dns_exchange(dns_server, dns_client, &server_address, qname,
|
||||
qname_length, dns_message, &query_length,
|
||||
&response_client_address) ||
|
||||
send_dns_answer(dns_server, dns_client, &response_client_address,
|
||||
dns_message, query_length, DNS_ID + 1, 30))
|
||||
goto cleanup;
|
||||
ring_buffer__poll(ring, 100);
|
||||
errno = 0;
|
||||
client = connect_tcp(options->port);
|
||||
if (client >= 0 || errno != EPERM)
|
||||
goto cleanup;
|
||||
ring_buffer__poll(ring, 100);
|
||||
printf("demo step=wrong-transaction-id result=blocked\n");
|
||||
|
||||
if (send_dns_answer(dns_server, dns_client, &response_client_address,
|
||||
dns_message, query_length, DNS_ID, 1))
|
||||
goto cleanup;
|
||||
ring_buffer__poll(ring, 100);
|
||||
client = connect_tcp(options->port);
|
||||
if (client < 0 || complete_tcp(listener, client))
|
||||
goto cleanup;
|
||||
client = -1;
|
||||
ring_buffer__poll(ring, 100);
|
||||
printf("demo step=live-answer result=allowed\n");
|
||||
|
||||
nanosleep(&wait_time, NULL);
|
||||
ring_buffer__poll(ring, 100);
|
||||
errno = 0;
|
||||
client = connect_tcp(options->port);
|
||||
if (client >= 0 || errno != EPERM)
|
||||
goto cleanup;
|
||||
ring_buffer__poll(ring, 100);
|
||||
printf("demo step=expired-answer result=blocked\n");
|
||||
|
||||
if (event_counts[DNS_LEARNED] != 1 || event_counts[DNS_ALLOWED] != 1 ||
|
||||
event_counts[DNS_DENIED] != 4 || event_counts[DNS_EXPIRED] != 1)
|
||||
goto cleanup;
|
||||
err = 0;
|
||||
|
||||
cleanup:
|
||||
if (client >= 0) close(client);
|
||||
if (listener >= 0) close(listener);
|
||||
if (dns_client >= 0) close(dns_client);
|
||||
if (dns_server >= 0) close(dns_server);
|
||||
return err;
|
||||
}
|
||||
|
||||
int main(int argc, char **argv)
|
||||
{
|
||||
struct options options = { .port = 443, .dns_port = 53 };
|
||||
struct dns_egress_bpf *skel = NULL;
|
||||
struct bpf_link *query_link = NULL, *ingress_link = NULL;
|
||||
struct bpf_link *connect_link = NULL;
|
||||
struct ring_buffer *ring = NULL;
|
||||
struct in_addr dns_server = {};
|
||||
unsigned char qname[DNS_QNAME_MAX] = {};
|
||||
unsigned long long deadline = 0;
|
||||
unsigned int qname_length = 0;
|
||||
int cgroup_fd = -1;
|
||||
int err = 1;
|
||||
|
||||
setvbuf(stdout, NULL, _IONBF, 0);
|
||||
if (parse_options(argc, argv, &options) ||
|
||||
encode_qname(options.domain, qname, &qname_length) ||
|
||||
inet_pton(AF_INET, options.dns_server, &dns_server) != 1) {
|
||||
usage(argv[0]);
|
||||
return 2;
|
||||
}
|
||||
cgroup_fd = open(options.cgroup_path,
|
||||
O_RDONLY | O_DIRECTORY | O_CLOEXEC);
|
||||
if (cgroup_fd < 0) {
|
||||
fprintf(stderr, "failed to open cgroup %s: %s\n",
|
||||
options.cgroup_path, strerror(errno));
|
||||
goto cleanup;
|
||||
}
|
||||
|
||||
skel = dns_egress_bpf__open();
|
||||
if (!skel)
|
||||
goto cleanup;
|
||||
skel->rodata->target_tgid = options.demo ? getpid() : 0;
|
||||
skel->rodata->dns_server_ip = dns_server.s_addr;
|
||||
skel->rodata->dns_server_port = options.dns_port;
|
||||
skel->rodata->protected_tcp_port = options.port;
|
||||
skel->rodata->configured_qname_length = qname_length;
|
||||
memcpy((void *)skel->rodata->configured_qname, qname, qname_length);
|
||||
if (dns_egress_bpf__load(skel)) {
|
||||
fprintf(stderr, "failed to load DNS egress BPF programs\n");
|
||||
goto cleanup;
|
||||
}
|
||||
query_link = bpf_program__attach_cgroup(skel->progs.record_dns_query,
|
||||
cgroup_fd);
|
||||
ingress_link = bpf_program__attach_cgroup(skel->progs.learn_dns_answer,
|
||||
cgroup_fd);
|
||||
connect_link = bpf_program__attach_cgroup(skel->progs.enforce_dns_policy,
|
||||
cgroup_fd);
|
||||
if (libbpf_get_error(query_link) || libbpf_get_error(ingress_link) ||
|
||||
libbpf_get_error(connect_link)) {
|
||||
fprintf(stderr, "failed to attach programs to cgroup %s\n",
|
||||
options.cgroup_path);
|
||||
query_link = libbpf_get_error(query_link) ? NULL : query_link;
|
||||
ingress_link = libbpf_get_error(ingress_link) ? NULL : ingress_link;
|
||||
connect_link = libbpf_get_error(connect_link) ? NULL : connect_link;
|
||||
goto cleanup;
|
||||
}
|
||||
ring = ring_buffer__new(bpf_map__fd(skel->maps.events), handle_event,
|
||||
NULL, NULL);
|
||||
if (!ring)
|
||||
goto cleanup;
|
||||
|
||||
printf("dns-egress attached cgroup=%s domain=%s resolver=%s tcp_port=%u dns_port=%u\n",
|
||||
options.cgroup_path, options.domain, options.dns_server,
|
||||
options.port, options.dns_port);
|
||||
if (options.demo) {
|
||||
if (run_demo(ring, &options, qname, qname_length))
|
||||
goto cleanup;
|
||||
} else {
|
||||
signal(SIGINT, handle_signal);
|
||||
signal(SIGTERM, handle_signal);
|
||||
if (options.duration_seconds)
|
||||
deadline = monotonic_ns() +
|
||||
(unsigned long long)options.duration_seconds *
|
||||
1000000000ULL;
|
||||
while (!stop && (!deadline || monotonic_ns() < deadline)) {
|
||||
int poll_result = ring_buffer__poll(ring, 100);
|
||||
|
||||
if (poll_result < 0 && poll_result != -EINTR) {
|
||||
fprintf(stderr, "ring buffer poll failed: %d\n",
|
||||
poll_result);
|
||||
goto cleanup;
|
||||
}
|
||||
}
|
||||
}
|
||||
err = 0;
|
||||
|
||||
cleanup:
|
||||
ring_buffer__free(ring);
|
||||
bpf_link__destroy(connect_link);
|
||||
bpf_link__destroy(ingress_link);
|
||||
bpf_link__destroy(query_link);
|
||||
if (cgroup_fd >= 0) close(cgroup_fd);
|
||||
dns_egress_bpf__destroy(skel);
|
||||
return err;
|
||||
}
|
||||
46
src/55-dns-egress/dns_egress.h
Normal file
46
src/55-dns-egress/dns_egress.h
Normal file
@@ -0,0 +1,46 @@
|
||||
/* SPDX-License-Identifier: (LGPL-2.1 OR BSD-2-Clause) */
|
||||
#ifndef __DNS_EGRESS_H
|
||||
#define __DNS_EGRESS_H
|
||||
|
||||
#define DNS_QNAME_MAX 64
|
||||
|
||||
enum dns_egress_event_type {
|
||||
DNS_LEARNED = 1,
|
||||
DNS_ALLOWED = 2,
|
||||
DNS_DENIED = 3,
|
||||
DNS_EXPIRED = 4,
|
||||
};
|
||||
|
||||
struct dns_header {
|
||||
unsigned short id;
|
||||
unsigned short flags;
|
||||
unsigned short questions;
|
||||
unsigned short answers;
|
||||
unsigned short authorities;
|
||||
unsigned short additionals;
|
||||
} __attribute__((packed));
|
||||
|
||||
struct dns_question {
|
||||
unsigned short type;
|
||||
unsigned short class;
|
||||
} __attribute__((packed));
|
||||
|
||||
struct dns_a_answer {
|
||||
unsigned short name;
|
||||
unsigned short type;
|
||||
unsigned short class;
|
||||
unsigned int ttl;
|
||||
unsigned short address_length;
|
||||
unsigned int address;
|
||||
} __attribute__((packed));
|
||||
|
||||
struct dns_egress_event {
|
||||
unsigned long long timestamp_ns;
|
||||
unsigned long long expires_ns;
|
||||
unsigned int type;
|
||||
unsigned int pid;
|
||||
unsigned int ip4;
|
||||
unsigned int ttl_seconds;
|
||||
};
|
||||
|
||||
#endif /* __DNS_EGRESS_H */
|
||||
29
src/55-dns-egress/tests/test_dns_egress.py
Normal file
29
src/55-dns-egress/tests/test_dns_egress.py
Normal file
@@ -0,0 +1,29 @@
|
||||
#!/usr/bin/env python3
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
|
||||
def main():
|
||||
result = subprocess.run(
|
||||
[sys.argv[1], "--demo"],
|
||||
text=True,
|
||||
capture_output=True,
|
||||
check=False,
|
||||
timeout=30,
|
||||
)
|
||||
output = result.stdout + result.stderr
|
||||
if result.returncode:
|
||||
raise AssertionError(output)
|
||||
assert "demo step=before-dns result=blocked" in output, output
|
||||
assert "demo step=unsolicited-response result=blocked" in output, output
|
||||
assert "demo step=wrong-transaction-id result=blocked" in output, output
|
||||
assert "event=learned" in output and "ip=127.0.0.1 ttl=1" in output, output
|
||||
assert "demo step=live-answer result=allowed" in output, output
|
||||
assert "event=expired" in output, output
|
||||
assert "demo step=expired-answer result=blocked" in output, output
|
||||
assert output.count("event=denied") == 4, output
|
||||
print("DNS-aware egress integration test: PASS")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
2
src/56-slow-syscall-index/.gitignore
vendored
Normal file
2
src/56-slow-syscall-index/.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
.output/
|
||||
slow_syscall_index
|
||||
94
src/56-slow-syscall-index/Makefile
Normal file
94
src/56-slow-syscall-index/Makefile
Normal file
@@ -0,0 +1,94 @@
|
||||
# SPDX-License-Identifier: (LGPL-2.1 OR BSD-2-Clause)
|
||||
OUTPUT := .output
|
||||
CLANG ?= clang
|
||||
LIBBPF_SRC := $(abspath ../third_party/libbpf/src)
|
||||
BPFTOOL_SRC := $(abspath ../third_party/bpftool/src)
|
||||
LIBBPF_OBJ := $(abspath $(OUTPUT)/libbpf.a)
|
||||
BPFTOOL_OUTPUT ?= $(abspath $(OUTPUT)/bpftool)
|
||||
BPFTOOL ?= $(BPFTOOL_OUTPUT)/bootstrap/bpftool
|
||||
ARCH ?= $(shell uname -m | sed 's/x86_64/x86/' \
|
||||
| sed 's/arm.*/arm/' \
|
||||
| sed 's/aarch64/arm64/' \
|
||||
| sed 's/ppc64le/powerpc/' \
|
||||
| sed 's/mips.*/mips/' \
|
||||
| sed 's/riscv64/riscv/' \
|
||||
| sed 's/loongarch64/loongarch/')
|
||||
VMLINUX := ../third_party/vmlinux/$(ARCH)/vmlinux.h
|
||||
INCLUDES := -I$(OUTPUT) -I../third_party/libbpf/include/uapi -I$(dir $(VMLINUX)) -I.
|
||||
CFLAGS := -g -Wall -Wextra -D_GNU_SOURCE
|
||||
ALL_LDFLAGS := $(LDFLAGS) $(EXTRA_LDFLAGS)
|
||||
APPS = slow_syscall_index
|
||||
|
||||
CLANG_BPF_SYS_INCLUDES ?= $(shell $(CLANG) -v -E - </dev/null 2>&1 \
|
||||
| sed -n '/<...> search starts here:/,/End of search list./{ s| \(/.*\)|-idirafter \1|p }')
|
||||
|
||||
ifeq ($(V),1)
|
||||
Q =
|
||||
msg =
|
||||
else
|
||||
Q = @
|
||||
msg = @printf ' %-8s %s%s\n' "$(1)" \
|
||||
"$(patsubst $(abspath $(OUTPUT))/%,%,$(2))" \
|
||||
"$(if $(3), $(3))";
|
||||
MAKEFLAGS += --no-print-directory
|
||||
endif
|
||||
|
||||
define allow-override
|
||||
$(if $(or $(findstring environment,$(origin $(1))),\
|
||||
$(findstring command line,$(origin $(1)))),,\
|
||||
$(eval $(1) = $(2)))
|
||||
endef
|
||||
|
||||
$(call allow-override,CC,$(CROSS_COMPILE)cc)
|
||||
|
||||
.PHONY: all clean test
|
||||
all: $(APPS)
|
||||
|
||||
clean:
|
||||
$(call msg,CLEAN)
|
||||
$(Q)rm -rf $(OUTPUT) $(APPS)
|
||||
|
||||
test: all
|
||||
@if [ "$$(id -u)" -ne 0 ]; then \
|
||||
echo "test requires root; run inside the documented KVM guest" >&2; \
|
||||
exit 1; \
|
||||
fi
|
||||
python3 tests/test_slow_syscall_index.py ./slow_syscall_index
|
||||
|
||||
$(OUTPUT) $(OUTPUT)/libbpf $(BPFTOOL_OUTPUT):
|
||||
$(call msg,MKDIR,$@)
|
||||
$(Q)mkdir -p $@
|
||||
|
||||
$(LIBBPF_OBJ): $(wildcard $(LIBBPF_SRC)/*.[ch] $(LIBBPF_SRC)/Makefile) | $(OUTPUT)/libbpf
|
||||
$(call msg,LIB,$@)
|
||||
$(Q)$(MAKE) -C $(LIBBPF_SRC) BUILD_STATIC_ONLY=1 \
|
||||
OBJDIR=$(dir $@)/libbpf DESTDIR=$(dir $@) \
|
||||
INCLUDEDIR= LIBDIR= UAPIDIR= install
|
||||
|
||||
$(BPFTOOL): | $(BPFTOOL_OUTPUT)
|
||||
$(call msg,BPFTOOL,$@)
|
||||
$(Q)$(MAKE) ARCH= CROSS_COMPILE= OUTPUT=$(BPFTOOL_OUTPUT)/ -C $(BPFTOOL_SRC) bootstrap
|
||||
|
||||
$(OUTPUT)/%.bpf.o: %.bpf.c $(LIBBPF_OBJ) $(wildcard %.h) $(VMLINUX) | $(OUTPUT) $(BPFTOOL)
|
||||
$(call msg,BPF,$@)
|
||||
$(Q)$(CLANG) -g -O2 -target bpf -D__TARGET_ARCH_$(ARCH) \
|
||||
$(INCLUDES) $(CLANG_BPF_SYS_INCLUDES) \
|
||||
-c $(filter %.c,$^) -o $(patsubst %.bpf.o,%.tmp.bpf.o,$@)
|
||||
$(Q)$(BPFTOOL) gen object $@ $(patsubst %.bpf.o,%.tmp.bpf.o,$@)
|
||||
|
||||
$(OUTPUT)/%.skel.h: $(OUTPUT)/%.bpf.o | $(OUTPUT) $(BPFTOOL)
|
||||
$(call msg,GEN-SKEL,$@)
|
||||
$(Q)$(BPFTOOL) gen skeleton $< > $@
|
||||
|
||||
$(patsubst %,$(OUTPUT)/%.o,$(APPS)): %.o: %.skel.h
|
||||
|
||||
$(OUTPUT)/%.o: %.c $(wildcard %.h) | $(OUTPUT)
|
||||
$(call msg,CC,$@)
|
||||
$(Q)$(CC) $(CFLAGS) $(INCLUDES) -c $(filter %.c,$^) -o $@
|
||||
|
||||
$(APPS): %: $(OUTPUT)/%.o $(LIBBPF_OBJ) | $(OUTPUT)
|
||||
$(call msg,BINARY,$@)
|
||||
$(Q)$(CC) $(CFLAGS) $^ $(ALL_LDFLAGS) -lelf -lz -o $@
|
||||
|
||||
.DELETE_ON_ERROR:
|
||||
.SECONDARY:
|
||||
604
src/56-slow-syscall-index/README.md
Normal file
604
src/56-slow-syscall-index/README.md
Normal file
@@ -0,0 +1,604 @@
|
||||
# eBPF Tutorial by Example: Slow Syscall Latency Index
|
||||
|
||||
Which syscalls are taking the longest in your application? This tutorial builds a tool that traces syscall latency per process using BPF task local storage, aggregates the results by TGID and syscall number, and ranks them by total latency. The result shows which recorded slow syscalls account for most of the observed syscall latency.
|
||||
|
||||
> Complete source code: <https://github.com/eunomia-bpf/bpf-developer-tutorial/tree/main/src/56-slow-syscall-index>
|
||||
|
||||
## eBPF and Syscall Tracing
|
||||
|
||||
eBPF lets verified programs run at Linux kernel hooks and send selected state to user space. This tutorial uses the `raw_syscalls/sys_enter` and `raw_syscalls/sys_exit` tracepoints, which fire on every syscall entry and exit system-wide. To correlate entry with exit, we use BPF task local storage, a map type introduced in Linux 5.11 that attaches storage directly to each task struct. The kernel removes that storage when the task exits, which fits per-thread state with task lifetime. The measurement is syscall enter-to-exit wall-clock latency rather than scheduler-only blocked time. This implementation requires Linux 5.11 or later.
|
||||
|
||||
## How the Implementation Works
|
||||
|
||||
The tool attaches to the raw syscall entry and exit tracepoints. On entry, it records the start timestamp and syscall ID in task local storage. On exit, it calculates the duration and reports events exceeding the configured threshold. User space aggregates events by TGID and syscall, tracking count, total latency, maximum latency, and error count.
|
||||
|
||||
## Header File
|
||||
|
||||
```c
|
||||
/* SPDX-License-Identifier: (LGPL-2.1 OR BSD-2-Clause) */
|
||||
#ifndef __SLOW_SYSCALL_INDEX_H
|
||||
#define __SLOW_SYSCALL_INDEX_H
|
||||
|
||||
#define SLOW_SYSCALL_COMM_LEN 16
|
||||
|
||||
struct slow_syscall_event {
|
||||
unsigned long long timestamp_ns;
|
||||
unsigned long long duration_ns;
|
||||
long long return_value;
|
||||
unsigned int tgid;
|
||||
unsigned int tid;
|
||||
unsigned int syscall_id;
|
||||
char comm[SLOW_SYSCALL_COMM_LEN];
|
||||
};
|
||||
|
||||
#endif /* __SLOW_SYSCALL_INDEX_H */
|
||||
```
|
||||
|
||||
The header defines the event structure sent to user space via ring buffer. Each event includes the timestamp, duration, return value, TGID, TID, syscall number, and process name.
|
||||
|
||||
## BPF Program
|
||||
|
||||
```c
|
||||
// SPDX-License-Identifier: GPL-2.0
|
||||
#include "vmlinux.h"
|
||||
#include <bpf/bpf_helpers.h>
|
||||
#include "slow_syscall_index.h"
|
||||
|
||||
char LICENSE[] SEC("license") = "GPL";
|
||||
|
||||
const volatile __u64 minimum_ns = 10 * 1000 * 1000ULL;
|
||||
const volatile __u32 target_tgid;
|
||||
const volatile __u32 ignored_tgid;
|
||||
|
||||
struct syscall_state {
|
||||
__u64 started_ns;
|
||||
__u32 syscall_id;
|
||||
__u32 pad;
|
||||
};
|
||||
|
||||
struct {
|
||||
__uint(type, BPF_MAP_TYPE_TASK_STORAGE);
|
||||
__uint(map_flags, BPF_F_NO_PREALLOC);
|
||||
__type(key, int);
|
||||
__type(value, struct syscall_state);
|
||||
} active_syscalls SEC(".maps");
|
||||
|
||||
struct {
|
||||
__uint(type, BPF_MAP_TYPE_RINGBUF);
|
||||
__uint(max_entries, 256 * 1024);
|
||||
} events SEC(".maps");
|
||||
|
||||
__u64 completed_syscalls;
|
||||
__u64 slow_syscalls;
|
||||
__u64 dropped_events;
|
||||
|
||||
SEC("tp/raw_syscalls/sys_enter")
|
||||
int record_syscall_entry(struct trace_event_raw_sys_enter *ctx)
|
||||
{
|
||||
struct task_struct *task;
|
||||
struct syscall_state *state;
|
||||
__u64 pid_tgid = bpf_get_current_pid_tgid();
|
||||
__u32 tgid = pid_tgid >> 32;
|
||||
|
||||
if (tgid == ignored_tgid || (target_tgid && tgid != target_tgid))
|
||||
return 0;
|
||||
task = bpf_get_current_task_btf();
|
||||
state = bpf_task_storage_get(&active_syscalls, task, 0,
|
||||
BPF_LOCAL_STORAGE_GET_F_CREATE);
|
||||
if (!state)
|
||||
return 0;
|
||||
state->started_ns = bpf_ktime_get_ns();
|
||||
state->syscall_id = ctx->id;
|
||||
return 0;
|
||||
}
|
||||
|
||||
SEC("tp/raw_syscalls/sys_exit")
|
||||
int report_slow_syscall(struct trace_event_raw_sys_exit *ctx)
|
||||
{
|
||||
struct task_struct *task = bpf_get_current_task_btf();
|
||||
struct slow_syscall_event *event;
|
||||
struct syscall_state *state;
|
||||
__u64 pid_tgid = bpf_get_current_pid_tgid();
|
||||
__u64 duration_ns;
|
||||
|
||||
state = bpf_task_storage_get(&active_syscalls, task, 0, 0);
|
||||
if (!state)
|
||||
return 0;
|
||||
duration_ns = bpf_ktime_get_ns() - state->started_ns;
|
||||
__sync_fetch_and_add(&completed_syscalls, 1);
|
||||
if (duration_ns < minimum_ns)
|
||||
goto out;
|
||||
|
||||
__sync_fetch_and_add(&slow_syscalls, 1);
|
||||
event = bpf_ringbuf_reserve(&events, sizeof(*event), 0);
|
||||
if (!event) {
|
||||
__sync_fetch_and_add(&dropped_events, 1);
|
||||
goto out;
|
||||
}
|
||||
event->timestamp_ns = bpf_ktime_get_ns();
|
||||
event->duration_ns = duration_ns;
|
||||
event->return_value = ctx->ret;
|
||||
event->tgid = pid_tgid >> 32;
|
||||
event->tid = (__u32)pid_tgid;
|
||||
event->syscall_id = state->syscall_id;
|
||||
bpf_get_current_comm(event->comm, sizeof(event->comm));
|
||||
bpf_ringbuf_submit(event, 0);
|
||||
out:
|
||||
bpf_task_storage_delete(&active_syscalls, task);
|
||||
return 0;
|
||||
}
|
||||
```
|
||||
|
||||
The BPF program uses `BPF_MAP_TYPE_TASK_STORAGE` to store syscall state per task. This map type was introduced in Linux 5.11 and provides storage that is automatically cleaned up when the task exits.
|
||||
|
||||
The `record_syscall_entry` function runs on syscall entry, storing the start timestamp and syscall ID. It filters by target TGID and ignores the tracer process itself.
|
||||
|
||||
The `report_slow_syscall` function runs on syscall exit. It calculates the duration by subtracting the start timestamp from the current time. If the duration exceeds the threshold, it reserves space in the ring buffer, fills in the event, and submits it. The task storage is deleted after each syscall to keep memory usage bounded.
|
||||
|
||||
The program tracks three global counters: `completed_syscalls` counts all traced syscalls, `slow_syscalls` counts those exceeding the threshold, and `dropped_events` counts ring buffer allocation failures.
|
||||
|
||||
## User Space Program
|
||||
|
||||
```c
|
||||
// SPDX-License-Identifier: (LGPL-2.1 OR BSD-2-Clause)
|
||||
#include <errno.h>
|
||||
#include <getopt.h>
|
||||
#include <signal.h>
|
||||
#include <stdbool.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <sys/syscall.h>
|
||||
#include <sys/types.h>
|
||||
#include <sys/wait.h>
|
||||
#include <time.h>
|
||||
#include <unistd.h>
|
||||
#include <bpf/libbpf.h>
|
||||
#include "slow_syscall_index.h"
|
||||
#include "slow_syscall_index.skel.h"
|
||||
|
||||
#define MAX_STATS 512
|
||||
|
||||
struct options {
|
||||
unsigned int pid;
|
||||
unsigned int minimum_ms;
|
||||
unsigned int duration_seconds;
|
||||
unsigned int top;
|
||||
bool demo;
|
||||
};
|
||||
|
||||
struct syscall_stats {
|
||||
unsigned int tgid;
|
||||
unsigned int syscall_id;
|
||||
unsigned long long count;
|
||||
unsigned long long total_ns;
|
||||
unsigned long long maximum_ns;
|
||||
unsigned long long errors;
|
||||
char comm[SLOW_SYSCALL_COMM_LEN];
|
||||
};
|
||||
|
||||
static struct syscall_stats stats[MAX_STATS];
|
||||
static size_t stats_count;
|
||||
static volatile sig_atomic_t stop;
|
||||
static unsigned int received_events;
|
||||
static unsigned int unaggregated_events;
|
||||
|
||||
static void handle_signal(int signal_number)
|
||||
{
|
||||
(void)signal_number;
|
||||
stop = 1;
|
||||
}
|
||||
|
||||
static unsigned long long monotonic_ns(void)
|
||||
{
|
||||
struct timespec now;
|
||||
|
||||
clock_gettime(CLOCK_MONOTONIC, &now);
|
||||
return (unsigned long long)now.tv_sec * 1000000000ULL + now.tv_nsec;
|
||||
}
|
||||
|
||||
static const char *syscall_name(unsigned int id)
|
||||
{
|
||||
switch (id) {
|
||||
#ifdef __NR_read
|
||||
case __NR_read: return "read";
|
||||
#endif
|
||||
#ifdef __NR_write
|
||||
case __NR_write: return "write";
|
||||
#endif
|
||||
#ifdef __NR_openat
|
||||
case __NR_openat: return "openat";
|
||||
#endif
|
||||
#ifdef __NR_close
|
||||
case __NR_close: return "close";
|
||||
#endif
|
||||
#ifdef __NR_fsync
|
||||
case __NR_fsync: return "fsync";
|
||||
#endif
|
||||
#ifdef __NR_fdatasync
|
||||
case __NR_fdatasync: return "fdatasync";
|
||||
#endif
|
||||
#ifdef __NR_poll
|
||||
case __NR_poll: return "poll";
|
||||
#endif
|
||||
#ifdef __NR_ppoll
|
||||
case __NR_ppoll: return "ppoll";
|
||||
#endif
|
||||
#ifdef __NR_epoll_wait
|
||||
case __NR_epoll_wait: return "epoll_wait";
|
||||
#endif
|
||||
#ifdef __NR_epoll_pwait
|
||||
case __NR_epoll_pwait: return "epoll_pwait";
|
||||
#endif
|
||||
#ifdef __NR_futex
|
||||
case __NR_futex: return "futex";
|
||||
#endif
|
||||
#ifdef __NR_nanosleep
|
||||
case __NR_nanosleep: return "nanosleep";
|
||||
#endif
|
||||
#ifdef __NR_clock_nanosleep
|
||||
case __NR_clock_nanosleep: return "clock_nanosleep";
|
||||
#endif
|
||||
#ifdef __NR_connect
|
||||
case __NR_connect: return "connect";
|
||||
#endif
|
||||
#ifdef __NR_accept
|
||||
case __NR_accept: return "accept";
|
||||
#endif
|
||||
#ifdef __NR_accept4
|
||||
case __NR_accept4: return "accept4";
|
||||
#endif
|
||||
#ifdef __NR_recvfrom
|
||||
case __NR_recvfrom: return "recvfrom";
|
||||
#endif
|
||||
#ifdef __NR_recvmsg
|
||||
case __NR_recvmsg: return "recvmsg";
|
||||
#endif
|
||||
#ifdef __NR_sendto
|
||||
case __NR_sendto: return "sendto";
|
||||
#endif
|
||||
#ifdef __NR_sendmsg
|
||||
case __NR_sendmsg: return "sendmsg";
|
||||
#endif
|
||||
default: return "unknown";
|
||||
}
|
||||
}
|
||||
|
||||
static struct syscall_stats *get_stats(const struct slow_syscall_event *event)
|
||||
{
|
||||
struct syscall_stats *entry;
|
||||
|
||||
for (size_t i = 0; i < stats_count; i++)
|
||||
if (stats[i].tgid == event->tgid &&
|
||||
stats[i].syscall_id == event->syscall_id)
|
||||
return &stats[i];
|
||||
if (stats_count == MAX_STATS)
|
||||
return NULL;
|
||||
entry = &stats[stats_count++];
|
||||
entry->tgid = event->tgid;
|
||||
entry->syscall_id = event->syscall_id;
|
||||
memcpy(entry->comm, event->comm, sizeof(entry->comm));
|
||||
entry->comm[sizeof(entry->comm) - 1] = '\0';
|
||||
return entry;
|
||||
}
|
||||
|
||||
static int handle_event(void *ctx, void *data, size_t size)
|
||||
{
|
||||
const struct slow_syscall_event *event = data;
|
||||
struct syscall_stats *entry;
|
||||
|
||||
(void)ctx;
|
||||
if (size != sizeof(*event))
|
||||
return 0;
|
||||
entry = get_stats(event);
|
||||
if (entry) {
|
||||
entry->count++;
|
||||
entry->total_ns += event->duration_ns;
|
||||
if (event->duration_ns > entry->maximum_ns)
|
||||
entry->maximum_ns = event->duration_ns;
|
||||
if (event->return_value < 0)
|
||||
entry->errors++;
|
||||
} else {
|
||||
unaggregated_events++;
|
||||
}
|
||||
received_events++;
|
||||
printf("pid=%u tid=%u comm=%s syscall=%s(%u) latency_ms=%.3f return=%lld\n",
|
||||
event->tgid, event->tid, event->comm,
|
||||
syscall_name(event->syscall_id), event->syscall_id,
|
||||
event->duration_ns / 1000000.0, event->return_value);
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int compare_stats(const void *left, const void *right)
|
||||
{
|
||||
const struct syscall_stats *a = left;
|
||||
const struct syscall_stats *b = right;
|
||||
|
||||
return a->total_ns < b->total_ns ? 1 : a->total_ns > b->total_ns ? -1 : 0;
|
||||
}
|
||||
|
||||
static void print_summary(unsigned int top)
|
||||
{
|
||||
size_t limit;
|
||||
|
||||
qsort(stats, stats_count, sizeof(stats[0]), compare_stats);
|
||||
limit = stats_count < top ? stats_count : top;
|
||||
printf("\nSlow syscall index, ranked by total latency:\n");
|
||||
printf("%-7s %-16s %-18s %8s %12s %12s %8s\n",
|
||||
"PID", "COMM", "SYSCALL", "COUNT", "TOTAL_MS", "MAX_MS", "ERRORS");
|
||||
for (size_t i = 0; i < limit; i++) {
|
||||
const struct syscall_stats *entry = &stats[i];
|
||||
|
||||
printf("%-7u %-16s %-12s(%3u) %8llu %12.3f %12.3f %8llu\n",
|
||||
entry->tgid, entry->comm, syscall_name(entry->syscall_id),
|
||||
entry->syscall_id, entry->count,
|
||||
entry->total_ns / 1000000.0,
|
||||
entry->maximum_ns / 1000000.0, entry->errors);
|
||||
}
|
||||
}
|
||||
|
||||
static int parse_uint(const char *text, unsigned int maximum,
|
||||
unsigned int *value)
|
||||
{
|
||||
char *end = NULL;
|
||||
unsigned long parsed;
|
||||
|
||||
errno = 0;
|
||||
parsed = strtoul(text, &end, 10);
|
||||
if (errno || !*text || *end || parsed > maximum)
|
||||
return -1;
|
||||
*value = parsed;
|
||||
return 0;
|
||||
}
|
||||
|
||||
static void usage(const char *program)
|
||||
{
|
||||
printf("Usage: %s [--pid PID] [--min-ms MS] [--duration SEC] [--top N] [--demo]\n",
|
||||
program);
|
||||
}
|
||||
|
||||
static int parse_options(int argc, char **argv, struct options *options)
|
||||
{
|
||||
static const struct option long_options[] = {
|
||||
{ "pid", required_argument, NULL, 'p' },
|
||||
{ "min-ms", required_argument, NULL, 'm' },
|
||||
{ "duration", required_argument, NULL, 'd' },
|
||||
{ "top", required_argument, NULL, 't' },
|
||||
{ "demo", no_argument, NULL, 'D' },
|
||||
{ "help", no_argument, NULL, 'h' },
|
||||
{},
|
||||
};
|
||||
int option;
|
||||
|
||||
while ((option = getopt_long(argc, argv, "p:m:d:t:Dh", long_options,
|
||||
NULL)) != -1) {
|
||||
switch (option) {
|
||||
case 'p':
|
||||
if (parse_uint(optarg, 0xffffffffU, &options->pid))
|
||||
return -1;
|
||||
break;
|
||||
case 'm':
|
||||
if (parse_uint(optarg, 60000, &options->minimum_ms))
|
||||
return -1;
|
||||
break;
|
||||
case 'd':
|
||||
if (parse_uint(optarg, 86400, &options->duration_seconds))
|
||||
return -1;
|
||||
break;
|
||||
case 't':
|
||||
if (parse_uint(optarg, MAX_STATS, &options->top))
|
||||
return -1;
|
||||
break;
|
||||
case 'D': options->demo = true; break;
|
||||
case 'h': usage(argv[0]); exit(0);
|
||||
default: return -1;
|
||||
}
|
||||
}
|
||||
return optind == argc && options->minimum_ms && options->top ? 0 : -1;
|
||||
}
|
||||
|
||||
static pid_t start_demo(int gate[2], int data_pipe[2])
|
||||
{
|
||||
pid_t child = fork();
|
||||
char byte;
|
||||
|
||||
if (child)
|
||||
return child;
|
||||
close(gate[1]);
|
||||
close(data_pipe[1]);
|
||||
if (read(gate[0], &byte, 1) != 1)
|
||||
_exit(2);
|
||||
if (read(data_pipe[0], &byte, 1) != 1)
|
||||
_exit(3);
|
||||
_exit(0);
|
||||
}
|
||||
|
||||
int main(int argc, char **argv)
|
||||
{
|
||||
struct options options = { .minimum_ms = 10, .top = 10 };
|
||||
struct slow_syscall_index_bpf *skel = NULL;
|
||||
struct ring_buffer *ring = NULL;
|
||||
unsigned long long deadline = 0;
|
||||
int gate[2] = { -1, -1 }, data_pipe[2] = { -1, -1 };
|
||||
pid_t demo_child = -1;
|
||||
int demo_status = 0;
|
||||
int err = 1;
|
||||
|
||||
setvbuf(stdout, NULL, _IONBF, 0);
|
||||
if (parse_options(argc, argv, &options)) {
|
||||
usage(argv[0]);
|
||||
return 2;
|
||||
}
|
||||
if (options.demo) {
|
||||
if (options.pid) {
|
||||
fprintf(stderr, "--demo and --pid cannot be combined\n");
|
||||
return 2;
|
||||
}
|
||||
if (pipe(gate) || pipe(data_pipe)) {
|
||||
perror("pipe");
|
||||
goto cleanup;
|
||||
}
|
||||
demo_child = start_demo(gate, data_pipe);
|
||||
if (demo_child < 0) {
|
||||
perror("fork");
|
||||
goto cleanup;
|
||||
}
|
||||
options.pid = demo_child;
|
||||
}
|
||||
|
||||
skel = slow_syscall_index_bpf__open();
|
||||
if (!skel)
|
||||
goto cleanup;
|
||||
skel->rodata->minimum_ns = (unsigned long long)options.minimum_ms * 1000000ULL;
|
||||
skel->rodata->target_tgid = options.pid;
|
||||
skel->rodata->ignored_tgid = getpid();
|
||||
if (slow_syscall_index_bpf__load(skel) ||
|
||||
slow_syscall_index_bpf__attach(skel)) {
|
||||
fprintf(stderr, "failed to load and attach slow syscall tracer\n");
|
||||
goto cleanup;
|
||||
}
|
||||
ring = ring_buffer__new(bpf_map__fd(skel->maps.events), handle_event,
|
||||
NULL, NULL);
|
||||
if (!ring)
|
||||
goto cleanup;
|
||||
|
||||
signal(SIGINT, handle_signal);
|
||||
signal(SIGTERM, handle_signal);
|
||||
if (options.pid)
|
||||
printf("Tracing PID %u syscalls slower than %u ms. Press Ctrl-C to stop.\n",
|
||||
options.pid, options.minimum_ms);
|
||||
else
|
||||
printf("Tracing all syscalls slower than %u ms. Press Ctrl-C to stop.\n",
|
||||
options.minimum_ms);
|
||||
if (options.duration_seconds)
|
||||
deadline = monotonic_ns() +
|
||||
(unsigned long long)options.duration_seconds * 1000000000ULL;
|
||||
|
||||
if (options.demo) {
|
||||
char byte = 'x';
|
||||
struct timespec delay = {
|
||||
.tv_sec = options.minimum_ms / 1000,
|
||||
.tv_nsec = (long)(options.minimum_ms % 1000 + 20) * 1000000L,
|
||||
};
|
||||
|
||||
if (delay.tv_nsec >= 1000000000L) {
|
||||
delay.tv_sec++;
|
||||
delay.tv_nsec -= 1000000000L;
|
||||
}
|
||||
close(gate[0]); gate[0] = -1;
|
||||
close(data_pipe[0]); data_pipe[0] = -1;
|
||||
write(gate[1], &byte, 1);
|
||||
nanosleep(&delay, NULL);
|
||||
write(data_pipe[1], &byte, 1);
|
||||
waitpid(demo_child, &demo_status, 0);
|
||||
demo_child = -1;
|
||||
for (int i = 0; i < 20 && !received_events; i++)
|
||||
ring_buffer__poll(ring, 50);
|
||||
} else {
|
||||
while (!stop && (!deadline || monotonic_ns() < deadline)) {
|
||||
int poll_result = ring_buffer__poll(ring, 100);
|
||||
|
||||
if (poll_result < 0 && poll_result != -EINTR) {
|
||||
fprintf(stderr, "ring buffer poll failed: %d\n", poll_result);
|
||||
goto cleanup;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
print_summary(options.top);
|
||||
printf("completed=%llu slow=%llu dropped=%llu unaggregated=%u\n",
|
||||
(unsigned long long)skel->bss->completed_syscalls,
|
||||
(unsigned long long)skel->bss->slow_syscalls,
|
||||
(unsigned long long)skel->bss->dropped_events,
|
||||
unaggregated_events);
|
||||
if (options.demo && (!WIFEXITED(demo_status) || WEXITSTATUS(demo_status) ||
|
||||
!received_events))
|
||||
goto cleanup;
|
||||
err = 0;
|
||||
|
||||
cleanup:
|
||||
if (demo_child > 0) {
|
||||
kill(demo_child, SIGKILL);
|
||||
waitpid(demo_child, NULL, 0);
|
||||
}
|
||||
for (size_t i = 0; i < 2; i++) {
|
||||
if (gate[i] >= 0) close(gate[i]);
|
||||
if (data_pipe[i] >= 0) close(data_pipe[i]);
|
||||
}
|
||||
ring_buffer__free(ring);
|
||||
slow_syscall_index_bpf__destroy(skel);
|
||||
return err;
|
||||
}
|
||||
```
|
||||
|
||||
The user space program aggregates events by TGID and syscall number. Each event updates the count, total latency, maximum latency, and error count for that combination. On exit, it sorts the results by total latency and prints the top N entries.
|
||||
|
||||
The demo mode forks a child process that blocks on a pipe read. The parent delays long enough to exceed the threshold, then writes to unblock the child. This creates a guaranteed slow syscall for testing.
|
||||
|
||||
## Compilation and Execution
|
||||
|
||||
Build the tool:
|
||||
|
||||
```bash
|
||||
cd src/56-slow-syscall-index
|
||||
make
|
||||
```
|
||||
|
||||
Run with options:
|
||||
|
||||
```bash
|
||||
sudo ./slow_syscall_index --pid 1234 --min-ms 10 --duration 60 --top 20
|
||||
```
|
||||
|
||||
Or run the built-in demo:
|
||||
|
||||
```bash
|
||||
sudo ./slow_syscall_index --demo --min-ms 10 --top 10
|
||||
```
|
||||
|
||||
Example output:
|
||||
|
||||
```text
|
||||
Tracing PID 1255 syscalls slower than 10 ms. Press Ctrl-C to stop.
|
||||
pid=1255 tid=1255 comm=slow_syscall_in syscall=read(0) latency_ms=30.080 return=1
|
||||
|
||||
Slow syscall index, ranked by total latency:
|
||||
PID COMM SYSCALL COUNT TOTAL_MS MAX_MS ERRORS
|
||||
1255 slow_syscall_in read ( 0) 1 30.080 30.080 0
|
||||
completed=1 slow=1 dropped=0 unaggregated=0
|
||||
```
|
||||
|
||||
## Requirements
|
||||
|
||||
| Requirement | Details |
|
||||
|-------------|---------|
|
||||
| Kernel | Linux 5.11+ (BPF task local storage) |
|
||||
| Config | `CONFIG_BPF_SYSCALL`, `CONFIG_DEBUG_INFO_BTF` |
|
||||
| Privileges | Root |
|
||||
|
||||
## Understanding the Output
|
||||
|
||||
The index shows:
|
||||
|
||||
- **PID**: Thread group ID (the process ID carried in the event)
|
||||
- **COMM**: Process name
|
||||
- **SYSCALL**: Syscall name and number
|
||||
- **COUNT**: Number of slow invocations
|
||||
- **TOTAL_MS**: Sum of all slow syscall durations
|
||||
- **MAX_MS**: Longest single invocation
|
||||
- **ERRORS**: Count of invocations returning negative values
|
||||
|
||||
The `completed` counter shows all traced syscalls, `slow` shows those exceeding the threshold, and `dropped` shows ring buffer allocation failures. User space keeps 512 `(TGID, syscall)` groups; events beyond that table capacity are still received and counted as `unaggregated`.
|
||||
|
||||
## Summary
|
||||
|
||||
Task local storage joins each syscall exit to its matching entry without a user-managed PID map. Kernel-side thresholding keeps the event stream focused, and user-space aggregation turns the remaining completions into a ranked latency index. The result measures wall-clock syscall latency; scheduler correlation can be added when off-CPU attribution is needed.
|
||||
|
||||
> If you'd like to dive deeper into eBPF, check out our tutorial repository at <https://github.com/eunomia-bpf/bpf-developer-tutorial> or visit our website at <https://eunomia.dev/tutorials/>.
|
||||
|
||||
## References
|
||||
|
||||
- [BPF task local storage documentation](https://docs.ebpf.io/linux/map-type/BPF_MAP_TYPE_TASK_STORAGE/)
|
||||
- [BPF ring buffer documentation](https://docs.kernel.org/6.6/bpf/ringbuf.html)
|
||||
- [raw_syscalls tracepoints](https://github.com/torvalds/linux/blob/master/include/trace/events/syscalls.h)
|
||||
- [Task local storage commit](https://github.com/torvalds/linux/commit/4cf1bc1f1045)
|
||||
604
src/56-slow-syscall-index/README.zh.md
Normal file
604
src/56-slow-syscall-index/README.zh.md
Normal file
@@ -0,0 +1,604 @@
|
||||
# eBPF 实战教程:慢系统调用延迟索引
|
||||
|
||||
应用程序中哪些系统调用耗时最长?本教程构建一个工具,使用 BPF 任务本地存储跟踪每个进程的系统调用延迟,按 TGID 和系统调用号聚合结果,并按总延迟排名,最终找出哪些慢系统调用占据了最多的观测延迟。
|
||||
|
||||
> 完整源代码:<https://github.com/eunomia-bpf/bpf-developer-tutorial/tree/main/src/56-slow-syscall-index>
|
||||
|
||||
## eBPF 与系统调用跟踪
|
||||
|
||||
eBPF 让经过验证器检查的程序运行在 Linux 内核 hook 上,并把选出的状态发送到用户态。本教程使用 `raw_syscalls/sys_enter` 和 `raw_syscalls/sys_exit` 跟踪点,它们在系统范围内每次系统调用进入和退出时触发。为了关联进入和退出,我们使用 Linux 5.11 引入的 BPF 任务本地存储,把状态直接附加到 task 结构上,并由内核在任务退出时清理。这里测量的是系统调用从进入到退出的挂钟延迟,而不是只统计调度器阻塞时间,因此本例需要 Linux 5.11 或更高版本。
|
||||
|
||||
## 实现原理
|
||||
|
||||
该工具附加到原始系统调用进入和退出跟踪点。在进入时,它在任务本地存储中记录开始时间戳和系统调用 ID。在退出时,它计算持续时间并报告超过配置阈值的事件。用户空间按 TGID 和系统调用聚合事件,跟踪计数、总延迟、最大延迟和错误计数。
|
||||
|
||||
## 头文件
|
||||
|
||||
```c
|
||||
/* SPDX-License-Identifier: (LGPL-2.1 OR BSD-2-Clause) */
|
||||
#ifndef __SLOW_SYSCALL_INDEX_H
|
||||
#define __SLOW_SYSCALL_INDEX_H
|
||||
|
||||
#define SLOW_SYSCALL_COMM_LEN 16
|
||||
|
||||
struct slow_syscall_event {
|
||||
unsigned long long timestamp_ns;
|
||||
unsigned long long duration_ns;
|
||||
long long return_value;
|
||||
unsigned int tgid;
|
||||
unsigned int tid;
|
||||
unsigned int syscall_id;
|
||||
char comm[SLOW_SYSCALL_COMM_LEN];
|
||||
};
|
||||
|
||||
#endif /* __SLOW_SYSCALL_INDEX_H */
|
||||
```
|
||||
|
||||
头文件定义了通过 ring buffer 发送到用户空间的事件结构。每个事件包括时间戳、持续时间、返回值、TGID、TID、系统调用号和进程名。
|
||||
|
||||
## BPF 程序
|
||||
|
||||
```c
|
||||
// SPDX-License-Identifier: GPL-2.0
|
||||
#include "vmlinux.h"
|
||||
#include <bpf/bpf_helpers.h>
|
||||
#include "slow_syscall_index.h"
|
||||
|
||||
char LICENSE[] SEC("license") = "GPL";
|
||||
|
||||
const volatile __u64 minimum_ns = 10 * 1000 * 1000ULL;
|
||||
const volatile __u32 target_tgid;
|
||||
const volatile __u32 ignored_tgid;
|
||||
|
||||
struct syscall_state {
|
||||
__u64 started_ns;
|
||||
__u32 syscall_id;
|
||||
__u32 pad;
|
||||
};
|
||||
|
||||
struct {
|
||||
__uint(type, BPF_MAP_TYPE_TASK_STORAGE);
|
||||
__uint(map_flags, BPF_F_NO_PREALLOC);
|
||||
__type(key, int);
|
||||
__type(value, struct syscall_state);
|
||||
} active_syscalls SEC(".maps");
|
||||
|
||||
struct {
|
||||
__uint(type, BPF_MAP_TYPE_RINGBUF);
|
||||
__uint(max_entries, 256 * 1024);
|
||||
} events SEC(".maps");
|
||||
|
||||
__u64 completed_syscalls;
|
||||
__u64 slow_syscalls;
|
||||
__u64 dropped_events;
|
||||
|
||||
SEC("tp/raw_syscalls/sys_enter")
|
||||
int record_syscall_entry(struct trace_event_raw_sys_enter *ctx)
|
||||
{
|
||||
struct task_struct *task;
|
||||
struct syscall_state *state;
|
||||
__u64 pid_tgid = bpf_get_current_pid_tgid();
|
||||
__u32 tgid = pid_tgid >> 32;
|
||||
|
||||
if (tgid == ignored_tgid || (target_tgid && tgid != target_tgid))
|
||||
return 0;
|
||||
task = bpf_get_current_task_btf();
|
||||
state = bpf_task_storage_get(&active_syscalls, task, 0,
|
||||
BPF_LOCAL_STORAGE_GET_F_CREATE);
|
||||
if (!state)
|
||||
return 0;
|
||||
state->started_ns = bpf_ktime_get_ns();
|
||||
state->syscall_id = ctx->id;
|
||||
return 0;
|
||||
}
|
||||
|
||||
SEC("tp/raw_syscalls/sys_exit")
|
||||
int report_slow_syscall(struct trace_event_raw_sys_exit *ctx)
|
||||
{
|
||||
struct task_struct *task = bpf_get_current_task_btf();
|
||||
struct slow_syscall_event *event;
|
||||
struct syscall_state *state;
|
||||
__u64 pid_tgid = bpf_get_current_pid_tgid();
|
||||
__u64 duration_ns;
|
||||
|
||||
state = bpf_task_storage_get(&active_syscalls, task, 0, 0);
|
||||
if (!state)
|
||||
return 0;
|
||||
duration_ns = bpf_ktime_get_ns() - state->started_ns;
|
||||
__sync_fetch_and_add(&completed_syscalls, 1);
|
||||
if (duration_ns < minimum_ns)
|
||||
goto out;
|
||||
|
||||
__sync_fetch_and_add(&slow_syscalls, 1);
|
||||
event = bpf_ringbuf_reserve(&events, sizeof(*event), 0);
|
||||
if (!event) {
|
||||
__sync_fetch_and_add(&dropped_events, 1);
|
||||
goto out;
|
||||
}
|
||||
event->timestamp_ns = bpf_ktime_get_ns();
|
||||
event->duration_ns = duration_ns;
|
||||
event->return_value = ctx->ret;
|
||||
event->tgid = pid_tgid >> 32;
|
||||
event->tid = (__u32)pid_tgid;
|
||||
event->syscall_id = state->syscall_id;
|
||||
bpf_get_current_comm(event->comm, sizeof(event->comm));
|
||||
bpf_ringbuf_submit(event, 0);
|
||||
out:
|
||||
bpf_task_storage_delete(&active_syscalls, task);
|
||||
return 0;
|
||||
}
|
||||
```
|
||||
|
||||
BPF 程序使用 `BPF_MAP_TYPE_TASK_STORAGE` 存储每个任务的系统调用状态。这种映射类型在 Linux 5.11 中引入,提供在任务退出时自动清理的存储。
|
||||
|
||||
`record_syscall_entry` 函数在系统调用进入时运行,存储开始时间戳和系统调用 ID。它按目标 TGID 过滤并忽略跟踪器进程本身。
|
||||
|
||||
`report_slow_syscall` 函数在系统调用退出时运行。它通过从当前时间减去开始时间戳来计算持续时间。如果持续时间超过阈值,它在 ring buffer 中预留空间,填写事件并提交。每次系统调用后删除任务存储以保持内存使用有界。
|
||||
|
||||
程序跟踪三个全局计数器:`completed_syscalls` 计数所有跟踪的系统调用,`slow_syscalls` 计数超过阈值的调用,`dropped_events` 计数 ring buffer 分配失败。
|
||||
|
||||
## 用户空间程序
|
||||
|
||||
```c
|
||||
// SPDX-License-Identifier: (LGPL-2.1 OR BSD-2-Clause)
|
||||
#include <errno.h>
|
||||
#include <getopt.h>
|
||||
#include <signal.h>
|
||||
#include <stdbool.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <sys/syscall.h>
|
||||
#include <sys/types.h>
|
||||
#include <sys/wait.h>
|
||||
#include <time.h>
|
||||
#include <unistd.h>
|
||||
#include <bpf/libbpf.h>
|
||||
#include "slow_syscall_index.h"
|
||||
#include "slow_syscall_index.skel.h"
|
||||
|
||||
#define MAX_STATS 512
|
||||
|
||||
struct options {
|
||||
unsigned int pid;
|
||||
unsigned int minimum_ms;
|
||||
unsigned int duration_seconds;
|
||||
unsigned int top;
|
||||
bool demo;
|
||||
};
|
||||
|
||||
struct syscall_stats {
|
||||
unsigned int tgid;
|
||||
unsigned int syscall_id;
|
||||
unsigned long long count;
|
||||
unsigned long long total_ns;
|
||||
unsigned long long maximum_ns;
|
||||
unsigned long long errors;
|
||||
char comm[SLOW_SYSCALL_COMM_LEN];
|
||||
};
|
||||
|
||||
static struct syscall_stats stats[MAX_STATS];
|
||||
static size_t stats_count;
|
||||
static volatile sig_atomic_t stop;
|
||||
static unsigned int received_events;
|
||||
static unsigned int unaggregated_events;
|
||||
|
||||
static void handle_signal(int signal_number)
|
||||
{
|
||||
(void)signal_number;
|
||||
stop = 1;
|
||||
}
|
||||
|
||||
static unsigned long long monotonic_ns(void)
|
||||
{
|
||||
struct timespec now;
|
||||
|
||||
clock_gettime(CLOCK_MONOTONIC, &now);
|
||||
return (unsigned long long)now.tv_sec * 1000000000ULL + now.tv_nsec;
|
||||
}
|
||||
|
||||
static const char *syscall_name(unsigned int id)
|
||||
{
|
||||
switch (id) {
|
||||
#ifdef __NR_read
|
||||
case __NR_read: return "read";
|
||||
#endif
|
||||
#ifdef __NR_write
|
||||
case __NR_write: return "write";
|
||||
#endif
|
||||
#ifdef __NR_openat
|
||||
case __NR_openat: return "openat";
|
||||
#endif
|
||||
#ifdef __NR_close
|
||||
case __NR_close: return "close";
|
||||
#endif
|
||||
#ifdef __NR_fsync
|
||||
case __NR_fsync: return "fsync";
|
||||
#endif
|
||||
#ifdef __NR_fdatasync
|
||||
case __NR_fdatasync: return "fdatasync";
|
||||
#endif
|
||||
#ifdef __NR_poll
|
||||
case __NR_poll: return "poll";
|
||||
#endif
|
||||
#ifdef __NR_ppoll
|
||||
case __NR_ppoll: return "ppoll";
|
||||
#endif
|
||||
#ifdef __NR_epoll_wait
|
||||
case __NR_epoll_wait: return "epoll_wait";
|
||||
#endif
|
||||
#ifdef __NR_epoll_pwait
|
||||
case __NR_epoll_pwait: return "epoll_pwait";
|
||||
#endif
|
||||
#ifdef __NR_futex
|
||||
case __NR_futex: return "futex";
|
||||
#endif
|
||||
#ifdef __NR_nanosleep
|
||||
case __NR_nanosleep: return "nanosleep";
|
||||
#endif
|
||||
#ifdef __NR_clock_nanosleep
|
||||
case __NR_clock_nanosleep: return "clock_nanosleep";
|
||||
#endif
|
||||
#ifdef __NR_connect
|
||||
case __NR_connect: return "connect";
|
||||
#endif
|
||||
#ifdef __NR_accept
|
||||
case __NR_accept: return "accept";
|
||||
#endif
|
||||
#ifdef __NR_accept4
|
||||
case __NR_accept4: return "accept4";
|
||||
#endif
|
||||
#ifdef __NR_recvfrom
|
||||
case __NR_recvfrom: return "recvfrom";
|
||||
#endif
|
||||
#ifdef __NR_recvmsg
|
||||
case __NR_recvmsg: return "recvmsg";
|
||||
#endif
|
||||
#ifdef __NR_sendto
|
||||
case __NR_sendto: return "sendto";
|
||||
#endif
|
||||
#ifdef __NR_sendmsg
|
||||
case __NR_sendmsg: return "sendmsg";
|
||||
#endif
|
||||
default: return "unknown";
|
||||
}
|
||||
}
|
||||
|
||||
static struct syscall_stats *get_stats(const struct slow_syscall_event *event)
|
||||
{
|
||||
struct syscall_stats *entry;
|
||||
|
||||
for (size_t i = 0; i < stats_count; i++)
|
||||
if (stats[i].tgid == event->tgid &&
|
||||
stats[i].syscall_id == event->syscall_id)
|
||||
return &stats[i];
|
||||
if (stats_count == MAX_STATS)
|
||||
return NULL;
|
||||
entry = &stats[stats_count++];
|
||||
entry->tgid = event->tgid;
|
||||
entry->syscall_id = event->syscall_id;
|
||||
memcpy(entry->comm, event->comm, sizeof(entry->comm));
|
||||
entry->comm[sizeof(entry->comm) - 1] = '\0';
|
||||
return entry;
|
||||
}
|
||||
|
||||
static int handle_event(void *ctx, void *data, size_t size)
|
||||
{
|
||||
const struct slow_syscall_event *event = data;
|
||||
struct syscall_stats *entry;
|
||||
|
||||
(void)ctx;
|
||||
if (size != sizeof(*event))
|
||||
return 0;
|
||||
entry = get_stats(event);
|
||||
if (entry) {
|
||||
entry->count++;
|
||||
entry->total_ns += event->duration_ns;
|
||||
if (event->duration_ns > entry->maximum_ns)
|
||||
entry->maximum_ns = event->duration_ns;
|
||||
if (event->return_value < 0)
|
||||
entry->errors++;
|
||||
} else {
|
||||
unaggregated_events++;
|
||||
}
|
||||
received_events++;
|
||||
printf("pid=%u tid=%u comm=%s syscall=%s(%u) latency_ms=%.3f return=%lld\n",
|
||||
event->tgid, event->tid, event->comm,
|
||||
syscall_name(event->syscall_id), event->syscall_id,
|
||||
event->duration_ns / 1000000.0, event->return_value);
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int compare_stats(const void *left, const void *right)
|
||||
{
|
||||
const struct syscall_stats *a = left;
|
||||
const struct syscall_stats *b = right;
|
||||
|
||||
return a->total_ns < b->total_ns ? 1 : a->total_ns > b->total_ns ? -1 : 0;
|
||||
}
|
||||
|
||||
static void print_summary(unsigned int top)
|
||||
{
|
||||
size_t limit;
|
||||
|
||||
qsort(stats, stats_count, sizeof(stats[0]), compare_stats);
|
||||
limit = stats_count < top ? stats_count : top;
|
||||
printf("\nSlow syscall index, ranked by total latency:\n");
|
||||
printf("%-7s %-16s %-18s %8s %12s %12s %8s\n",
|
||||
"PID", "COMM", "SYSCALL", "COUNT", "TOTAL_MS", "MAX_MS", "ERRORS");
|
||||
for (size_t i = 0; i < limit; i++) {
|
||||
const struct syscall_stats *entry = &stats[i];
|
||||
|
||||
printf("%-7u %-16s %-12s(%3u) %8llu %12.3f %12.3f %8llu\n",
|
||||
entry->tgid, entry->comm, syscall_name(entry->syscall_id),
|
||||
entry->syscall_id, entry->count,
|
||||
entry->total_ns / 1000000.0,
|
||||
entry->maximum_ns / 1000000.0, entry->errors);
|
||||
}
|
||||
}
|
||||
|
||||
static int parse_uint(const char *text, unsigned int maximum,
|
||||
unsigned int *value)
|
||||
{
|
||||
char *end = NULL;
|
||||
unsigned long parsed;
|
||||
|
||||
errno = 0;
|
||||
parsed = strtoul(text, &end, 10);
|
||||
if (errno || !*text || *end || parsed > maximum)
|
||||
return -1;
|
||||
*value = parsed;
|
||||
return 0;
|
||||
}
|
||||
|
||||
static void usage(const char *program)
|
||||
{
|
||||
printf("Usage: %s [--pid PID] [--min-ms MS] [--duration SEC] [--top N] [--demo]\n",
|
||||
program);
|
||||
}
|
||||
|
||||
static int parse_options(int argc, char **argv, struct options *options)
|
||||
{
|
||||
static const struct option long_options[] = {
|
||||
{ "pid", required_argument, NULL, 'p' },
|
||||
{ "min-ms", required_argument, NULL, 'm' },
|
||||
{ "duration", required_argument, NULL, 'd' },
|
||||
{ "top", required_argument, NULL, 't' },
|
||||
{ "demo", no_argument, NULL, 'D' },
|
||||
{ "help", no_argument, NULL, 'h' },
|
||||
{},
|
||||
};
|
||||
int option;
|
||||
|
||||
while ((option = getopt_long(argc, argv, "p:m:d:t:Dh", long_options,
|
||||
NULL)) != -1) {
|
||||
switch (option) {
|
||||
case 'p':
|
||||
if (parse_uint(optarg, 0xffffffffU, &options->pid))
|
||||
return -1;
|
||||
break;
|
||||
case 'm':
|
||||
if (parse_uint(optarg, 60000, &options->minimum_ms))
|
||||
return -1;
|
||||
break;
|
||||
case 'd':
|
||||
if (parse_uint(optarg, 86400, &options->duration_seconds))
|
||||
return -1;
|
||||
break;
|
||||
case 't':
|
||||
if (parse_uint(optarg, MAX_STATS, &options->top))
|
||||
return -1;
|
||||
break;
|
||||
case 'D': options->demo = true; break;
|
||||
case 'h': usage(argv[0]); exit(0);
|
||||
default: return -1;
|
||||
}
|
||||
}
|
||||
return optind == argc && options->minimum_ms && options->top ? 0 : -1;
|
||||
}
|
||||
|
||||
static pid_t start_demo(int gate[2], int data_pipe[2])
|
||||
{
|
||||
pid_t child = fork();
|
||||
char byte;
|
||||
|
||||
if (child)
|
||||
return child;
|
||||
close(gate[1]);
|
||||
close(data_pipe[1]);
|
||||
if (read(gate[0], &byte, 1) != 1)
|
||||
_exit(2);
|
||||
if (read(data_pipe[0], &byte, 1) != 1)
|
||||
_exit(3);
|
||||
_exit(0);
|
||||
}
|
||||
|
||||
int main(int argc, char **argv)
|
||||
{
|
||||
struct options options = { .minimum_ms = 10, .top = 10 };
|
||||
struct slow_syscall_index_bpf *skel = NULL;
|
||||
struct ring_buffer *ring = NULL;
|
||||
unsigned long long deadline = 0;
|
||||
int gate[2] = { -1, -1 }, data_pipe[2] = { -1, -1 };
|
||||
pid_t demo_child = -1;
|
||||
int demo_status = 0;
|
||||
int err = 1;
|
||||
|
||||
setvbuf(stdout, NULL, _IONBF, 0);
|
||||
if (parse_options(argc, argv, &options)) {
|
||||
usage(argv[0]);
|
||||
return 2;
|
||||
}
|
||||
if (options.demo) {
|
||||
if (options.pid) {
|
||||
fprintf(stderr, "--demo and --pid cannot be combined\n");
|
||||
return 2;
|
||||
}
|
||||
if (pipe(gate) || pipe(data_pipe)) {
|
||||
perror("pipe");
|
||||
goto cleanup;
|
||||
}
|
||||
demo_child = start_demo(gate, data_pipe);
|
||||
if (demo_child < 0) {
|
||||
perror("fork");
|
||||
goto cleanup;
|
||||
}
|
||||
options.pid = demo_child;
|
||||
}
|
||||
|
||||
skel = slow_syscall_index_bpf__open();
|
||||
if (!skel)
|
||||
goto cleanup;
|
||||
skel->rodata->minimum_ns = (unsigned long long)options.minimum_ms * 1000000ULL;
|
||||
skel->rodata->target_tgid = options.pid;
|
||||
skel->rodata->ignored_tgid = getpid();
|
||||
if (slow_syscall_index_bpf__load(skel) ||
|
||||
slow_syscall_index_bpf__attach(skel)) {
|
||||
fprintf(stderr, "failed to load and attach slow syscall tracer\n");
|
||||
goto cleanup;
|
||||
}
|
||||
ring = ring_buffer__new(bpf_map__fd(skel->maps.events), handle_event,
|
||||
NULL, NULL);
|
||||
if (!ring)
|
||||
goto cleanup;
|
||||
|
||||
signal(SIGINT, handle_signal);
|
||||
signal(SIGTERM, handle_signal);
|
||||
if (options.pid)
|
||||
printf("Tracing PID %u syscalls slower than %u ms. Press Ctrl-C to stop.\n",
|
||||
options.pid, options.minimum_ms);
|
||||
else
|
||||
printf("Tracing all syscalls slower than %u ms. Press Ctrl-C to stop.\n",
|
||||
options.minimum_ms);
|
||||
if (options.duration_seconds)
|
||||
deadline = monotonic_ns() +
|
||||
(unsigned long long)options.duration_seconds * 1000000000ULL;
|
||||
|
||||
if (options.demo) {
|
||||
char byte = 'x';
|
||||
struct timespec delay = {
|
||||
.tv_sec = options.minimum_ms / 1000,
|
||||
.tv_nsec = (long)(options.minimum_ms % 1000 + 20) * 1000000L,
|
||||
};
|
||||
|
||||
if (delay.tv_nsec >= 1000000000L) {
|
||||
delay.tv_sec++;
|
||||
delay.tv_nsec -= 1000000000L;
|
||||
}
|
||||
close(gate[0]); gate[0] = -1;
|
||||
close(data_pipe[0]); data_pipe[0] = -1;
|
||||
write(gate[1], &byte, 1);
|
||||
nanosleep(&delay, NULL);
|
||||
write(data_pipe[1], &byte, 1);
|
||||
waitpid(demo_child, &demo_status, 0);
|
||||
demo_child = -1;
|
||||
for (int i = 0; i < 20 && !received_events; i++)
|
||||
ring_buffer__poll(ring, 50);
|
||||
} else {
|
||||
while (!stop && (!deadline || monotonic_ns() < deadline)) {
|
||||
int poll_result = ring_buffer__poll(ring, 100);
|
||||
|
||||
if (poll_result < 0 && poll_result != -EINTR) {
|
||||
fprintf(stderr, "ring buffer poll failed: %d\n", poll_result);
|
||||
goto cleanup;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
print_summary(options.top);
|
||||
printf("completed=%llu slow=%llu dropped=%llu unaggregated=%u\n",
|
||||
(unsigned long long)skel->bss->completed_syscalls,
|
||||
(unsigned long long)skel->bss->slow_syscalls,
|
||||
(unsigned long long)skel->bss->dropped_events,
|
||||
unaggregated_events);
|
||||
if (options.demo && (!WIFEXITED(demo_status) || WEXITSTATUS(demo_status) ||
|
||||
!received_events))
|
||||
goto cleanup;
|
||||
err = 0;
|
||||
|
||||
cleanup:
|
||||
if (demo_child > 0) {
|
||||
kill(demo_child, SIGKILL);
|
||||
waitpid(demo_child, NULL, 0);
|
||||
}
|
||||
for (size_t i = 0; i < 2; i++) {
|
||||
if (gate[i] >= 0) close(gate[i]);
|
||||
if (data_pipe[i] >= 0) close(data_pipe[i]);
|
||||
}
|
||||
ring_buffer__free(ring);
|
||||
slow_syscall_index_bpf__destroy(skel);
|
||||
return err;
|
||||
}
|
||||
```
|
||||
|
||||
用户空间程序按 TGID 和系统调用号聚合事件。每个事件更新该组合的计数、总延迟、最大延迟和错误计数。在退出时,它按总延迟排序结果并打印前 N 个条目。
|
||||
|
||||
演示模式 fork 一个子进程,该进程阻塞在管道读取上。父进程延迟足够长的时间以超过阈值,然后写入以解除子进程阻塞。这会创建一个有保证的慢系统调用用于测试。
|
||||
|
||||
## 编译和执行
|
||||
|
||||
构建工具:
|
||||
|
||||
```bash
|
||||
cd src/56-slow-syscall-index
|
||||
make
|
||||
```
|
||||
|
||||
使用选项运行:
|
||||
|
||||
```bash
|
||||
sudo ./slow_syscall_index --pid 1234 --min-ms 10 --duration 60 --top 20
|
||||
```
|
||||
|
||||
或运行内置演示:
|
||||
|
||||
```bash
|
||||
sudo ./slow_syscall_index --demo --min-ms 10 --top 10
|
||||
```
|
||||
|
||||
示例输出:
|
||||
|
||||
```text
|
||||
Tracing PID 1255 syscalls slower than 10 ms. Press Ctrl-C to stop.
|
||||
pid=1255 tid=1255 comm=slow_syscall_in syscall=read(0) latency_ms=30.080 return=1
|
||||
|
||||
Slow syscall index, ranked by total latency:
|
||||
PID COMM SYSCALL COUNT TOTAL_MS MAX_MS ERRORS
|
||||
1255 slow_syscall_in read ( 0) 1 30.080 30.080 0
|
||||
completed=1 slow=1 dropped=0 unaggregated=0
|
||||
```
|
||||
|
||||
## 环境要求
|
||||
|
||||
| 要求 | 详情 |
|
||||
|------|------|
|
||||
| 内核 | Linux 5.11+(BPF 任务本地存储) |
|
||||
| 配置 | `CONFIG_BPF_SYSCALL`、`CONFIG_DEBUG_INFO_BTF` |
|
||||
| 权限 | Root |
|
||||
|
||||
## 理解输出
|
||||
|
||||
索引显示:
|
||||
|
||||
- **PID**:线程组 ID,也就是事件中携带的进程 ID
|
||||
- **COMM**:进程名
|
||||
- **SYSCALL**:系统调用名和号
|
||||
- **COUNT**:慢调用次数
|
||||
- **TOTAL_MS**:所有慢系统调用持续时间之和
|
||||
- **MAX_MS**:单次最长调用
|
||||
- **ERRORS**:返回负值的调用次数
|
||||
|
||||
`completed` 计数器显示所有跟踪的系统调用,`slow` 显示超过阈值的调用,`dropped` 显示 ring buffer 分配失败。用户态最多保存 512 个 `(TGID, syscall)` 分组,超过表容量的事件仍会被接收,并计入 `unaggregated`。
|
||||
|
||||
## 总结
|
||||
|
||||
任务本地存储把每次系统调用退出与对应的入口关联起来,无需在用户态维护 PID 映射。内核中的阈值过滤让事件流保持集中,用户态聚合再把慢调用整理成延迟排名。这里得到的是系统调用的挂钟延迟,如果还需要区分 off-CPU 时间,可以继续关联调度器事件。
|
||||
|
||||
> 如果你想深入了解 eBPF,请查看我们的教程代码仓库 <https://github.com/eunomia-bpf/bpf-developer-tutorial> 或访问我们的网站 <https://eunomia.dev/tutorials/>。
|
||||
|
||||
## 参考资料
|
||||
|
||||
- [BPF 任务本地存储文档](https://docs.ebpf.io/linux/map-type/BPF_MAP_TYPE_TASK_STORAGE/)
|
||||
- [BPF ring buffer 文档](https://docs.kernel.org/6.6/bpf/ringbuf.html)
|
||||
- [raw_syscalls 跟踪点](https://github.com/torvalds/linux/blob/master/include/trace/events/syscalls.h)
|
||||
- [任务本地存储 commit](https://github.com/torvalds/linux/commit/4cf1bc1f1045)
|
||||
88
src/56-slow-syscall-index/slow_syscall_index.bpf.c
Normal file
88
src/56-slow-syscall-index/slow_syscall_index.bpf.c
Normal file
@@ -0,0 +1,88 @@
|
||||
// SPDX-License-Identifier: GPL-2.0
|
||||
#include "vmlinux.h"
|
||||
#include <bpf/bpf_helpers.h>
|
||||
#include "slow_syscall_index.h"
|
||||
|
||||
char LICENSE[] SEC("license") = "GPL";
|
||||
|
||||
const volatile __u64 minimum_ns = 10 * 1000 * 1000ULL;
|
||||
const volatile __u32 target_tgid;
|
||||
const volatile __u32 ignored_tgid;
|
||||
|
||||
struct syscall_state {
|
||||
__u64 started_ns;
|
||||
__u32 syscall_id;
|
||||
__u32 pad;
|
||||
};
|
||||
|
||||
struct {
|
||||
__uint(type, BPF_MAP_TYPE_TASK_STORAGE);
|
||||
__uint(map_flags, BPF_F_NO_PREALLOC);
|
||||
__type(key, int);
|
||||
__type(value, struct syscall_state);
|
||||
} active_syscalls SEC(".maps");
|
||||
|
||||
struct {
|
||||
__uint(type, BPF_MAP_TYPE_RINGBUF);
|
||||
__uint(max_entries, 256 * 1024);
|
||||
} events SEC(".maps");
|
||||
|
||||
__u64 completed_syscalls;
|
||||
__u64 slow_syscalls;
|
||||
__u64 dropped_events;
|
||||
|
||||
SEC("tp/raw_syscalls/sys_enter")
|
||||
int record_syscall_entry(struct trace_event_raw_sys_enter *ctx)
|
||||
{
|
||||
struct task_struct *task;
|
||||
struct syscall_state *state;
|
||||
__u64 pid_tgid = bpf_get_current_pid_tgid();
|
||||
__u32 tgid = pid_tgid >> 32;
|
||||
|
||||
if (tgid == ignored_tgid || (target_tgid && tgid != target_tgid))
|
||||
return 0;
|
||||
task = bpf_get_current_task_btf();
|
||||
state = bpf_task_storage_get(&active_syscalls, task, 0,
|
||||
BPF_LOCAL_STORAGE_GET_F_CREATE);
|
||||
if (!state)
|
||||
return 0;
|
||||
state->started_ns = bpf_ktime_get_ns();
|
||||
state->syscall_id = ctx->id;
|
||||
return 0;
|
||||
}
|
||||
|
||||
SEC("tp/raw_syscalls/sys_exit")
|
||||
int report_slow_syscall(struct trace_event_raw_sys_exit *ctx)
|
||||
{
|
||||
struct task_struct *task = bpf_get_current_task_btf();
|
||||
struct slow_syscall_event *event;
|
||||
struct syscall_state *state;
|
||||
__u64 pid_tgid = bpf_get_current_pid_tgid();
|
||||
__u64 duration_ns;
|
||||
|
||||
state = bpf_task_storage_get(&active_syscalls, task, 0, 0);
|
||||
if (!state)
|
||||
return 0;
|
||||
duration_ns = bpf_ktime_get_ns() - state->started_ns;
|
||||
__sync_fetch_and_add(&completed_syscalls, 1);
|
||||
if (duration_ns < minimum_ns)
|
||||
goto out;
|
||||
|
||||
__sync_fetch_and_add(&slow_syscalls, 1);
|
||||
event = bpf_ringbuf_reserve(&events, sizeof(*event), 0);
|
||||
if (!event) {
|
||||
__sync_fetch_and_add(&dropped_events, 1);
|
||||
goto out;
|
||||
}
|
||||
event->timestamp_ns = bpf_ktime_get_ns();
|
||||
event->duration_ns = duration_ns;
|
||||
event->return_value = ctx->ret;
|
||||
event->tgid = pid_tgid >> 32;
|
||||
event->tid = (__u32)pid_tgid;
|
||||
event->syscall_id = state->syscall_id;
|
||||
bpf_get_current_comm(event->comm, sizeof(event->comm));
|
||||
bpf_ringbuf_submit(event, 0);
|
||||
out:
|
||||
bpf_task_storage_delete(&active_syscalls, task);
|
||||
return 0;
|
||||
}
|
||||
389
src/56-slow-syscall-index/slow_syscall_index.c
Normal file
389
src/56-slow-syscall-index/slow_syscall_index.c
Normal file
@@ -0,0 +1,389 @@
|
||||
// SPDX-License-Identifier: (LGPL-2.1 OR BSD-2-Clause)
|
||||
#include <errno.h>
|
||||
#include <getopt.h>
|
||||
#include <signal.h>
|
||||
#include <stdbool.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <sys/syscall.h>
|
||||
#include <sys/types.h>
|
||||
#include <sys/wait.h>
|
||||
#include <time.h>
|
||||
#include <unistd.h>
|
||||
#include <bpf/libbpf.h>
|
||||
#include "slow_syscall_index.h"
|
||||
#include "slow_syscall_index.skel.h"
|
||||
|
||||
#define MAX_STATS 512
|
||||
|
||||
struct options {
|
||||
unsigned int pid;
|
||||
unsigned int minimum_ms;
|
||||
unsigned int duration_seconds;
|
||||
unsigned int top;
|
||||
bool demo;
|
||||
};
|
||||
|
||||
struct syscall_stats {
|
||||
unsigned int tgid;
|
||||
unsigned int syscall_id;
|
||||
unsigned long long count;
|
||||
unsigned long long total_ns;
|
||||
unsigned long long maximum_ns;
|
||||
unsigned long long errors;
|
||||
char comm[SLOW_SYSCALL_COMM_LEN];
|
||||
};
|
||||
|
||||
static struct syscall_stats stats[MAX_STATS];
|
||||
static size_t stats_count;
|
||||
static volatile sig_atomic_t stop;
|
||||
static unsigned int received_events;
|
||||
static unsigned int unaggregated_events;
|
||||
|
||||
static void handle_signal(int signal_number)
|
||||
{
|
||||
(void)signal_number;
|
||||
stop = 1;
|
||||
}
|
||||
|
||||
static unsigned long long monotonic_ns(void)
|
||||
{
|
||||
struct timespec now;
|
||||
|
||||
clock_gettime(CLOCK_MONOTONIC, &now);
|
||||
return (unsigned long long)now.tv_sec * 1000000000ULL + now.tv_nsec;
|
||||
}
|
||||
|
||||
static const char *syscall_name(unsigned int id)
|
||||
{
|
||||
switch (id) {
|
||||
#ifdef __NR_read
|
||||
case __NR_read: return "read";
|
||||
#endif
|
||||
#ifdef __NR_write
|
||||
case __NR_write: return "write";
|
||||
#endif
|
||||
#ifdef __NR_openat
|
||||
case __NR_openat: return "openat";
|
||||
#endif
|
||||
#ifdef __NR_close
|
||||
case __NR_close: return "close";
|
||||
#endif
|
||||
#ifdef __NR_fsync
|
||||
case __NR_fsync: return "fsync";
|
||||
#endif
|
||||
#ifdef __NR_fdatasync
|
||||
case __NR_fdatasync: return "fdatasync";
|
||||
#endif
|
||||
#ifdef __NR_poll
|
||||
case __NR_poll: return "poll";
|
||||
#endif
|
||||
#ifdef __NR_ppoll
|
||||
case __NR_ppoll: return "ppoll";
|
||||
#endif
|
||||
#ifdef __NR_epoll_wait
|
||||
case __NR_epoll_wait: return "epoll_wait";
|
||||
#endif
|
||||
#ifdef __NR_epoll_pwait
|
||||
case __NR_epoll_pwait: return "epoll_pwait";
|
||||
#endif
|
||||
#ifdef __NR_futex
|
||||
case __NR_futex: return "futex";
|
||||
#endif
|
||||
#ifdef __NR_nanosleep
|
||||
case __NR_nanosleep: return "nanosleep";
|
||||
#endif
|
||||
#ifdef __NR_clock_nanosleep
|
||||
case __NR_clock_nanosleep: return "clock_nanosleep";
|
||||
#endif
|
||||
#ifdef __NR_connect
|
||||
case __NR_connect: return "connect";
|
||||
#endif
|
||||
#ifdef __NR_accept
|
||||
case __NR_accept: return "accept";
|
||||
#endif
|
||||
#ifdef __NR_accept4
|
||||
case __NR_accept4: return "accept4";
|
||||
#endif
|
||||
#ifdef __NR_recvfrom
|
||||
case __NR_recvfrom: return "recvfrom";
|
||||
#endif
|
||||
#ifdef __NR_recvmsg
|
||||
case __NR_recvmsg: return "recvmsg";
|
||||
#endif
|
||||
#ifdef __NR_sendto
|
||||
case __NR_sendto: return "sendto";
|
||||
#endif
|
||||
#ifdef __NR_sendmsg
|
||||
case __NR_sendmsg: return "sendmsg";
|
||||
#endif
|
||||
default: return "unknown";
|
||||
}
|
||||
}
|
||||
|
||||
static struct syscall_stats *get_stats(const struct slow_syscall_event *event)
|
||||
{
|
||||
struct syscall_stats *entry;
|
||||
|
||||
for (size_t i = 0; i < stats_count; i++)
|
||||
if (stats[i].tgid == event->tgid &&
|
||||
stats[i].syscall_id == event->syscall_id)
|
||||
return &stats[i];
|
||||
if (stats_count == MAX_STATS)
|
||||
return NULL;
|
||||
entry = &stats[stats_count++];
|
||||
entry->tgid = event->tgid;
|
||||
entry->syscall_id = event->syscall_id;
|
||||
memcpy(entry->comm, event->comm, sizeof(entry->comm));
|
||||
entry->comm[sizeof(entry->comm) - 1] = '\0';
|
||||
return entry;
|
||||
}
|
||||
|
||||
static int handle_event(void *ctx, void *data, size_t size)
|
||||
{
|
||||
const struct slow_syscall_event *event = data;
|
||||
struct syscall_stats *entry;
|
||||
|
||||
(void)ctx;
|
||||
if (size != sizeof(*event))
|
||||
return 0;
|
||||
entry = get_stats(event);
|
||||
if (entry) {
|
||||
entry->count++;
|
||||
entry->total_ns += event->duration_ns;
|
||||
if (event->duration_ns > entry->maximum_ns)
|
||||
entry->maximum_ns = event->duration_ns;
|
||||
if (event->return_value < 0)
|
||||
entry->errors++;
|
||||
} else {
|
||||
unaggregated_events++;
|
||||
}
|
||||
received_events++;
|
||||
printf("pid=%u tid=%u comm=%s syscall=%s(%u) latency_ms=%.3f return=%lld\n",
|
||||
event->tgid, event->tid, event->comm,
|
||||
syscall_name(event->syscall_id), event->syscall_id,
|
||||
event->duration_ns / 1000000.0, event->return_value);
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int compare_stats(const void *left, const void *right)
|
||||
{
|
||||
const struct syscall_stats *a = left;
|
||||
const struct syscall_stats *b = right;
|
||||
|
||||
return a->total_ns < b->total_ns ? 1 : a->total_ns > b->total_ns ? -1 : 0;
|
||||
}
|
||||
|
||||
static void print_summary(unsigned int top)
|
||||
{
|
||||
size_t limit;
|
||||
|
||||
qsort(stats, stats_count, sizeof(stats[0]), compare_stats);
|
||||
limit = stats_count < top ? stats_count : top;
|
||||
printf("\nSlow syscall index, ranked by total latency:\n");
|
||||
printf("%-7s %-16s %-18s %8s %12s %12s %8s\n",
|
||||
"PID", "COMM", "SYSCALL", "COUNT", "TOTAL_MS", "MAX_MS", "ERRORS");
|
||||
for (size_t i = 0; i < limit; i++) {
|
||||
const struct syscall_stats *entry = &stats[i];
|
||||
|
||||
printf("%-7u %-16s %-12s(%3u) %8llu %12.3f %12.3f %8llu\n",
|
||||
entry->tgid, entry->comm, syscall_name(entry->syscall_id),
|
||||
entry->syscall_id, entry->count,
|
||||
entry->total_ns / 1000000.0,
|
||||
entry->maximum_ns / 1000000.0, entry->errors);
|
||||
}
|
||||
}
|
||||
|
||||
static int parse_uint(const char *text, unsigned int maximum,
|
||||
unsigned int *value)
|
||||
{
|
||||
char *end = NULL;
|
||||
unsigned long parsed;
|
||||
|
||||
errno = 0;
|
||||
parsed = strtoul(text, &end, 10);
|
||||
if (errno || !*text || *end || parsed > maximum)
|
||||
return -1;
|
||||
*value = parsed;
|
||||
return 0;
|
||||
}
|
||||
|
||||
static void usage(const char *program)
|
||||
{
|
||||
printf("Usage: %s [--pid PID] [--min-ms MS] [--duration SEC] [--top N] [--demo]\n",
|
||||
program);
|
||||
}
|
||||
|
||||
static int parse_options(int argc, char **argv, struct options *options)
|
||||
{
|
||||
static const struct option long_options[] = {
|
||||
{ "pid", required_argument, NULL, 'p' },
|
||||
{ "min-ms", required_argument, NULL, 'm' },
|
||||
{ "duration", required_argument, NULL, 'd' },
|
||||
{ "top", required_argument, NULL, 't' },
|
||||
{ "demo", no_argument, NULL, 'D' },
|
||||
{ "help", no_argument, NULL, 'h' },
|
||||
{},
|
||||
};
|
||||
int option;
|
||||
|
||||
while ((option = getopt_long(argc, argv, "p:m:d:t:Dh", long_options,
|
||||
NULL)) != -1) {
|
||||
switch (option) {
|
||||
case 'p':
|
||||
if (parse_uint(optarg, 0xffffffffU, &options->pid))
|
||||
return -1;
|
||||
break;
|
||||
case 'm':
|
||||
if (parse_uint(optarg, 60000, &options->minimum_ms))
|
||||
return -1;
|
||||
break;
|
||||
case 'd':
|
||||
if (parse_uint(optarg, 86400, &options->duration_seconds))
|
||||
return -1;
|
||||
break;
|
||||
case 't':
|
||||
if (parse_uint(optarg, MAX_STATS, &options->top))
|
||||
return -1;
|
||||
break;
|
||||
case 'D': options->demo = true; break;
|
||||
case 'h': usage(argv[0]); exit(0);
|
||||
default: return -1;
|
||||
}
|
||||
}
|
||||
return optind == argc && options->minimum_ms && options->top ? 0 : -1;
|
||||
}
|
||||
|
||||
static pid_t start_demo(int gate[2], int data_pipe[2])
|
||||
{
|
||||
pid_t child = fork();
|
||||
char byte;
|
||||
|
||||
if (child)
|
||||
return child;
|
||||
close(gate[1]);
|
||||
close(data_pipe[1]);
|
||||
if (read(gate[0], &byte, 1) != 1)
|
||||
_exit(2);
|
||||
if (read(data_pipe[0], &byte, 1) != 1)
|
||||
_exit(3);
|
||||
_exit(0);
|
||||
}
|
||||
|
||||
int main(int argc, char **argv)
|
||||
{
|
||||
struct options options = { .minimum_ms = 10, .top = 10 };
|
||||
struct slow_syscall_index_bpf *skel = NULL;
|
||||
struct ring_buffer *ring = NULL;
|
||||
unsigned long long deadline = 0;
|
||||
int gate[2] = { -1, -1 }, data_pipe[2] = { -1, -1 };
|
||||
pid_t demo_child = -1;
|
||||
int demo_status = 0;
|
||||
int err = 1;
|
||||
|
||||
setvbuf(stdout, NULL, _IONBF, 0);
|
||||
if (parse_options(argc, argv, &options)) {
|
||||
usage(argv[0]);
|
||||
return 2;
|
||||
}
|
||||
if (options.demo) {
|
||||
if (options.pid) {
|
||||
fprintf(stderr, "--demo and --pid cannot be combined\n");
|
||||
return 2;
|
||||
}
|
||||
if (pipe(gate) || pipe(data_pipe)) {
|
||||
perror("pipe");
|
||||
goto cleanup;
|
||||
}
|
||||
demo_child = start_demo(gate, data_pipe);
|
||||
if (demo_child < 0) {
|
||||
perror("fork");
|
||||
goto cleanup;
|
||||
}
|
||||
options.pid = demo_child;
|
||||
}
|
||||
|
||||
skel = slow_syscall_index_bpf__open();
|
||||
if (!skel)
|
||||
goto cleanup;
|
||||
skel->rodata->minimum_ns = (unsigned long long)options.minimum_ms * 1000000ULL;
|
||||
skel->rodata->target_tgid = options.pid;
|
||||
skel->rodata->ignored_tgid = getpid();
|
||||
if (slow_syscall_index_bpf__load(skel) ||
|
||||
slow_syscall_index_bpf__attach(skel)) {
|
||||
fprintf(stderr, "failed to load and attach slow syscall tracer\n");
|
||||
goto cleanup;
|
||||
}
|
||||
ring = ring_buffer__new(bpf_map__fd(skel->maps.events), handle_event,
|
||||
NULL, NULL);
|
||||
if (!ring)
|
||||
goto cleanup;
|
||||
|
||||
signal(SIGINT, handle_signal);
|
||||
signal(SIGTERM, handle_signal);
|
||||
if (options.pid)
|
||||
printf("Tracing PID %u syscalls slower than %u ms. Press Ctrl-C to stop.\n",
|
||||
options.pid, options.minimum_ms);
|
||||
else
|
||||
printf("Tracing all syscalls slower than %u ms. Press Ctrl-C to stop.\n",
|
||||
options.minimum_ms);
|
||||
if (options.duration_seconds)
|
||||
deadline = monotonic_ns() +
|
||||
(unsigned long long)options.duration_seconds * 1000000000ULL;
|
||||
|
||||
if (options.demo) {
|
||||
char byte = 'x';
|
||||
struct timespec delay = {
|
||||
.tv_sec = options.minimum_ms / 1000,
|
||||
.tv_nsec = (long)(options.minimum_ms % 1000 + 20) * 1000000L,
|
||||
};
|
||||
|
||||
if (delay.tv_nsec >= 1000000000L) {
|
||||
delay.tv_sec++;
|
||||
delay.tv_nsec -= 1000000000L;
|
||||
}
|
||||
close(gate[0]); gate[0] = -1;
|
||||
close(data_pipe[0]); data_pipe[0] = -1;
|
||||
write(gate[1], &byte, 1);
|
||||
nanosleep(&delay, NULL);
|
||||
write(data_pipe[1], &byte, 1);
|
||||
waitpid(demo_child, &demo_status, 0);
|
||||
demo_child = -1;
|
||||
for (int i = 0; i < 20 && !received_events; i++)
|
||||
ring_buffer__poll(ring, 50);
|
||||
} else {
|
||||
while (!stop && (!deadline || monotonic_ns() < deadline)) {
|
||||
int poll_result = ring_buffer__poll(ring, 100);
|
||||
|
||||
if (poll_result < 0 && poll_result != -EINTR) {
|
||||
fprintf(stderr, "ring buffer poll failed: %d\n", poll_result);
|
||||
goto cleanup;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
print_summary(options.top);
|
||||
printf("completed=%llu slow=%llu dropped=%llu unaggregated=%u\n",
|
||||
(unsigned long long)skel->bss->completed_syscalls,
|
||||
(unsigned long long)skel->bss->slow_syscalls,
|
||||
(unsigned long long)skel->bss->dropped_events,
|
||||
unaggregated_events);
|
||||
if (options.demo && (!WIFEXITED(demo_status) || WEXITSTATUS(demo_status) ||
|
||||
!received_events))
|
||||
goto cleanup;
|
||||
err = 0;
|
||||
|
||||
cleanup:
|
||||
if (demo_child > 0) {
|
||||
kill(demo_child, SIGKILL);
|
||||
waitpid(demo_child, NULL, 0);
|
||||
}
|
||||
for (size_t i = 0; i < 2; i++) {
|
||||
if (gate[i] >= 0) close(gate[i]);
|
||||
if (data_pipe[i] >= 0) close(data_pipe[i]);
|
||||
}
|
||||
ring_buffer__free(ring);
|
||||
slow_syscall_index_bpf__destroy(skel);
|
||||
return err;
|
||||
}
|
||||
17
src/56-slow-syscall-index/slow_syscall_index.h
Normal file
17
src/56-slow-syscall-index/slow_syscall_index.h
Normal file
@@ -0,0 +1,17 @@
|
||||
/* SPDX-License-Identifier: (LGPL-2.1 OR BSD-2-Clause) */
|
||||
#ifndef __SLOW_SYSCALL_INDEX_H
|
||||
#define __SLOW_SYSCALL_INDEX_H
|
||||
|
||||
#define SLOW_SYSCALL_COMM_LEN 16
|
||||
|
||||
struct slow_syscall_event {
|
||||
unsigned long long timestamp_ns;
|
||||
unsigned long long duration_ns;
|
||||
long long return_value;
|
||||
unsigned int tgid;
|
||||
unsigned int tid;
|
||||
unsigned int syscall_id;
|
||||
char comm[SLOW_SYSCALL_COMM_LEN];
|
||||
};
|
||||
|
||||
#endif /* __SLOW_SYSCALL_INDEX_H */
|
||||
25
src/56-slow-syscall-index/tests/test_slow_syscall_index.py
Normal file
25
src/56-slow-syscall-index/tests/test_slow_syscall_index.py
Normal file
@@ -0,0 +1,25 @@
|
||||
#!/usr/bin/env python3
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
|
||||
def main():
|
||||
result = subprocess.run(
|
||||
[sys.argv[1], "--demo", "--min-ms", "10"],
|
||||
text=True,
|
||||
capture_output=True,
|
||||
check=False,
|
||||
timeout=30,
|
||||
)
|
||||
output = result.stdout + result.stderr
|
||||
if result.returncode:
|
||||
raise AssertionError(output)
|
||||
assert "syscall=read(" in output, output
|
||||
assert "Slow syscall index, ranked by total latency:" in output, output
|
||||
assert "completed=" in output and "slow=1" in output, output
|
||||
assert "unaggregated=0" in output, output
|
||||
print("slow syscall index integration test: PASS")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
2
src/57-oom-watch/.gitignore
vendored
Normal file
2
src/57-oom-watch/.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
.output/
|
||||
oom_watch
|
||||
94
src/57-oom-watch/Makefile
Normal file
94
src/57-oom-watch/Makefile
Normal file
@@ -0,0 +1,94 @@
|
||||
# SPDX-License-Identifier: (LGPL-2.1 OR BSD-2-Clause)
|
||||
OUTPUT := .output
|
||||
CLANG ?= clang
|
||||
LIBBPF_SRC := $(abspath ../third_party/libbpf/src)
|
||||
BPFTOOL_SRC := $(abspath ../third_party/bpftool/src)
|
||||
LIBBPF_OBJ := $(abspath $(OUTPUT)/libbpf.a)
|
||||
BPFTOOL_OUTPUT ?= $(abspath $(OUTPUT)/bpftool)
|
||||
BPFTOOL ?= $(BPFTOOL_OUTPUT)/bootstrap/bpftool
|
||||
ARCH ?= $(shell uname -m | sed 's/x86_64/x86/' \
|
||||
| sed 's/arm.*/arm/' \
|
||||
| sed 's/aarch64/arm64/' \
|
||||
| sed 's/ppc64le/powerpc/' \
|
||||
| sed 's/mips.*/mips/' \
|
||||
| sed 's/riscv64/riscv/' \
|
||||
| sed 's/loongarch64/loongarch/')
|
||||
VMLINUX := ../third_party/vmlinux/$(ARCH)/vmlinux.h
|
||||
INCLUDES := -I$(OUTPUT) -I../third_party/libbpf/include/uapi -I$(dir $(VMLINUX)) -I.
|
||||
CFLAGS := -g -Wall -Wextra -D_GNU_SOURCE -pthread
|
||||
ALL_LDFLAGS := $(LDFLAGS) $(EXTRA_LDFLAGS)
|
||||
APPS = oom_watch
|
||||
|
||||
CLANG_BPF_SYS_INCLUDES ?= $(shell $(CLANG) -v -E - </dev/null 2>&1 \
|
||||
| sed -n '/<...> search starts here:/,/End of search list./{ s| \(/.*\)|-idirafter \1|p }')
|
||||
|
||||
ifeq ($(V),1)
|
||||
Q =
|
||||
msg =
|
||||
else
|
||||
Q = @
|
||||
msg = @printf ' %-8s %s%s\n' "$(1)" \
|
||||
"$(patsubst $(abspath $(OUTPUT))/%,%,$(2))" \
|
||||
"$(if $(3), $(3))";
|
||||
MAKEFLAGS += --no-print-directory
|
||||
endif
|
||||
|
||||
define allow-override
|
||||
$(if $(or $(findstring environment,$(origin $(1))),\
|
||||
$(findstring command line,$(origin $(1)))),,\
|
||||
$(eval $(1) = $(2)))
|
||||
endef
|
||||
|
||||
$(call allow-override,CC,$(CROSS_COMPILE)cc)
|
||||
|
||||
.PHONY: all clean test
|
||||
all: $(APPS)
|
||||
|
||||
clean:
|
||||
$(call msg,CLEAN)
|
||||
$(Q)rm -rf $(OUTPUT) $(APPS)
|
||||
|
||||
test: all
|
||||
@if [ "$$(id -u)" -ne 0 ]; then \
|
||||
echo "test requires root; run inside the documented KVM guest" >&2; \
|
||||
exit 1; \
|
||||
fi
|
||||
python3 tests/test_oom_watch.py ./oom_watch
|
||||
|
||||
$(OUTPUT) $(OUTPUT)/libbpf $(BPFTOOL_OUTPUT):
|
||||
$(call msg,MKDIR,$@)
|
||||
$(Q)mkdir -p $@
|
||||
|
||||
$(LIBBPF_OBJ): $(wildcard $(LIBBPF_SRC)/*.[ch] $(LIBBPF_SRC)/Makefile) | $(OUTPUT)/libbpf
|
||||
$(call msg,LIB,$@)
|
||||
$(Q)$(MAKE) -C $(LIBBPF_SRC) BUILD_STATIC_ONLY=1 \
|
||||
OBJDIR=$(dir $@)/libbpf DESTDIR=$(dir $@) \
|
||||
INCLUDEDIR= LIBDIR= UAPIDIR= install
|
||||
|
||||
$(BPFTOOL): | $(BPFTOOL_OUTPUT)
|
||||
$(call msg,BPFTOOL,$@)
|
||||
$(Q)$(MAKE) ARCH= CROSS_COMPILE= OUTPUT=$(BPFTOOL_OUTPUT)/ -C $(BPFTOOL_SRC) bootstrap
|
||||
|
||||
$(OUTPUT)/%.bpf.o: %.bpf.c $(LIBBPF_OBJ) $(wildcard %.h) $(VMLINUX) | $(OUTPUT) $(BPFTOOL)
|
||||
$(call msg,BPF,$@)
|
||||
$(Q)$(CLANG) -g -O2 -target bpf -D__TARGET_ARCH_$(ARCH) \
|
||||
$(INCLUDES) $(CLANG_BPF_SYS_INCLUDES) \
|
||||
-c $(filter %.c,$^) -o $(patsubst %.bpf.o,%.tmp.bpf.o,$@)
|
||||
$(Q)$(BPFTOOL) gen object $@ $(patsubst %.bpf.o,%.tmp.bpf.o,$@)
|
||||
|
||||
$(OUTPUT)/%.skel.h: $(OUTPUT)/%.bpf.o | $(OUTPUT) $(BPFTOOL)
|
||||
$(call msg,GEN-SKEL,$@)
|
||||
$(Q)$(BPFTOOL) gen skeleton $< > $@
|
||||
|
||||
$(patsubst %,$(OUTPUT)/%.o,$(APPS)): %.o: %.skel.h
|
||||
|
||||
$(OUTPUT)/%.o: %.c $(wildcard %.h) | $(OUTPUT)
|
||||
$(call msg,CC,$@)
|
||||
$(Q)$(CC) $(CFLAGS) $(INCLUDES) -c $(filter %.c,$^) -o $@
|
||||
|
||||
$(APPS): %: $(OUTPUT)/%.o $(LIBBPF_OBJ) | $(OUTPUT)
|
||||
$(call msg,BINARY,$@)
|
||||
$(Q)$(CC) $(CFLAGS) $^ $(ALL_LDFLAGS) -lelf -lz -o $@
|
||||
|
||||
.DELETE_ON_ERROR:
|
||||
.SECONDARY:
|
||||
759
src/57-oom-watch/README.md
Normal file
759
src/57-oom-watch/README.md
Normal file
@@ -0,0 +1,759 @@
|
||||
# eBPF Tutorial by Example: OOM Victim Monitoring with Reclaim Profiling
|
||||
|
||||
When the kernel kills a process due to memory pressure, how do you know which cgroup was under stress and how hard it was fighting to reclaim memory before the OOM killer stepped in? This tutorial builds a tool that tracks memory reclaim activity per cgroup and captures detailed context when the OOM killer selects a victim.
|
||||
|
||||
> Complete source code: <https://github.com/eunomia-bpf/bpf-developer-tutorial/tree/main/src/57-oom-watch>
|
||||
|
||||
## eBPF and OOM Tracepoints
|
||||
|
||||
eBPF lets verified programs run at Linux kernel hooks and send selected state to user space. This tutorial uses the `oom/mark_victim` tracepoint to capture when the OOM killer selects a victim, and `mm_vmscan_memcg_reclaim_begin/end` tracepoints to track memory reclaim activity. Linux 7.1 added the target `mem_cgroup` to these vmscan tracepoints, which lets the BPF program attribute reclaim to the memory cgroup being scanned instead of the task that happened to trigger it. Victim lookup uses the `bpf_task_from_pid()` kfunc introduced in Linux 6.2, while ordinary tracepoint programs gained tracing-kfunc access in Linux 6.12. The vmscan tracepoint change is the newest dependency, so this implementation requires Linux 7.1 or later.
|
||||
|
||||
## How the Implementation Works
|
||||
|
||||
The tool tracks two phases of the OOM lifecycle. Before any OOM event, the kernel attempts memory reclaim. We count reclaim cycles per memory cgroup, track how many pages were reclaimed, and note when reclaim was triggered from a different cgroup (cross-cgroup reclaim). When the OOM killer selects a victim, we capture the victim's memory statistics, link it to the accumulated reclaim profile, and track the victim until it exits.
|
||||
|
||||
## Header File
|
||||
|
||||
```c
|
||||
/* SPDX-License-Identifier: (LGPL-2.1 OR BSD-2-Clause) */
|
||||
#ifndef __OOM_WATCH_H
|
||||
#define __OOM_WATCH_H
|
||||
|
||||
enum oom_watch_event_type {
|
||||
OOM_VICTIM_MARKED = 1,
|
||||
OOM_VICTIM_EXITED = 2,
|
||||
};
|
||||
|
||||
struct oom_watch_event {
|
||||
unsigned long long timestamp_ns;
|
||||
unsigned long long cgroup_id;
|
||||
unsigned long long reclaim_begin_count;
|
||||
unsigned long long reclaim_end_count;
|
||||
unsigned long long reclaimed_pages;
|
||||
unsigned long long cross_cgroup_reclaims;
|
||||
unsigned long long last_reclaim_ns;
|
||||
unsigned long long total_vm_kb;
|
||||
unsigned long long anon_rss_kb;
|
||||
unsigned long long file_rss_kb;
|
||||
unsigned int type;
|
||||
unsigned int victim_pid;
|
||||
unsigned int victim_tid;
|
||||
unsigned int triggering_tgid;
|
||||
signed int exit_code;
|
||||
char comm[16];
|
||||
};
|
||||
|
||||
#endif /* __OOM_WATCH_H */
|
||||
```
|
||||
|
||||
The header defines the event structure sent to user space. Each event includes the cgroup ID, reclaim statistics (begin/end counts, pages reclaimed, cross-cgroup reclaims), victim memory statistics from the tracepoint, and exit code when the victim terminates.
|
||||
|
||||
## BPF Program
|
||||
|
||||
```c
|
||||
// SPDX-License-Identifier: GPL-2.0
|
||||
#include "vmlinux.h"
|
||||
#include <bpf/bpf_helpers.h>
|
||||
#include <bpf/bpf_tracing.h>
|
||||
#include <bpf/bpf_core_read.h>
|
||||
#include "oom_watch.h"
|
||||
|
||||
char LICENSE[] SEC("license") = "GPL";
|
||||
|
||||
const volatile __u64 target_cgroup_id;
|
||||
|
||||
extern struct task_struct *bpf_task_from_pid(__s32 pid) __ksym;
|
||||
extern void bpf_task_release(struct task_struct *task) __ksym;
|
||||
|
||||
struct reclaim_profile {
|
||||
__u64 begin_count;
|
||||
__u64 end_count;
|
||||
__u64 reclaimed_pages;
|
||||
__u64 cross_cgroup_reclaims;
|
||||
__u64 last_reclaim_ns;
|
||||
};
|
||||
|
||||
struct victim_state {
|
||||
__u64 cgroup_id;
|
||||
__u32 triggering_tgid;
|
||||
__u32 victim_tgid;
|
||||
};
|
||||
|
||||
struct {
|
||||
__uint(type, BPF_MAP_TYPE_LRU_HASH);
|
||||
__uint(max_entries, 4096);
|
||||
__type(key, __u64);
|
||||
__type(value, struct reclaim_profile);
|
||||
} profiles SEC(".maps");
|
||||
|
||||
struct {
|
||||
__uint(type, BPF_MAP_TYPE_HASH);
|
||||
__uint(max_entries, 1024);
|
||||
__type(key, __u32);
|
||||
__type(value, struct victim_state);
|
||||
} victims SEC(".maps");
|
||||
|
||||
struct {
|
||||
__uint(type, BPF_MAP_TYPE_RINGBUF);
|
||||
__uint(max_entries, 256 * 1024);
|
||||
} events SEC(".maps");
|
||||
|
||||
__u64 dropped_victim_states;
|
||||
|
||||
static __always_inline bool selected_cgroup(__u64 cgroup_id)
|
||||
{
|
||||
return !target_cgroup_id || cgroup_id == target_cgroup_id;
|
||||
}
|
||||
|
||||
static __always_inline __u64 victim_task_info(__u32 victim_pid,
|
||||
__u32 *victim_tgid)
|
||||
{
|
||||
struct task_struct *task;
|
||||
__u64 cgroup_id = 0;
|
||||
|
||||
task = bpf_task_from_pid(victim_pid);
|
||||
if (!task)
|
||||
return 0;
|
||||
cgroup_id = BPF_CORE_READ(task, cgroups, dfl_cgrp, kn, id);
|
||||
*victim_tgid = BPF_CORE_READ(task, tgid);
|
||||
bpf_task_release(task);
|
||||
return cgroup_id;
|
||||
}
|
||||
|
||||
static __always_inline __u64 memcg_cgroup_id(struct mem_cgroup *memcg)
|
||||
{
|
||||
struct cgroup *cgroup;
|
||||
|
||||
cgroup = BPF_CORE_READ(memcg, css.cgroup);
|
||||
if (!cgroup)
|
||||
return 0;
|
||||
return BPF_CORE_READ(cgroup, kn, id);
|
||||
}
|
||||
|
||||
static __always_inline struct reclaim_profile *get_profile(__u64 cgroup_id)
|
||||
{
|
||||
struct reclaim_profile initial = {};
|
||||
struct reclaim_profile *profile;
|
||||
|
||||
profile = bpf_map_lookup_elem(&profiles, &cgroup_id);
|
||||
if (profile)
|
||||
return profile;
|
||||
bpf_map_update_elem(&profiles, &cgroup_id, &initial, BPF_NOEXIST);
|
||||
return bpf_map_lookup_elem(&profiles, &cgroup_id);
|
||||
}
|
||||
|
||||
SEC("tp_btf/mm_vmscan_memcg_reclaim_begin")
|
||||
int BPF_PROG(track_reclaim_begin, gfp_t gfp_flags, int order,
|
||||
struct mem_cgroup *memcg)
|
||||
{
|
||||
struct reclaim_profile *profile;
|
||||
__u64 cgroup_id = memcg_cgroup_id(memcg);
|
||||
|
||||
(void)gfp_flags;
|
||||
(void)order;
|
||||
if (!selected_cgroup(cgroup_id))
|
||||
return 0;
|
||||
profile = get_profile(cgroup_id);
|
||||
if (!profile)
|
||||
return 0;
|
||||
__sync_fetch_and_add(&profile->begin_count, 1);
|
||||
if (bpf_get_current_cgroup_id() != cgroup_id)
|
||||
__sync_fetch_and_add(&profile->cross_cgroup_reclaims, 1);
|
||||
profile->last_reclaim_ns = bpf_ktime_get_ns();
|
||||
return 0;
|
||||
}
|
||||
|
||||
SEC("tp_btf/mm_vmscan_memcg_reclaim_end")
|
||||
int BPF_PROG(track_reclaim_end, unsigned long reclaimed,
|
||||
struct mem_cgroup *memcg)
|
||||
{
|
||||
struct reclaim_profile *profile;
|
||||
__u64 cgroup_id = memcg_cgroup_id(memcg);
|
||||
|
||||
if (!selected_cgroup(cgroup_id))
|
||||
return 0;
|
||||
profile = get_profile(cgroup_id);
|
||||
if (!profile)
|
||||
return 0;
|
||||
__sync_fetch_and_add(&profile->end_count, 1);
|
||||
__sync_fetch_and_add(&profile->reclaimed_pages, reclaimed);
|
||||
profile->last_reclaim_ns = bpf_ktime_get_ns();
|
||||
return 0;
|
||||
}
|
||||
|
||||
SEC("tracepoint/oom/mark_victim")
|
||||
int capture_oom_victim(struct trace_event_raw_mark_victim *ctx)
|
||||
{
|
||||
struct reclaim_profile *profile;
|
||||
struct victim_state victim;
|
||||
struct oom_watch_event *event;
|
||||
__u64 cgroup_id;
|
||||
__u32 victim_pid = ctx->pid;
|
||||
__u32 victim_tgid = 0;
|
||||
|
||||
cgroup_id = victim_task_info(victim_pid, &victim_tgid);
|
||||
if (!victim_tgid || !selected_cgroup(cgroup_id))
|
||||
return 0;
|
||||
victim.cgroup_id = cgroup_id;
|
||||
victim.triggering_tgid = bpf_get_current_pid_tgid() >> 32;
|
||||
victim.victim_tgid = victim_tgid;
|
||||
if (bpf_map_update_elem(&victims, &victim_pid, &victim, BPF_ANY)) {
|
||||
__sync_fetch_and_add(&dropped_victim_states, 1);
|
||||
return 0;
|
||||
}
|
||||
|
||||
event = bpf_ringbuf_reserve(&events, sizeof(*event), 0);
|
||||
if (!event)
|
||||
return 0;
|
||||
__builtin_memset(event, 0, sizeof(*event));
|
||||
event->timestamp_ns = bpf_ktime_get_ns();
|
||||
event->cgroup_id = cgroup_id;
|
||||
event->type = OOM_VICTIM_MARKED;
|
||||
event->victim_pid = victim_tgid;
|
||||
event->victim_tid = victim_pid;
|
||||
event->triggering_tgid = victim.triggering_tgid;
|
||||
event->total_vm_kb = ctx->total_vm;
|
||||
event->anon_rss_kb = ctx->anon_rss;
|
||||
event->file_rss_kb = ctx->file_rss;
|
||||
bpf_probe_read_kernel_str(event->comm, sizeof(event->comm),
|
||||
(void *)ctx + (ctx->__data_loc_comm & 0xffff));
|
||||
|
||||
profile = bpf_map_lookup_elem(&profiles, &cgroup_id);
|
||||
if (profile) {
|
||||
event->reclaim_begin_count = profile->begin_count;
|
||||
event->reclaim_end_count = profile->end_count;
|
||||
event->reclaimed_pages = profile->reclaimed_pages;
|
||||
event->cross_cgroup_reclaims = profile->cross_cgroup_reclaims;
|
||||
event->last_reclaim_ns = profile->last_reclaim_ns;
|
||||
}
|
||||
bpf_ringbuf_submit(event, 0);
|
||||
return 0;
|
||||
}
|
||||
|
||||
SEC("tracepoint/sched/sched_process_exit")
|
||||
int capture_victim_exit(void *ctx)
|
||||
{
|
||||
struct victim_state *victim;
|
||||
struct task_struct *task;
|
||||
struct oom_watch_event *event;
|
||||
__u64 pid_tgid = bpf_get_current_pid_tgid();
|
||||
__u32 tid = pid_tgid;
|
||||
|
||||
(void)ctx;
|
||||
victim = bpf_map_lookup_elem(&victims, &tid);
|
||||
if (!victim)
|
||||
return 0;
|
||||
event = bpf_ringbuf_reserve(&events, sizeof(*event), 0);
|
||||
if (event) {
|
||||
__builtin_memset(event, 0, sizeof(*event));
|
||||
event->timestamp_ns = bpf_ktime_get_ns();
|
||||
event->cgroup_id = victim->cgroup_id;
|
||||
event->type = OOM_VICTIM_EXITED;
|
||||
event->victim_pid = victim->victim_tgid;
|
||||
event->victim_tid = tid;
|
||||
event->triggering_tgid = victim->triggering_tgid;
|
||||
task = (struct task_struct *)bpf_get_current_task_btf();
|
||||
event->exit_code = BPF_CORE_READ(task, exit_code);
|
||||
bpf_get_current_comm(event->comm, sizeof(event->comm));
|
||||
bpf_ringbuf_submit(event, 0);
|
||||
}
|
||||
bpf_map_delete_elem(&victims, &tid);
|
||||
return 0;
|
||||
}
|
||||
```
|
||||
|
||||
The BPF program uses `bpf_task_from_pid()` kfunc to look up the victim task from its PID. This kfunc was introduced in Linux 6.2 and returns a referenced pointer that must be released with `bpf_task_release()`. `BPF_PROG_TYPE_TRACEPOINT` programs gained access to tracing kfuncs in Linux 6.12. The reclaim callbacks also consume the target `mem_cgroup` argument added to vmscan tracepoints in Linux 7.1, which sets the minimum kernel version for the complete tool.
|
||||
|
||||
The `track_reclaim_begin` and `track_reclaim_end` functions attach to memcg reclaim tracepoints. They count reclaim cycles and track cross-cgroup reclaims (when a task in one cgroup triggers reclaim in another cgroup's memory space).
|
||||
|
||||
The `capture_oom_victim` function runs when the OOM killer selects a victim. It uses `bpf_task_from_pid()` to get the victim's cgroup ID and TGID, stores victim state for exit tracking, and sends an event with memory statistics and accumulated reclaim profile.
|
||||
|
||||
The `capture_victim_exit` function tracks when the victim actually exits, confirming the kill completed and reporting the exit signal.
|
||||
|
||||
## User Space Program
|
||||
|
||||
```c
|
||||
// SPDX-License-Identifier: (LGPL-2.1 OR BSD-2-Clause)
|
||||
#include <errno.h>
|
||||
#include <fcntl.h>
|
||||
#include <getopt.h>
|
||||
#include <poll.h>
|
||||
#include <pthread.h>
|
||||
#include <signal.h>
|
||||
#include <stdbool.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <sys/mman.h>
|
||||
#include <sys/stat.h>
|
||||
#include <sys/types.h>
|
||||
#include <sys/wait.h>
|
||||
#include <time.h>
|
||||
#include <unistd.h>
|
||||
#include <bpf/libbpf.h>
|
||||
#include "oom_watch.h"
|
||||
#include "oom_watch.skel.h"
|
||||
|
||||
struct options {
|
||||
const char *cgroup_path;
|
||||
unsigned int duration_seconds;
|
||||
bool demo;
|
||||
};
|
||||
|
||||
static volatile sig_atomic_t stop;
|
||||
static int victim_events;
|
||||
static int exit_events;
|
||||
static unsigned long long observed_cgroup_id;
|
||||
static unsigned int observed_victim_pid;
|
||||
static unsigned int observed_victim_tid;
|
||||
static unsigned long long observed_reclaims;
|
||||
static unsigned long long observed_cross_cgroup_reclaims;
|
||||
|
||||
struct allocation_context {
|
||||
int ready_fd;
|
||||
int continue_fd;
|
||||
};
|
||||
|
||||
static struct allocation_context allocation_context;
|
||||
|
||||
static void handle_signal(int signal_number)
|
||||
{
|
||||
(void)signal_number;
|
||||
stop = 1;
|
||||
}
|
||||
|
||||
static unsigned long long monotonic_ns(void)
|
||||
{
|
||||
struct timespec now;
|
||||
|
||||
clock_gettime(CLOCK_MONOTONIC, &now);
|
||||
return (unsigned long long)now.tv_sec * 1000000000ULL + now.tv_nsec;
|
||||
}
|
||||
|
||||
static int write_text(const char *path, const char *text)
|
||||
{
|
||||
int fd = open(path, O_WRONLY | O_CLOEXEC);
|
||||
ssize_t length = strlen(text);
|
||||
int err = 0;
|
||||
|
||||
if (fd < 0)
|
||||
return -1;
|
||||
if (write(fd, text, length) != length)
|
||||
err = -1;
|
||||
close(fd);
|
||||
return err;
|
||||
}
|
||||
|
||||
static int memory_controller_enabled(bool *enabled)
|
||||
{
|
||||
char controllers[4096];
|
||||
ssize_t length;
|
||||
int fd;
|
||||
|
||||
fd = open("/sys/fs/cgroup/cgroup.subtree_control",
|
||||
O_RDONLY | O_CLOEXEC);
|
||||
if (fd < 0)
|
||||
return -1;
|
||||
length = read(fd, controllers, sizeof(controllers) - 1);
|
||||
close(fd);
|
||||
if (length < 0)
|
||||
return -1;
|
||||
controllers[length] = '\0';
|
||||
*enabled = strstr(controllers, "memory") != NULL;
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int handle_event(void *ctx, void *data, size_t size)
|
||||
{
|
||||
const struct oom_watch_event *event = data;
|
||||
|
||||
(void)ctx;
|
||||
if (size != sizeof(*event))
|
||||
return 0;
|
||||
if (event->type == OOM_VICTIM_MARKED) {
|
||||
victim_events++;
|
||||
observed_cgroup_id = event->cgroup_id;
|
||||
observed_victim_pid = event->victim_pid;
|
||||
observed_reclaims = event->reclaim_begin_count;
|
||||
observed_victim_tid = event->victim_tid;
|
||||
observed_cross_cgroup_reclaims = event->cross_cgroup_reclaims;
|
||||
printf("event=oom-victim pid=%u tid=%u comm=%s trigger_pid=%u cgroup_id=%llu "
|
||||
"anon_rss_kb=%llu file_rss_kb=%llu total_vm_kb=%llu "
|
||||
"reclaim_cycles=%llu cross_cgroup_reclaims=%llu "
|
||||
"reclaimed_pages=%llu\n",
|
||||
event->victim_pid, event->victim_tid, event->comm,
|
||||
event->triggering_tgid,
|
||||
(unsigned long long)event->cgroup_id,
|
||||
(unsigned long long)event->anon_rss_kb,
|
||||
(unsigned long long)event->file_rss_kb,
|
||||
(unsigned long long)event->total_vm_kb,
|
||||
(unsigned long long)event->reclaim_begin_count,
|
||||
(unsigned long long)event->cross_cgroup_reclaims,
|
||||
(unsigned long long)event->reclaimed_pages);
|
||||
} else if (event->type == OOM_VICTIM_EXITED) {
|
||||
exit_events++;
|
||||
printf("event=victim-exit pid=%u tid=%u cgroup_id=%llu exit_code=%d\n",
|
||||
event->victim_pid, event->victim_tid,
|
||||
(unsigned long long)event->cgroup_id, event->exit_code);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int parse_uint(const char *text, unsigned int *value)
|
||||
{
|
||||
char *end = NULL;
|
||||
unsigned long parsed;
|
||||
|
||||
errno = 0;
|
||||
parsed = strtoul(text, &end, 10);
|
||||
if (errno || !*text || *end || parsed > 86400)
|
||||
return -1;
|
||||
*value = parsed;
|
||||
return 0;
|
||||
}
|
||||
|
||||
static void usage(const char *program)
|
||||
{
|
||||
printf("Usage: %s [--cgroup PATH] [--duration SEC]\n"
|
||||
" %s --demo\n", program, program);
|
||||
}
|
||||
|
||||
static int parse_options(int argc, char **argv, struct options *options)
|
||||
{
|
||||
static const struct option long_options[] = {
|
||||
{ "cgroup", required_argument, NULL, 'c' },
|
||||
{ "duration", required_argument, NULL, 'd' },
|
||||
{ "demo", no_argument, NULL, 'D' },
|
||||
{ "help", no_argument, NULL, 'h' },
|
||||
{},
|
||||
};
|
||||
int option;
|
||||
|
||||
while ((option = getopt_long(argc, argv, "c:d:Dh", long_options,
|
||||
NULL)) != -1) {
|
||||
switch (option) {
|
||||
case 'c': options->cgroup_path = optarg; break;
|
||||
case 'd':
|
||||
if (parse_uint(optarg, &options->duration_seconds))
|
||||
return -1;
|
||||
break;
|
||||
case 'D': options->demo = true; break;
|
||||
case 'h': usage(argv[0]); exit(0);
|
||||
default: return -1;
|
||||
}
|
||||
}
|
||||
return optind == argc && !(options->demo && options->cgroup_path) ? 0 : -1;
|
||||
}
|
||||
|
||||
static void *allocation_worker(void *argument)
|
||||
{
|
||||
struct allocation_context *context = argument;
|
||||
size_t first_stage = 24 * 1024 * 1024;
|
||||
size_t length = 128 * 1024 * 1024;
|
||||
unsigned char *memory;
|
||||
char byte = 'x';
|
||||
|
||||
memory = mmap(NULL, length, PROT_READ | PROT_WRITE,
|
||||
MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
|
||||
if (memory == MAP_FAILED)
|
||||
_exit(4);
|
||||
for (size_t offset = 0; offset < first_stage; offset += 4096)
|
||||
memory[offset] = 0xa5;
|
||||
if (write(context->ready_fd, &byte, 1) != 1 ||
|
||||
read(context->continue_fd, &byte, 1) != 1)
|
||||
_exit(5);
|
||||
for (size_t offset = first_stage; offset < length; offset += 4096)
|
||||
memory[offset] = 0xa5;
|
||||
_exit(6);
|
||||
}
|
||||
|
||||
static void allocate_until_killed(const char *cgroup_path, int ready_fd,
|
||||
int continue_fd)
|
||||
{
|
||||
char procs_path[512];
|
||||
char pid_text[32];
|
||||
pthread_t worker;
|
||||
|
||||
snprintf(procs_path, sizeof(procs_path), "%s/cgroup.procs", cgroup_path);
|
||||
snprintf(pid_text, sizeof(pid_text), "%d", getpid());
|
||||
if (write_text(procs_path, pid_text))
|
||||
_exit(3);
|
||||
allocation_context.ready_fd = ready_fd;
|
||||
allocation_context.continue_fd = continue_fd;
|
||||
if (pthread_create(&worker, NULL, allocation_worker,
|
||||
&allocation_context))
|
||||
_exit(4);
|
||||
pthread_detach(worker);
|
||||
pthread_exit(NULL);
|
||||
}
|
||||
|
||||
static int configure_demo_cgroup(const char *path, bool *created,
|
||||
bool *enabled_by_demo)
|
||||
{
|
||||
bool memory_enabled;
|
||||
char file[512];
|
||||
|
||||
if (memory_controller_enabled(&memory_enabled))
|
||||
return -1;
|
||||
if (!memory_enabled) {
|
||||
if (write_text("/sys/fs/cgroup/cgroup.subtree_control", "+memory"))
|
||||
return -1;
|
||||
*enabled_by_demo = true;
|
||||
}
|
||||
if (mkdir(path, 0755))
|
||||
return -1;
|
||||
*created = true;
|
||||
snprintf(file, sizeof(file), "%s/memory.max", path);
|
||||
if (write_text(file, "33554432"))
|
||||
return -1;
|
||||
snprintf(file, sizeof(file), "%s/memory.swap.max", path);
|
||||
if (write_text(file, "0"))
|
||||
return -1;
|
||||
snprintf(file, sizeof(file), "%s/memory.oom.group", path);
|
||||
return write_text(file, "1");
|
||||
}
|
||||
|
||||
static int trigger_cross_cgroup_reclaim(const char *cgroup_path)
|
||||
{
|
||||
char reclaim_path[512];
|
||||
|
||||
snprintf(reclaim_path, sizeof(reclaim_path), "%s/memory.reclaim",
|
||||
cgroup_path);
|
||||
if (!write_text(reclaim_path, "8388608") || errno == EAGAIN)
|
||||
return 0;
|
||||
return -1;
|
||||
}
|
||||
|
||||
int main(int argc, char **argv)
|
||||
{
|
||||
struct options options = {};
|
||||
struct oom_watch_bpf *skel = NULL;
|
||||
struct ring_buffer *ring = NULL;
|
||||
char demo_cgroup_path[256];
|
||||
unsigned long long deadline = 0;
|
||||
struct stat cgroup_stat = {};
|
||||
const char *selected_path = NULL;
|
||||
pid_t child = -1;
|
||||
int ready_pipe[2] = { -1, -1 };
|
||||
int continue_pipe[2] = { -1, -1 };
|
||||
int status = 0;
|
||||
int err = 1;
|
||||
bool demo_cgroup_created = false;
|
||||
bool memory_enabled_by_demo = false;
|
||||
|
||||
setvbuf(stdout, NULL, _IONBF, 0);
|
||||
if (parse_options(argc, argv, &options)) {
|
||||
usage(argv[0]);
|
||||
return 2;
|
||||
}
|
||||
if (options.demo) {
|
||||
snprintf(demo_cgroup_path, sizeof(demo_cgroup_path),
|
||||
"/sys/fs/cgroup/ebpf-oom-watch-%d", getpid());
|
||||
if (configure_demo_cgroup(demo_cgroup_path,
|
||||
&demo_cgroup_created,
|
||||
&memory_enabled_by_demo)) {
|
||||
fprintf(stderr, "failed to configure demo memory cgroup: %s\n",
|
||||
strerror(errno));
|
||||
goto cleanup;
|
||||
}
|
||||
selected_path = demo_cgroup_path;
|
||||
} else {
|
||||
selected_path = options.cgroup_path;
|
||||
}
|
||||
if (selected_path && stat(selected_path, &cgroup_stat)) {
|
||||
fprintf(stderr, "failed to stat cgroup %s: %s\n", selected_path,
|
||||
strerror(errno));
|
||||
goto cleanup;
|
||||
}
|
||||
|
||||
skel = oom_watch_bpf__open();
|
||||
if (!skel)
|
||||
goto cleanup;
|
||||
skel->rodata->target_cgroup_id = selected_path ? cgroup_stat.st_ino : 0;
|
||||
if (oom_watch_bpf__load(skel) || oom_watch_bpf__attach(skel)) {
|
||||
fprintf(stderr, "failed to load and attach OOM watcher\n");
|
||||
goto cleanup;
|
||||
}
|
||||
ring = ring_buffer__new(bpf_map__fd(skel->maps.events), handle_event,
|
||||
NULL, NULL);
|
||||
if (!ring)
|
||||
goto cleanup;
|
||||
|
||||
if (selected_path)
|
||||
printf("oom-watch tracing cgroup=%s cgroup_id=%llu\n",
|
||||
selected_path, (unsigned long long)cgroup_stat.st_ino);
|
||||
else
|
||||
printf("oom-watch tracing all cgroups\n");
|
||||
|
||||
if (options.demo) {
|
||||
struct pollfd ready = { .events = POLLIN };
|
||||
struct timespec leader_exit_delay = { .tv_nsec = 100000000 };
|
||||
char byte = 'x';
|
||||
|
||||
if (pipe(ready_pipe) || pipe(continue_pipe))
|
||||
goto cleanup;
|
||||
child = fork();
|
||||
if (child < 0)
|
||||
goto cleanup;
|
||||
if (!child) {
|
||||
close(ready_pipe[0]);
|
||||
close(continue_pipe[1]);
|
||||
allocate_until_killed(selected_path, ready_pipe[1],
|
||||
continue_pipe[0]);
|
||||
}
|
||||
close(ready_pipe[1]); ready_pipe[1] = -1;
|
||||
close(continue_pipe[0]); continue_pipe[0] = -1;
|
||||
ready.fd = ready_pipe[0];
|
||||
if (poll(&ready, 1, 5000) != 1 ||
|
||||
read(ready_pipe[0], &byte, 1) != 1 ||
|
||||
trigger_cross_cgroup_reclaim(selected_path))
|
||||
goto cleanup;
|
||||
nanosleep(&leader_exit_delay, NULL);
|
||||
if (write(continue_pipe[1], &byte, 1) != 1)
|
||||
goto cleanup;
|
||||
close(ready_pipe[0]); ready_pipe[0] = -1;
|
||||
close(continue_pipe[1]); continue_pipe[1] = -1;
|
||||
|
||||
for (int i = 0; i < 200; i++) {
|
||||
pid_t waited;
|
||||
|
||||
ring_buffer__poll(ring, 50);
|
||||
waited = waitpid(child, &status, WNOHANG);
|
||||
if (waited == child) {
|
||||
child = -1;
|
||||
break;
|
||||
}
|
||||
}
|
||||
for (int i = 0; i < 10 && exit_events < 1; i++)
|
||||
ring_buffer__poll(ring, 50);
|
||||
|
||||
printf("demo workload signaled=%d signal=%d\n",
|
||||
WIFSIGNALED(status),
|
||||
WIFSIGNALED(status) ? WTERMSIG(status) : 0);
|
||||
if (!WIFSIGNALED(status) || WTERMSIG(status) != SIGKILL ||
|
||||
victim_events != 1 || exit_events != 1 ||
|
||||
observed_cgroup_id != (unsigned long long)cgroup_stat.st_ino ||
|
||||
!observed_victim_pid || !observed_victim_tid ||
|
||||
observed_victim_pid == observed_victim_tid ||
|
||||
!observed_reclaims || !observed_cross_cgroup_reclaims ||
|
||||
skel->bss->dropped_victim_states)
|
||||
goto cleanup;
|
||||
printf("demo result=matched-profile-to-victim\n");
|
||||
} else {
|
||||
signal(SIGINT, handle_signal);
|
||||
signal(SIGTERM, handle_signal);
|
||||
if (options.duration_seconds)
|
||||
deadline = monotonic_ns() +
|
||||
(unsigned long long)options.duration_seconds *
|
||||
1000000000ULL;
|
||||
while (!stop && (!deadline || monotonic_ns() < deadline)) {
|
||||
int poll_result = ring_buffer__poll(ring, 100);
|
||||
|
||||
if (poll_result < 0 && poll_result != -EINTR) {
|
||||
fprintf(stderr, "ring buffer poll failed: %d\n",
|
||||
poll_result);
|
||||
goto cleanup;
|
||||
}
|
||||
}
|
||||
}
|
||||
printf("dropped_victim_states=%llu\n",
|
||||
(unsigned long long)skel->bss->dropped_victim_states);
|
||||
err = 0;
|
||||
|
||||
cleanup:
|
||||
if (child > 0) {
|
||||
kill(child, SIGKILL);
|
||||
waitpid(child, NULL, 0);
|
||||
}
|
||||
for (size_t i = 0; i < 2; i++) {
|
||||
if (ready_pipe[i] >= 0) close(ready_pipe[i]);
|
||||
if (continue_pipe[i] >= 0) close(continue_pipe[i]);
|
||||
}
|
||||
ring_buffer__free(ring);
|
||||
oom_watch_bpf__destroy(skel);
|
||||
if (demo_cgroup_created && rmdir(demo_cgroup_path) && !err)
|
||||
err = 1;
|
||||
if (memory_enabled_by_demo &&
|
||||
write_text("/sys/fs/cgroup/cgroup.subtree_control", "-memory") &&
|
||||
!err)
|
||||
err = 1;
|
||||
return err;
|
||||
}
|
||||
```
|
||||
|
||||
The user space program creates events for OOM victims and their exits. In demo mode, it creates a memory-limited cgroup (32MB), forks a child that joins the cgroup and allocates memory until killed, and triggers cross-cgroup reclaim to generate reclaim profile data before the OOM.
|
||||
|
||||
## Compilation and Execution
|
||||
|
||||
Build the tool:
|
||||
|
||||
```bash
|
||||
cd src/57-oom-watch
|
||||
make
|
||||
```
|
||||
|
||||
Run with cgroup filtering:
|
||||
|
||||
```bash
|
||||
sudo ./oom_watch --cgroup /sys/fs/cgroup/my-service
|
||||
```
|
||||
|
||||
Or watch all OOM events:
|
||||
|
||||
```bash
|
||||
sudo ./oom_watch --duration 60
|
||||
```
|
||||
|
||||
Run the built-in demo:
|
||||
|
||||
```bash
|
||||
sudo ./oom_watch --demo
|
||||
```
|
||||
|
||||
Example output:
|
||||
|
||||
```text
|
||||
oom-watch tracing cgroup=/sys/fs/cgroup/ebpf-oom-watch-1246 cgroup_id=28
|
||||
event=oom-victim pid=1257 tid=1258 comm=oom_watch trigger_pid=1257 cgroup_id=28 anon_rss_kb=32820 file_rss_kb=1576 total_vm_kb=144200 reclaim_cycles=43 cross_cgroup_reclaims=21 reclaimed_pages=40
|
||||
event=victim-exit pid=1257 tid=1258 cgroup_id=28 exit_code=9
|
||||
demo workload signaled=1 signal=9
|
||||
demo result=matched-profile-to-victim
|
||||
dropped_victim_states=0
|
||||
```
|
||||
|
||||
## Requirements
|
||||
|
||||
| Requirement | Details |
|
||||
|-------------|---------|
|
||||
| Kernel | Linux 7.1+ (target memcg in vmscan tracepoints) |
|
||||
| Config | `CONFIG_BPF_SYSCALL`, `CONFIG_DEBUG_INFO_BTF`, `CONFIG_MEMCG` |
|
||||
| Privileges | Root |
|
||||
| cgroup | cgroup v2 with memory controller enabled |
|
||||
|
||||
## Understanding the Output
|
||||
|
||||
The victim event includes:
|
||||
|
||||
- **pid/tid**: TGID and TID of the victim (may differ for multithreaded processes)
|
||||
- **trigger_pid**: The process whose allocation triggered the OOM killer
|
||||
- **anon_rss_kb/file_rss_kb/total_vm_kb**: Memory statistics from the tracepoint
|
||||
- **reclaim_cycles**: How many reclaim attempts occurred in this cgroup
|
||||
- **cross_cgroup_reclaims**: Reclaims triggered by other cgroups
|
||||
- **reclaimed_pages**: Total pages reclaimed before OOM
|
||||
|
||||
The reclaim counters accumulate from the time tracing starts. The `dropped_victim_states` counter records victim states that could not be inserted into the 1024-entry map.
|
||||
|
||||
## Summary
|
||||
|
||||
`oom-watch` joins three moments that are usually inspected separately: memory cgroup reclaim, OOM victim selection, and the victim's eventual exit. The resulting event preserves both TGID and TID, adds the victim's memory footprint, and carries the reclaim profile that accumulated while the tool was tracing.
|
||||
|
||||
> If you'd like to dive deeper into eBPF, check out our tutorial repository at <https://github.com/eunomia-bpf/bpf-developer-tutorial> or visit our website at <https://eunomia.dev/tutorials/>.
|
||||
|
||||
## References
|
||||
|
||||
- [BPF kfuncs documentation](https://docs.kernel.org/6.2/bpf/kfuncs.html)
|
||||
- [OOM tracepoints](https://github.com/torvalds/linux/blob/master/include/trace/events/oom.h)
|
||||
- [vmscan tracepoints](https://github.com/torvalds/linux/blob/master/include/trace/events/vmscan.h)
|
||||
- [cgroup v2 documentation](https://docs.kernel.org/admin-guide/cgroup-v2.html)
|
||||
- [Tracepoint kfunc access commit](https://github.com/torvalds/linux/commit/bc638d8cb5be813d4eeb9f63cce52caaa18f3960)
|
||||
- [vmscan memcg attribution commit](https://github.com/torvalds/linux/commit/874a0a566ede40f3d6062cae8fe1022e616edd1a)
|
||||
759
src/57-oom-watch/README.zh.md
Normal file
759
src/57-oom-watch/README.zh.md
Normal file
@@ -0,0 +1,759 @@
|
||||
# eBPF 实战教程:带回收分析的 OOM 受害者监控
|
||||
|
||||
当内核因内存压力杀死进程时,你如何知道哪个 cgroup 处于压力之下,以及在 OOM killer 介入之前它为回收内存付出了多大努力?本教程构建一个工具,跟踪每个 cgroup 的内存回收活动,并在 OOM killer 选择受害者时捕获详细上下文。
|
||||
|
||||
> 完整源代码:<https://github.com/eunomia-bpf/bpf-developer-tutorial/tree/main/src/57-oom-watch>
|
||||
|
||||
## eBPF 与 OOM 跟踪点
|
||||
|
||||
eBPF 让经过验证器检查的程序运行在 Linux 内核 hook 上,并把选出的状态发送到用户态。本教程使用 `oom/mark_victim` 跟踪点捕获 OOM killer 选择受害者的时刻,再使用 `mm_vmscan_memcg_reclaim_begin/end` 跟踪点记录内存回收活动。Linux 7.1 为这些 vmscan 跟踪点加入目标 `mem_cgroup`,BPF 程序因此可以把回收归因到正在扫描的内存 cgroup,而不是碰巧触发回收的任务。受害者查找使用 Linux 6.2 引入的 `bpf_task_from_pid()` kfunc,普通 tracepoint 程序则从 Linux 6.12 开始访问 tracing kfunc。vmscan 跟踪点的变化是最新依赖,因此本例需要 Linux 7.1 或更高版本。
|
||||
|
||||
## 实现原理
|
||||
|
||||
该工具跟踪 OOM 生命周期的两个阶段。在任何 OOM 事件之前,内核会尝试内存回收。我们统计每个内存 cgroup 的回收周期,跟踪回收了多少页面,并记录何时从不同的 cgroup 触发了回收(跨 cgroup 回收)。当 OOM killer 选择受害者时,我们捕获受害者的内存统计信息,将其与累积的回收分析关联,并跟踪受害者直到它退出。
|
||||
|
||||
## 头文件
|
||||
|
||||
```c
|
||||
/* SPDX-License-Identifier: (LGPL-2.1 OR BSD-2-Clause) */
|
||||
#ifndef __OOM_WATCH_H
|
||||
#define __OOM_WATCH_H
|
||||
|
||||
enum oom_watch_event_type {
|
||||
OOM_VICTIM_MARKED = 1,
|
||||
OOM_VICTIM_EXITED = 2,
|
||||
};
|
||||
|
||||
struct oom_watch_event {
|
||||
unsigned long long timestamp_ns;
|
||||
unsigned long long cgroup_id;
|
||||
unsigned long long reclaim_begin_count;
|
||||
unsigned long long reclaim_end_count;
|
||||
unsigned long long reclaimed_pages;
|
||||
unsigned long long cross_cgroup_reclaims;
|
||||
unsigned long long last_reclaim_ns;
|
||||
unsigned long long total_vm_kb;
|
||||
unsigned long long anon_rss_kb;
|
||||
unsigned long long file_rss_kb;
|
||||
unsigned int type;
|
||||
unsigned int victim_pid;
|
||||
unsigned int victim_tid;
|
||||
unsigned int triggering_tgid;
|
||||
signed int exit_code;
|
||||
char comm[16];
|
||||
};
|
||||
|
||||
#endif /* __OOM_WATCH_H */
|
||||
```
|
||||
|
||||
头文件定义了发送到用户空间的事件结构。每个事件包括 cgroup ID、回收统计(开始/结束计数、回收的页面、跨 cgroup 回收)、来自跟踪点的受害者内存统计,以及受害者终止时的退出代码。
|
||||
|
||||
## BPF 程序
|
||||
|
||||
```c
|
||||
// SPDX-License-Identifier: GPL-2.0
|
||||
#include "vmlinux.h"
|
||||
#include <bpf/bpf_helpers.h>
|
||||
#include <bpf/bpf_tracing.h>
|
||||
#include <bpf/bpf_core_read.h>
|
||||
#include "oom_watch.h"
|
||||
|
||||
char LICENSE[] SEC("license") = "GPL";
|
||||
|
||||
const volatile __u64 target_cgroup_id;
|
||||
|
||||
extern struct task_struct *bpf_task_from_pid(__s32 pid) __ksym;
|
||||
extern void bpf_task_release(struct task_struct *task) __ksym;
|
||||
|
||||
struct reclaim_profile {
|
||||
__u64 begin_count;
|
||||
__u64 end_count;
|
||||
__u64 reclaimed_pages;
|
||||
__u64 cross_cgroup_reclaims;
|
||||
__u64 last_reclaim_ns;
|
||||
};
|
||||
|
||||
struct victim_state {
|
||||
__u64 cgroup_id;
|
||||
__u32 triggering_tgid;
|
||||
__u32 victim_tgid;
|
||||
};
|
||||
|
||||
struct {
|
||||
__uint(type, BPF_MAP_TYPE_LRU_HASH);
|
||||
__uint(max_entries, 4096);
|
||||
__type(key, __u64);
|
||||
__type(value, struct reclaim_profile);
|
||||
} profiles SEC(".maps");
|
||||
|
||||
struct {
|
||||
__uint(type, BPF_MAP_TYPE_HASH);
|
||||
__uint(max_entries, 1024);
|
||||
__type(key, __u32);
|
||||
__type(value, struct victim_state);
|
||||
} victims SEC(".maps");
|
||||
|
||||
struct {
|
||||
__uint(type, BPF_MAP_TYPE_RINGBUF);
|
||||
__uint(max_entries, 256 * 1024);
|
||||
} events SEC(".maps");
|
||||
|
||||
__u64 dropped_victim_states;
|
||||
|
||||
static __always_inline bool selected_cgroup(__u64 cgroup_id)
|
||||
{
|
||||
return !target_cgroup_id || cgroup_id == target_cgroup_id;
|
||||
}
|
||||
|
||||
static __always_inline __u64 victim_task_info(__u32 victim_pid,
|
||||
__u32 *victim_tgid)
|
||||
{
|
||||
struct task_struct *task;
|
||||
__u64 cgroup_id = 0;
|
||||
|
||||
task = bpf_task_from_pid(victim_pid);
|
||||
if (!task)
|
||||
return 0;
|
||||
cgroup_id = BPF_CORE_READ(task, cgroups, dfl_cgrp, kn, id);
|
||||
*victim_tgid = BPF_CORE_READ(task, tgid);
|
||||
bpf_task_release(task);
|
||||
return cgroup_id;
|
||||
}
|
||||
|
||||
static __always_inline __u64 memcg_cgroup_id(struct mem_cgroup *memcg)
|
||||
{
|
||||
struct cgroup *cgroup;
|
||||
|
||||
cgroup = BPF_CORE_READ(memcg, css.cgroup);
|
||||
if (!cgroup)
|
||||
return 0;
|
||||
return BPF_CORE_READ(cgroup, kn, id);
|
||||
}
|
||||
|
||||
static __always_inline struct reclaim_profile *get_profile(__u64 cgroup_id)
|
||||
{
|
||||
struct reclaim_profile initial = {};
|
||||
struct reclaim_profile *profile;
|
||||
|
||||
profile = bpf_map_lookup_elem(&profiles, &cgroup_id);
|
||||
if (profile)
|
||||
return profile;
|
||||
bpf_map_update_elem(&profiles, &cgroup_id, &initial, BPF_NOEXIST);
|
||||
return bpf_map_lookup_elem(&profiles, &cgroup_id);
|
||||
}
|
||||
|
||||
SEC("tp_btf/mm_vmscan_memcg_reclaim_begin")
|
||||
int BPF_PROG(track_reclaim_begin, gfp_t gfp_flags, int order,
|
||||
struct mem_cgroup *memcg)
|
||||
{
|
||||
struct reclaim_profile *profile;
|
||||
__u64 cgroup_id = memcg_cgroup_id(memcg);
|
||||
|
||||
(void)gfp_flags;
|
||||
(void)order;
|
||||
if (!selected_cgroup(cgroup_id))
|
||||
return 0;
|
||||
profile = get_profile(cgroup_id);
|
||||
if (!profile)
|
||||
return 0;
|
||||
__sync_fetch_and_add(&profile->begin_count, 1);
|
||||
if (bpf_get_current_cgroup_id() != cgroup_id)
|
||||
__sync_fetch_and_add(&profile->cross_cgroup_reclaims, 1);
|
||||
profile->last_reclaim_ns = bpf_ktime_get_ns();
|
||||
return 0;
|
||||
}
|
||||
|
||||
SEC("tp_btf/mm_vmscan_memcg_reclaim_end")
|
||||
int BPF_PROG(track_reclaim_end, unsigned long reclaimed,
|
||||
struct mem_cgroup *memcg)
|
||||
{
|
||||
struct reclaim_profile *profile;
|
||||
__u64 cgroup_id = memcg_cgroup_id(memcg);
|
||||
|
||||
if (!selected_cgroup(cgroup_id))
|
||||
return 0;
|
||||
profile = get_profile(cgroup_id);
|
||||
if (!profile)
|
||||
return 0;
|
||||
__sync_fetch_and_add(&profile->end_count, 1);
|
||||
__sync_fetch_and_add(&profile->reclaimed_pages, reclaimed);
|
||||
profile->last_reclaim_ns = bpf_ktime_get_ns();
|
||||
return 0;
|
||||
}
|
||||
|
||||
SEC("tracepoint/oom/mark_victim")
|
||||
int capture_oom_victim(struct trace_event_raw_mark_victim *ctx)
|
||||
{
|
||||
struct reclaim_profile *profile;
|
||||
struct victim_state victim;
|
||||
struct oom_watch_event *event;
|
||||
__u64 cgroup_id;
|
||||
__u32 victim_pid = ctx->pid;
|
||||
__u32 victim_tgid = 0;
|
||||
|
||||
cgroup_id = victim_task_info(victim_pid, &victim_tgid);
|
||||
if (!victim_tgid || !selected_cgroup(cgroup_id))
|
||||
return 0;
|
||||
victim.cgroup_id = cgroup_id;
|
||||
victim.triggering_tgid = bpf_get_current_pid_tgid() >> 32;
|
||||
victim.victim_tgid = victim_tgid;
|
||||
if (bpf_map_update_elem(&victims, &victim_pid, &victim, BPF_ANY)) {
|
||||
__sync_fetch_and_add(&dropped_victim_states, 1);
|
||||
return 0;
|
||||
}
|
||||
|
||||
event = bpf_ringbuf_reserve(&events, sizeof(*event), 0);
|
||||
if (!event)
|
||||
return 0;
|
||||
__builtin_memset(event, 0, sizeof(*event));
|
||||
event->timestamp_ns = bpf_ktime_get_ns();
|
||||
event->cgroup_id = cgroup_id;
|
||||
event->type = OOM_VICTIM_MARKED;
|
||||
event->victim_pid = victim_tgid;
|
||||
event->victim_tid = victim_pid;
|
||||
event->triggering_tgid = victim.triggering_tgid;
|
||||
event->total_vm_kb = ctx->total_vm;
|
||||
event->anon_rss_kb = ctx->anon_rss;
|
||||
event->file_rss_kb = ctx->file_rss;
|
||||
bpf_probe_read_kernel_str(event->comm, sizeof(event->comm),
|
||||
(void *)ctx + (ctx->__data_loc_comm & 0xffff));
|
||||
|
||||
profile = bpf_map_lookup_elem(&profiles, &cgroup_id);
|
||||
if (profile) {
|
||||
event->reclaim_begin_count = profile->begin_count;
|
||||
event->reclaim_end_count = profile->end_count;
|
||||
event->reclaimed_pages = profile->reclaimed_pages;
|
||||
event->cross_cgroup_reclaims = profile->cross_cgroup_reclaims;
|
||||
event->last_reclaim_ns = profile->last_reclaim_ns;
|
||||
}
|
||||
bpf_ringbuf_submit(event, 0);
|
||||
return 0;
|
||||
}
|
||||
|
||||
SEC("tracepoint/sched/sched_process_exit")
|
||||
int capture_victim_exit(void *ctx)
|
||||
{
|
||||
struct victim_state *victim;
|
||||
struct task_struct *task;
|
||||
struct oom_watch_event *event;
|
||||
__u64 pid_tgid = bpf_get_current_pid_tgid();
|
||||
__u32 tid = pid_tgid;
|
||||
|
||||
(void)ctx;
|
||||
victim = bpf_map_lookup_elem(&victims, &tid);
|
||||
if (!victim)
|
||||
return 0;
|
||||
event = bpf_ringbuf_reserve(&events, sizeof(*event), 0);
|
||||
if (event) {
|
||||
__builtin_memset(event, 0, sizeof(*event));
|
||||
event->timestamp_ns = bpf_ktime_get_ns();
|
||||
event->cgroup_id = victim->cgroup_id;
|
||||
event->type = OOM_VICTIM_EXITED;
|
||||
event->victim_pid = victim->victim_tgid;
|
||||
event->victim_tid = tid;
|
||||
event->triggering_tgid = victim->triggering_tgid;
|
||||
task = (struct task_struct *)bpf_get_current_task_btf();
|
||||
event->exit_code = BPF_CORE_READ(task, exit_code);
|
||||
bpf_get_current_comm(event->comm, sizeof(event->comm));
|
||||
bpf_ringbuf_submit(event, 0);
|
||||
}
|
||||
bpf_map_delete_elem(&victims, &tid);
|
||||
return 0;
|
||||
}
|
||||
```
|
||||
|
||||
BPF 程序使用 `bpf_task_from_pid()` kfunc 从 PID 查找受害者任务,这个 kfunc 在 Linux 6.2 中引入,返回的引用指针需要用 `bpf_task_release()` 释放。`BPF_PROG_TYPE_TRACEPOINT` 程序从 Linux 6.12 开始访问 tracing kfunc,回收回调还会读取 Linux 7.1 加入 vmscan 跟踪点的目标 `mem_cgroup` 参数,因此完整工具的最低内核版本是 Linux 7.1。
|
||||
|
||||
`track_reclaim_begin` 和 `track_reclaim_end` 函数附加到 memcg 回收跟踪点。它们统计回收周期并跟踪跨 cgroup 回收(当一个 cgroup 中的任务触发另一个 cgroup 内存空间的回收时)。
|
||||
|
||||
`capture_oom_victim` 函数在 OOM killer 选择受害者时运行。它使用 `bpf_task_from_pid()` 获取受害者的 cgroup ID 和 TGID,存储受害者状态以进行退出跟踪,并发送包含内存统计和累积回收分析的事件。
|
||||
|
||||
`capture_victim_exit` 函数跟踪受害者实际退出的时间,确认杀死完成并报告退出信号。
|
||||
|
||||
## 用户空间程序
|
||||
|
||||
```c
|
||||
// SPDX-License-Identifier: (LGPL-2.1 OR BSD-2-Clause)
|
||||
#include <errno.h>
|
||||
#include <fcntl.h>
|
||||
#include <getopt.h>
|
||||
#include <poll.h>
|
||||
#include <pthread.h>
|
||||
#include <signal.h>
|
||||
#include <stdbool.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <sys/mman.h>
|
||||
#include <sys/stat.h>
|
||||
#include <sys/types.h>
|
||||
#include <sys/wait.h>
|
||||
#include <time.h>
|
||||
#include <unistd.h>
|
||||
#include <bpf/libbpf.h>
|
||||
#include "oom_watch.h"
|
||||
#include "oom_watch.skel.h"
|
||||
|
||||
struct options {
|
||||
const char *cgroup_path;
|
||||
unsigned int duration_seconds;
|
||||
bool demo;
|
||||
};
|
||||
|
||||
static volatile sig_atomic_t stop;
|
||||
static int victim_events;
|
||||
static int exit_events;
|
||||
static unsigned long long observed_cgroup_id;
|
||||
static unsigned int observed_victim_pid;
|
||||
static unsigned int observed_victim_tid;
|
||||
static unsigned long long observed_reclaims;
|
||||
static unsigned long long observed_cross_cgroup_reclaims;
|
||||
|
||||
struct allocation_context {
|
||||
int ready_fd;
|
||||
int continue_fd;
|
||||
};
|
||||
|
||||
static struct allocation_context allocation_context;
|
||||
|
||||
static void handle_signal(int signal_number)
|
||||
{
|
||||
(void)signal_number;
|
||||
stop = 1;
|
||||
}
|
||||
|
||||
static unsigned long long monotonic_ns(void)
|
||||
{
|
||||
struct timespec now;
|
||||
|
||||
clock_gettime(CLOCK_MONOTONIC, &now);
|
||||
return (unsigned long long)now.tv_sec * 1000000000ULL + now.tv_nsec;
|
||||
}
|
||||
|
||||
static int write_text(const char *path, const char *text)
|
||||
{
|
||||
int fd = open(path, O_WRONLY | O_CLOEXEC);
|
||||
ssize_t length = strlen(text);
|
||||
int err = 0;
|
||||
|
||||
if (fd < 0)
|
||||
return -1;
|
||||
if (write(fd, text, length) != length)
|
||||
err = -1;
|
||||
close(fd);
|
||||
return err;
|
||||
}
|
||||
|
||||
static int memory_controller_enabled(bool *enabled)
|
||||
{
|
||||
char controllers[4096];
|
||||
ssize_t length;
|
||||
int fd;
|
||||
|
||||
fd = open("/sys/fs/cgroup/cgroup.subtree_control",
|
||||
O_RDONLY | O_CLOEXEC);
|
||||
if (fd < 0)
|
||||
return -1;
|
||||
length = read(fd, controllers, sizeof(controllers) - 1);
|
||||
close(fd);
|
||||
if (length < 0)
|
||||
return -1;
|
||||
controllers[length] = '\0';
|
||||
*enabled = strstr(controllers, "memory") != NULL;
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int handle_event(void *ctx, void *data, size_t size)
|
||||
{
|
||||
const struct oom_watch_event *event = data;
|
||||
|
||||
(void)ctx;
|
||||
if (size != sizeof(*event))
|
||||
return 0;
|
||||
if (event->type == OOM_VICTIM_MARKED) {
|
||||
victim_events++;
|
||||
observed_cgroup_id = event->cgroup_id;
|
||||
observed_victim_pid = event->victim_pid;
|
||||
observed_reclaims = event->reclaim_begin_count;
|
||||
observed_victim_tid = event->victim_tid;
|
||||
observed_cross_cgroup_reclaims = event->cross_cgroup_reclaims;
|
||||
printf("event=oom-victim pid=%u tid=%u comm=%s trigger_pid=%u cgroup_id=%llu "
|
||||
"anon_rss_kb=%llu file_rss_kb=%llu total_vm_kb=%llu "
|
||||
"reclaim_cycles=%llu cross_cgroup_reclaims=%llu "
|
||||
"reclaimed_pages=%llu\n",
|
||||
event->victim_pid, event->victim_tid, event->comm,
|
||||
event->triggering_tgid,
|
||||
(unsigned long long)event->cgroup_id,
|
||||
(unsigned long long)event->anon_rss_kb,
|
||||
(unsigned long long)event->file_rss_kb,
|
||||
(unsigned long long)event->total_vm_kb,
|
||||
(unsigned long long)event->reclaim_begin_count,
|
||||
(unsigned long long)event->cross_cgroup_reclaims,
|
||||
(unsigned long long)event->reclaimed_pages);
|
||||
} else if (event->type == OOM_VICTIM_EXITED) {
|
||||
exit_events++;
|
||||
printf("event=victim-exit pid=%u tid=%u cgroup_id=%llu exit_code=%d\n",
|
||||
event->victim_pid, event->victim_tid,
|
||||
(unsigned long long)event->cgroup_id, event->exit_code);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int parse_uint(const char *text, unsigned int *value)
|
||||
{
|
||||
char *end = NULL;
|
||||
unsigned long parsed;
|
||||
|
||||
errno = 0;
|
||||
parsed = strtoul(text, &end, 10);
|
||||
if (errno || !*text || *end || parsed > 86400)
|
||||
return -1;
|
||||
*value = parsed;
|
||||
return 0;
|
||||
}
|
||||
|
||||
static void usage(const char *program)
|
||||
{
|
||||
printf("Usage: %s [--cgroup PATH] [--duration SEC]\n"
|
||||
" %s --demo\n", program, program);
|
||||
}
|
||||
|
||||
static int parse_options(int argc, char **argv, struct options *options)
|
||||
{
|
||||
static const struct option long_options[] = {
|
||||
{ "cgroup", required_argument, NULL, 'c' },
|
||||
{ "duration", required_argument, NULL, 'd' },
|
||||
{ "demo", no_argument, NULL, 'D' },
|
||||
{ "help", no_argument, NULL, 'h' },
|
||||
{},
|
||||
};
|
||||
int option;
|
||||
|
||||
while ((option = getopt_long(argc, argv, "c:d:Dh", long_options,
|
||||
NULL)) != -1) {
|
||||
switch (option) {
|
||||
case 'c': options->cgroup_path = optarg; break;
|
||||
case 'd':
|
||||
if (parse_uint(optarg, &options->duration_seconds))
|
||||
return -1;
|
||||
break;
|
||||
case 'D': options->demo = true; break;
|
||||
case 'h': usage(argv[0]); exit(0);
|
||||
default: return -1;
|
||||
}
|
||||
}
|
||||
return optind == argc && !(options->demo && options->cgroup_path) ? 0 : -1;
|
||||
}
|
||||
|
||||
static void *allocation_worker(void *argument)
|
||||
{
|
||||
struct allocation_context *context = argument;
|
||||
size_t first_stage = 24 * 1024 * 1024;
|
||||
size_t length = 128 * 1024 * 1024;
|
||||
unsigned char *memory;
|
||||
char byte = 'x';
|
||||
|
||||
memory = mmap(NULL, length, PROT_READ | PROT_WRITE,
|
||||
MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
|
||||
if (memory == MAP_FAILED)
|
||||
_exit(4);
|
||||
for (size_t offset = 0; offset < first_stage; offset += 4096)
|
||||
memory[offset] = 0xa5;
|
||||
if (write(context->ready_fd, &byte, 1) != 1 ||
|
||||
read(context->continue_fd, &byte, 1) != 1)
|
||||
_exit(5);
|
||||
for (size_t offset = first_stage; offset < length; offset += 4096)
|
||||
memory[offset] = 0xa5;
|
||||
_exit(6);
|
||||
}
|
||||
|
||||
static void allocate_until_killed(const char *cgroup_path, int ready_fd,
|
||||
int continue_fd)
|
||||
{
|
||||
char procs_path[512];
|
||||
char pid_text[32];
|
||||
pthread_t worker;
|
||||
|
||||
snprintf(procs_path, sizeof(procs_path), "%s/cgroup.procs", cgroup_path);
|
||||
snprintf(pid_text, sizeof(pid_text), "%d", getpid());
|
||||
if (write_text(procs_path, pid_text))
|
||||
_exit(3);
|
||||
allocation_context.ready_fd = ready_fd;
|
||||
allocation_context.continue_fd = continue_fd;
|
||||
if (pthread_create(&worker, NULL, allocation_worker,
|
||||
&allocation_context))
|
||||
_exit(4);
|
||||
pthread_detach(worker);
|
||||
pthread_exit(NULL);
|
||||
}
|
||||
|
||||
static int configure_demo_cgroup(const char *path, bool *created,
|
||||
bool *enabled_by_demo)
|
||||
{
|
||||
bool memory_enabled;
|
||||
char file[512];
|
||||
|
||||
if (memory_controller_enabled(&memory_enabled))
|
||||
return -1;
|
||||
if (!memory_enabled) {
|
||||
if (write_text("/sys/fs/cgroup/cgroup.subtree_control", "+memory"))
|
||||
return -1;
|
||||
*enabled_by_demo = true;
|
||||
}
|
||||
if (mkdir(path, 0755))
|
||||
return -1;
|
||||
*created = true;
|
||||
snprintf(file, sizeof(file), "%s/memory.max", path);
|
||||
if (write_text(file, "33554432"))
|
||||
return -1;
|
||||
snprintf(file, sizeof(file), "%s/memory.swap.max", path);
|
||||
if (write_text(file, "0"))
|
||||
return -1;
|
||||
snprintf(file, sizeof(file), "%s/memory.oom.group", path);
|
||||
return write_text(file, "1");
|
||||
}
|
||||
|
||||
static int trigger_cross_cgroup_reclaim(const char *cgroup_path)
|
||||
{
|
||||
char reclaim_path[512];
|
||||
|
||||
snprintf(reclaim_path, sizeof(reclaim_path), "%s/memory.reclaim",
|
||||
cgroup_path);
|
||||
if (!write_text(reclaim_path, "8388608") || errno == EAGAIN)
|
||||
return 0;
|
||||
return -1;
|
||||
}
|
||||
|
||||
int main(int argc, char **argv)
|
||||
{
|
||||
struct options options = {};
|
||||
struct oom_watch_bpf *skel = NULL;
|
||||
struct ring_buffer *ring = NULL;
|
||||
char demo_cgroup_path[256];
|
||||
unsigned long long deadline = 0;
|
||||
struct stat cgroup_stat = {};
|
||||
const char *selected_path = NULL;
|
||||
pid_t child = -1;
|
||||
int ready_pipe[2] = { -1, -1 };
|
||||
int continue_pipe[2] = { -1, -1 };
|
||||
int status = 0;
|
||||
int err = 1;
|
||||
bool demo_cgroup_created = false;
|
||||
bool memory_enabled_by_demo = false;
|
||||
|
||||
setvbuf(stdout, NULL, _IONBF, 0);
|
||||
if (parse_options(argc, argv, &options)) {
|
||||
usage(argv[0]);
|
||||
return 2;
|
||||
}
|
||||
if (options.demo) {
|
||||
snprintf(demo_cgroup_path, sizeof(demo_cgroup_path),
|
||||
"/sys/fs/cgroup/ebpf-oom-watch-%d", getpid());
|
||||
if (configure_demo_cgroup(demo_cgroup_path,
|
||||
&demo_cgroup_created,
|
||||
&memory_enabled_by_demo)) {
|
||||
fprintf(stderr, "failed to configure demo memory cgroup: %s\n",
|
||||
strerror(errno));
|
||||
goto cleanup;
|
||||
}
|
||||
selected_path = demo_cgroup_path;
|
||||
} else {
|
||||
selected_path = options.cgroup_path;
|
||||
}
|
||||
if (selected_path && stat(selected_path, &cgroup_stat)) {
|
||||
fprintf(stderr, "failed to stat cgroup %s: %s\n", selected_path,
|
||||
strerror(errno));
|
||||
goto cleanup;
|
||||
}
|
||||
|
||||
skel = oom_watch_bpf__open();
|
||||
if (!skel)
|
||||
goto cleanup;
|
||||
skel->rodata->target_cgroup_id = selected_path ? cgroup_stat.st_ino : 0;
|
||||
if (oom_watch_bpf__load(skel) || oom_watch_bpf__attach(skel)) {
|
||||
fprintf(stderr, "failed to load and attach OOM watcher\n");
|
||||
goto cleanup;
|
||||
}
|
||||
ring = ring_buffer__new(bpf_map__fd(skel->maps.events), handle_event,
|
||||
NULL, NULL);
|
||||
if (!ring)
|
||||
goto cleanup;
|
||||
|
||||
if (selected_path)
|
||||
printf("oom-watch tracing cgroup=%s cgroup_id=%llu\n",
|
||||
selected_path, (unsigned long long)cgroup_stat.st_ino);
|
||||
else
|
||||
printf("oom-watch tracing all cgroups\n");
|
||||
|
||||
if (options.demo) {
|
||||
struct pollfd ready = { .events = POLLIN };
|
||||
struct timespec leader_exit_delay = { .tv_nsec = 100000000 };
|
||||
char byte = 'x';
|
||||
|
||||
if (pipe(ready_pipe) || pipe(continue_pipe))
|
||||
goto cleanup;
|
||||
child = fork();
|
||||
if (child < 0)
|
||||
goto cleanup;
|
||||
if (!child) {
|
||||
close(ready_pipe[0]);
|
||||
close(continue_pipe[1]);
|
||||
allocate_until_killed(selected_path, ready_pipe[1],
|
||||
continue_pipe[0]);
|
||||
}
|
||||
close(ready_pipe[1]); ready_pipe[1] = -1;
|
||||
close(continue_pipe[0]); continue_pipe[0] = -1;
|
||||
ready.fd = ready_pipe[0];
|
||||
if (poll(&ready, 1, 5000) != 1 ||
|
||||
read(ready_pipe[0], &byte, 1) != 1 ||
|
||||
trigger_cross_cgroup_reclaim(selected_path))
|
||||
goto cleanup;
|
||||
nanosleep(&leader_exit_delay, NULL);
|
||||
if (write(continue_pipe[1], &byte, 1) != 1)
|
||||
goto cleanup;
|
||||
close(ready_pipe[0]); ready_pipe[0] = -1;
|
||||
close(continue_pipe[1]); continue_pipe[1] = -1;
|
||||
|
||||
for (int i = 0; i < 200; i++) {
|
||||
pid_t waited;
|
||||
|
||||
ring_buffer__poll(ring, 50);
|
||||
waited = waitpid(child, &status, WNOHANG);
|
||||
if (waited == child) {
|
||||
child = -1;
|
||||
break;
|
||||
}
|
||||
}
|
||||
for (int i = 0; i < 10 && exit_events < 1; i++)
|
||||
ring_buffer__poll(ring, 50);
|
||||
|
||||
printf("demo workload signaled=%d signal=%d\n",
|
||||
WIFSIGNALED(status),
|
||||
WIFSIGNALED(status) ? WTERMSIG(status) : 0);
|
||||
if (!WIFSIGNALED(status) || WTERMSIG(status) != SIGKILL ||
|
||||
victim_events != 1 || exit_events != 1 ||
|
||||
observed_cgroup_id != (unsigned long long)cgroup_stat.st_ino ||
|
||||
!observed_victim_pid || !observed_victim_tid ||
|
||||
observed_victim_pid == observed_victim_tid ||
|
||||
!observed_reclaims || !observed_cross_cgroup_reclaims ||
|
||||
skel->bss->dropped_victim_states)
|
||||
goto cleanup;
|
||||
printf("demo result=matched-profile-to-victim\n");
|
||||
} else {
|
||||
signal(SIGINT, handle_signal);
|
||||
signal(SIGTERM, handle_signal);
|
||||
if (options.duration_seconds)
|
||||
deadline = monotonic_ns() +
|
||||
(unsigned long long)options.duration_seconds *
|
||||
1000000000ULL;
|
||||
while (!stop && (!deadline || monotonic_ns() < deadline)) {
|
||||
int poll_result = ring_buffer__poll(ring, 100);
|
||||
|
||||
if (poll_result < 0 && poll_result != -EINTR) {
|
||||
fprintf(stderr, "ring buffer poll failed: %d\n",
|
||||
poll_result);
|
||||
goto cleanup;
|
||||
}
|
||||
}
|
||||
}
|
||||
printf("dropped_victim_states=%llu\n",
|
||||
(unsigned long long)skel->bss->dropped_victim_states);
|
||||
err = 0;
|
||||
|
||||
cleanup:
|
||||
if (child > 0) {
|
||||
kill(child, SIGKILL);
|
||||
waitpid(child, NULL, 0);
|
||||
}
|
||||
for (size_t i = 0; i < 2; i++) {
|
||||
if (ready_pipe[i] >= 0) close(ready_pipe[i]);
|
||||
if (continue_pipe[i] >= 0) close(continue_pipe[i]);
|
||||
}
|
||||
ring_buffer__free(ring);
|
||||
oom_watch_bpf__destroy(skel);
|
||||
if (demo_cgroup_created && rmdir(demo_cgroup_path) && !err)
|
||||
err = 1;
|
||||
if (memory_enabled_by_demo &&
|
||||
write_text("/sys/fs/cgroup/cgroup.subtree_control", "-memory") &&
|
||||
!err)
|
||||
err = 1;
|
||||
return err;
|
||||
}
|
||||
```
|
||||
|
||||
用户空间程序为 OOM 受害者及其退出创建事件。在演示模式下,它创建一个内存受限的 cgroup(32MB),fork 一个子进程加入该 cgroup 并分配内存直到被杀死,并触发跨 cgroup 回收以在 OOM 之前生成回收分析数据。
|
||||
|
||||
## 编译和执行
|
||||
|
||||
构建工具:
|
||||
|
||||
```bash
|
||||
cd src/57-oom-watch
|
||||
make
|
||||
```
|
||||
|
||||
使用 cgroup 过滤运行:
|
||||
|
||||
```bash
|
||||
sudo ./oom_watch --cgroup /sys/fs/cgroup/my-service
|
||||
```
|
||||
|
||||
或监视所有 OOM 事件:
|
||||
|
||||
```bash
|
||||
sudo ./oom_watch --duration 60
|
||||
```
|
||||
|
||||
运行内置演示:
|
||||
|
||||
```bash
|
||||
sudo ./oom_watch --demo
|
||||
```
|
||||
|
||||
示例输出:
|
||||
|
||||
```text
|
||||
oom-watch tracing cgroup=/sys/fs/cgroup/ebpf-oom-watch-1246 cgroup_id=28
|
||||
event=oom-victim pid=1257 tid=1258 comm=oom_watch trigger_pid=1257 cgroup_id=28 anon_rss_kb=32820 file_rss_kb=1576 total_vm_kb=144200 reclaim_cycles=43 cross_cgroup_reclaims=21 reclaimed_pages=40
|
||||
event=victim-exit pid=1257 tid=1258 cgroup_id=28 exit_code=9
|
||||
demo workload signaled=1 signal=9
|
||||
demo result=matched-profile-to-victim
|
||||
dropped_victim_states=0
|
||||
```
|
||||
|
||||
## 环境要求
|
||||
|
||||
| 要求 | 详情 |
|
||||
|------|------|
|
||||
| 内核 | Linux 7.1+(vmscan 跟踪点提供目标 memcg) |
|
||||
| 配置 | `CONFIG_BPF_SYSCALL`、`CONFIG_DEBUG_INFO_BTF`、`CONFIG_MEMCG` |
|
||||
| 权限 | Root |
|
||||
| cgroup | 启用内存控制器的 cgroup v2 |
|
||||
|
||||
## 理解输出
|
||||
|
||||
受害者事件包括:
|
||||
|
||||
- **pid/tid**:受害者的 TGID 和 TID(对于多线程进程可能不同)
|
||||
- **trigger_pid**:其分配触发 OOM killer 的进程
|
||||
- **anon_rss_kb/file_rss_kb/total_vm_kb**:来自跟踪点的内存统计
|
||||
- **reclaim_cycles**:在此 cgroup 中发生了多少次回收尝试
|
||||
- **cross_cgroup_reclaims**:由其他 cgroup 触发的回收
|
||||
- **reclaimed_pages**:OOM 之前回收的总页数
|
||||
|
||||
回收计数从工具开始跟踪时持续累积,`dropped_victim_states` 则记录未能写入 1024 条目受害者映射的状态。
|
||||
|
||||
## 总结
|
||||
|
||||
`oom-watch` 把通常分开查看的三个时刻连在一起,包括内存 cgroup 回收、OOM 受害者选择以及受害者最终退出。事件同时保留 TGID 和 TID,记录受害者的内存占用,并带上工具运行期间积累的回收画像。
|
||||
|
||||
> 如果你想深入了解 eBPF,请查看我们的教程代码仓库 <https://github.com/eunomia-bpf/bpf-developer-tutorial> 或访问我们的网站 <https://eunomia.dev/tutorials/>。
|
||||
|
||||
## 参考资料
|
||||
|
||||
- [BPF kfuncs 文档](https://docs.kernel.org/6.2/bpf/kfuncs.html)
|
||||
- [OOM 跟踪点](https://github.com/torvalds/linux/blob/master/include/trace/events/oom.h)
|
||||
- [vmscan 跟踪点](https://github.com/torvalds/linux/blob/master/include/trace/events/vmscan.h)
|
||||
- [cgroup v2 文档](https://docs.kernel.org/admin-guide/cgroup-v2.html)
|
||||
- [Tracepoint kfunc 访问 commit](https://github.com/torvalds/linux/commit/bc638d8cb5be813d4eeb9f63cce52caaa18f3960)
|
||||
- [vmscan memcg 归因 commit](https://github.com/torvalds/linux/commit/874a0a566ede40f3d6062cae8fe1022e616edd1a)
|
||||
209
src/57-oom-watch/oom_watch.bpf.c
Normal file
209
src/57-oom-watch/oom_watch.bpf.c
Normal file
@@ -0,0 +1,209 @@
|
||||
// SPDX-License-Identifier: GPL-2.0
|
||||
#include "vmlinux.h"
|
||||
#include <bpf/bpf_helpers.h>
|
||||
#include <bpf/bpf_tracing.h>
|
||||
#include <bpf/bpf_core_read.h>
|
||||
#include "oom_watch.h"
|
||||
|
||||
char LICENSE[] SEC("license") = "GPL";
|
||||
|
||||
const volatile __u64 target_cgroup_id;
|
||||
|
||||
extern struct task_struct *bpf_task_from_pid(__s32 pid) __ksym;
|
||||
extern void bpf_task_release(struct task_struct *task) __ksym;
|
||||
|
||||
struct reclaim_profile {
|
||||
__u64 begin_count;
|
||||
__u64 end_count;
|
||||
__u64 reclaimed_pages;
|
||||
__u64 cross_cgroup_reclaims;
|
||||
__u64 last_reclaim_ns;
|
||||
};
|
||||
|
||||
struct victim_state {
|
||||
__u64 cgroup_id;
|
||||
__u32 triggering_tgid;
|
||||
__u32 victim_tgid;
|
||||
};
|
||||
|
||||
struct {
|
||||
__uint(type, BPF_MAP_TYPE_LRU_HASH);
|
||||
__uint(max_entries, 4096);
|
||||
__type(key, __u64);
|
||||
__type(value, struct reclaim_profile);
|
||||
} profiles SEC(".maps");
|
||||
|
||||
struct {
|
||||
__uint(type, BPF_MAP_TYPE_HASH);
|
||||
__uint(max_entries, 1024);
|
||||
__type(key, __u32);
|
||||
__type(value, struct victim_state);
|
||||
} victims SEC(".maps");
|
||||
|
||||
struct {
|
||||
__uint(type, BPF_MAP_TYPE_RINGBUF);
|
||||
__uint(max_entries, 256 * 1024);
|
||||
} events SEC(".maps");
|
||||
|
||||
__u64 dropped_victim_states;
|
||||
|
||||
static __always_inline bool selected_cgroup(__u64 cgroup_id)
|
||||
{
|
||||
return !target_cgroup_id || cgroup_id == target_cgroup_id;
|
||||
}
|
||||
|
||||
static __always_inline __u64 victim_task_info(__u32 victim_pid,
|
||||
__u32 *victim_tgid)
|
||||
{
|
||||
struct task_struct *task;
|
||||
__u64 cgroup_id = 0;
|
||||
|
||||
task = bpf_task_from_pid(victim_pid);
|
||||
if (!task)
|
||||
return 0;
|
||||
cgroup_id = BPF_CORE_READ(task, cgroups, dfl_cgrp, kn, id);
|
||||
*victim_tgid = BPF_CORE_READ(task, tgid);
|
||||
bpf_task_release(task);
|
||||
return cgroup_id;
|
||||
}
|
||||
|
||||
static __always_inline __u64 memcg_cgroup_id(struct mem_cgroup *memcg)
|
||||
{
|
||||
struct cgroup *cgroup;
|
||||
|
||||
cgroup = BPF_CORE_READ(memcg, css.cgroup);
|
||||
if (!cgroup)
|
||||
return 0;
|
||||
return BPF_CORE_READ(cgroup, kn, id);
|
||||
}
|
||||
|
||||
static __always_inline struct reclaim_profile *get_profile(__u64 cgroup_id)
|
||||
{
|
||||
struct reclaim_profile initial = {};
|
||||
struct reclaim_profile *profile;
|
||||
|
||||
profile = bpf_map_lookup_elem(&profiles, &cgroup_id);
|
||||
if (profile)
|
||||
return profile;
|
||||
bpf_map_update_elem(&profiles, &cgroup_id, &initial, BPF_NOEXIST);
|
||||
return bpf_map_lookup_elem(&profiles, &cgroup_id);
|
||||
}
|
||||
|
||||
SEC("tp_btf/mm_vmscan_memcg_reclaim_begin")
|
||||
int BPF_PROG(track_reclaim_begin, gfp_t gfp_flags, int order,
|
||||
struct mem_cgroup *memcg)
|
||||
{
|
||||
struct reclaim_profile *profile;
|
||||
__u64 cgroup_id = memcg_cgroup_id(memcg);
|
||||
|
||||
(void)gfp_flags;
|
||||
(void)order;
|
||||
if (!selected_cgroup(cgroup_id))
|
||||
return 0;
|
||||
profile = get_profile(cgroup_id);
|
||||
if (!profile)
|
||||
return 0;
|
||||
__sync_fetch_and_add(&profile->begin_count, 1);
|
||||
if (bpf_get_current_cgroup_id() != cgroup_id)
|
||||
__sync_fetch_and_add(&profile->cross_cgroup_reclaims, 1);
|
||||
profile->last_reclaim_ns = bpf_ktime_get_ns();
|
||||
return 0;
|
||||
}
|
||||
|
||||
SEC("tp_btf/mm_vmscan_memcg_reclaim_end")
|
||||
int BPF_PROG(track_reclaim_end, unsigned long reclaimed,
|
||||
struct mem_cgroup *memcg)
|
||||
{
|
||||
struct reclaim_profile *profile;
|
||||
__u64 cgroup_id = memcg_cgroup_id(memcg);
|
||||
|
||||
if (!selected_cgroup(cgroup_id))
|
||||
return 0;
|
||||
profile = get_profile(cgroup_id);
|
||||
if (!profile)
|
||||
return 0;
|
||||
__sync_fetch_and_add(&profile->end_count, 1);
|
||||
__sync_fetch_and_add(&profile->reclaimed_pages, reclaimed);
|
||||
profile->last_reclaim_ns = bpf_ktime_get_ns();
|
||||
return 0;
|
||||
}
|
||||
|
||||
SEC("tracepoint/oom/mark_victim")
|
||||
int capture_oom_victim(struct trace_event_raw_mark_victim *ctx)
|
||||
{
|
||||
struct reclaim_profile *profile;
|
||||
struct victim_state victim;
|
||||
struct oom_watch_event *event;
|
||||
__u64 cgroup_id;
|
||||
__u32 victim_pid = ctx->pid;
|
||||
__u32 victim_tgid = 0;
|
||||
|
||||
cgroup_id = victim_task_info(victim_pid, &victim_tgid);
|
||||
if (!victim_tgid || !selected_cgroup(cgroup_id))
|
||||
return 0;
|
||||
victim.cgroup_id = cgroup_id;
|
||||
victim.triggering_tgid = bpf_get_current_pid_tgid() >> 32;
|
||||
victim.victim_tgid = victim_tgid;
|
||||
if (bpf_map_update_elem(&victims, &victim_pid, &victim, BPF_ANY)) {
|
||||
__sync_fetch_and_add(&dropped_victim_states, 1);
|
||||
return 0;
|
||||
}
|
||||
|
||||
event = bpf_ringbuf_reserve(&events, sizeof(*event), 0);
|
||||
if (!event)
|
||||
return 0;
|
||||
__builtin_memset(event, 0, sizeof(*event));
|
||||
event->timestamp_ns = bpf_ktime_get_ns();
|
||||
event->cgroup_id = cgroup_id;
|
||||
event->type = OOM_VICTIM_MARKED;
|
||||
event->victim_pid = victim_tgid;
|
||||
event->victim_tid = victim_pid;
|
||||
event->triggering_tgid = victim.triggering_tgid;
|
||||
event->total_vm_kb = ctx->total_vm;
|
||||
event->anon_rss_kb = ctx->anon_rss;
|
||||
event->file_rss_kb = ctx->file_rss;
|
||||
bpf_probe_read_kernel_str(event->comm, sizeof(event->comm),
|
||||
(void *)ctx + (ctx->__data_loc_comm & 0xffff));
|
||||
|
||||
profile = bpf_map_lookup_elem(&profiles, &cgroup_id);
|
||||
if (profile) {
|
||||
event->reclaim_begin_count = profile->begin_count;
|
||||
event->reclaim_end_count = profile->end_count;
|
||||
event->reclaimed_pages = profile->reclaimed_pages;
|
||||
event->cross_cgroup_reclaims = profile->cross_cgroup_reclaims;
|
||||
event->last_reclaim_ns = profile->last_reclaim_ns;
|
||||
}
|
||||
bpf_ringbuf_submit(event, 0);
|
||||
return 0;
|
||||
}
|
||||
|
||||
SEC("tracepoint/sched/sched_process_exit")
|
||||
int capture_victim_exit(void *ctx)
|
||||
{
|
||||
struct victim_state *victim;
|
||||
struct task_struct *task;
|
||||
struct oom_watch_event *event;
|
||||
__u64 pid_tgid = bpf_get_current_pid_tgid();
|
||||
__u32 tid = pid_tgid;
|
||||
|
||||
(void)ctx;
|
||||
victim = bpf_map_lookup_elem(&victims, &tid);
|
||||
if (!victim)
|
||||
return 0;
|
||||
event = bpf_ringbuf_reserve(&events, sizeof(*event), 0);
|
||||
if (event) {
|
||||
__builtin_memset(event, 0, sizeof(*event));
|
||||
event->timestamp_ns = bpf_ktime_get_ns();
|
||||
event->cgroup_id = victim->cgroup_id;
|
||||
event->type = OOM_VICTIM_EXITED;
|
||||
event->victim_pid = victim->victim_tgid;
|
||||
event->victim_tid = tid;
|
||||
event->triggering_tgid = victim->triggering_tgid;
|
||||
task = (struct task_struct *)bpf_get_current_task_btf();
|
||||
event->exit_code = BPF_CORE_READ(task, exit_code);
|
||||
bpf_get_current_comm(event->comm, sizeof(event->comm));
|
||||
bpf_ringbuf_submit(event, 0);
|
||||
}
|
||||
bpf_map_delete_elem(&victims, &tid);
|
||||
return 0;
|
||||
}
|
||||
406
src/57-oom-watch/oom_watch.c
Normal file
406
src/57-oom-watch/oom_watch.c
Normal file
@@ -0,0 +1,406 @@
|
||||
// SPDX-License-Identifier: (LGPL-2.1 OR BSD-2-Clause)
|
||||
#include <errno.h>
|
||||
#include <fcntl.h>
|
||||
#include <getopt.h>
|
||||
#include <poll.h>
|
||||
#include <pthread.h>
|
||||
#include <signal.h>
|
||||
#include <stdbool.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <sys/mman.h>
|
||||
#include <sys/stat.h>
|
||||
#include <sys/types.h>
|
||||
#include <sys/wait.h>
|
||||
#include <time.h>
|
||||
#include <unistd.h>
|
||||
#include <bpf/libbpf.h>
|
||||
#include "oom_watch.h"
|
||||
#include "oom_watch.skel.h"
|
||||
|
||||
struct options {
|
||||
const char *cgroup_path;
|
||||
unsigned int duration_seconds;
|
||||
bool demo;
|
||||
};
|
||||
|
||||
static volatile sig_atomic_t stop;
|
||||
static int victim_events;
|
||||
static int exit_events;
|
||||
static unsigned long long observed_cgroup_id;
|
||||
static unsigned int observed_victim_pid;
|
||||
static unsigned int observed_victim_tid;
|
||||
static unsigned long long observed_reclaims;
|
||||
static unsigned long long observed_cross_cgroup_reclaims;
|
||||
|
||||
struct allocation_context {
|
||||
int ready_fd;
|
||||
int continue_fd;
|
||||
};
|
||||
|
||||
static struct allocation_context allocation_context;
|
||||
|
||||
static void handle_signal(int signal_number)
|
||||
{
|
||||
(void)signal_number;
|
||||
stop = 1;
|
||||
}
|
||||
|
||||
static unsigned long long monotonic_ns(void)
|
||||
{
|
||||
struct timespec now;
|
||||
|
||||
clock_gettime(CLOCK_MONOTONIC, &now);
|
||||
return (unsigned long long)now.tv_sec * 1000000000ULL + now.tv_nsec;
|
||||
}
|
||||
|
||||
static int write_text(const char *path, const char *text)
|
||||
{
|
||||
int fd = open(path, O_WRONLY | O_CLOEXEC);
|
||||
ssize_t length = strlen(text);
|
||||
int err = 0;
|
||||
|
||||
if (fd < 0)
|
||||
return -1;
|
||||
if (write(fd, text, length) != length)
|
||||
err = -1;
|
||||
close(fd);
|
||||
return err;
|
||||
}
|
||||
|
||||
static int memory_controller_enabled(bool *enabled)
|
||||
{
|
||||
char controllers[4096];
|
||||
ssize_t length;
|
||||
int fd;
|
||||
|
||||
fd = open("/sys/fs/cgroup/cgroup.subtree_control",
|
||||
O_RDONLY | O_CLOEXEC);
|
||||
if (fd < 0)
|
||||
return -1;
|
||||
length = read(fd, controllers, sizeof(controllers) - 1);
|
||||
close(fd);
|
||||
if (length < 0)
|
||||
return -1;
|
||||
controllers[length] = '\0';
|
||||
*enabled = strstr(controllers, "memory") != NULL;
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int handle_event(void *ctx, void *data, size_t size)
|
||||
{
|
||||
const struct oom_watch_event *event = data;
|
||||
|
||||
(void)ctx;
|
||||
if (size != sizeof(*event))
|
||||
return 0;
|
||||
if (event->type == OOM_VICTIM_MARKED) {
|
||||
victim_events++;
|
||||
observed_cgroup_id = event->cgroup_id;
|
||||
observed_victim_pid = event->victim_pid;
|
||||
observed_reclaims = event->reclaim_begin_count;
|
||||
observed_victim_tid = event->victim_tid;
|
||||
observed_cross_cgroup_reclaims = event->cross_cgroup_reclaims;
|
||||
printf("event=oom-victim pid=%u tid=%u comm=%s trigger_pid=%u cgroup_id=%llu "
|
||||
"anon_rss_kb=%llu file_rss_kb=%llu total_vm_kb=%llu "
|
||||
"reclaim_cycles=%llu cross_cgroup_reclaims=%llu "
|
||||
"reclaimed_pages=%llu\n",
|
||||
event->victim_pid, event->victim_tid, event->comm,
|
||||
event->triggering_tgid,
|
||||
(unsigned long long)event->cgroup_id,
|
||||
(unsigned long long)event->anon_rss_kb,
|
||||
(unsigned long long)event->file_rss_kb,
|
||||
(unsigned long long)event->total_vm_kb,
|
||||
(unsigned long long)event->reclaim_begin_count,
|
||||
(unsigned long long)event->cross_cgroup_reclaims,
|
||||
(unsigned long long)event->reclaimed_pages);
|
||||
} else if (event->type == OOM_VICTIM_EXITED) {
|
||||
exit_events++;
|
||||
printf("event=victim-exit pid=%u tid=%u cgroup_id=%llu exit_code=%d\n",
|
||||
event->victim_pid, event->victim_tid,
|
||||
(unsigned long long)event->cgroup_id, event->exit_code);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int parse_uint(const char *text, unsigned int *value)
|
||||
{
|
||||
char *end = NULL;
|
||||
unsigned long parsed;
|
||||
|
||||
errno = 0;
|
||||
parsed = strtoul(text, &end, 10);
|
||||
if (errno || !*text || *end || parsed > 86400)
|
||||
return -1;
|
||||
*value = parsed;
|
||||
return 0;
|
||||
}
|
||||
|
||||
static void usage(const char *program)
|
||||
{
|
||||
printf("Usage: %s [--cgroup PATH] [--duration SEC]\n"
|
||||
" %s --demo\n", program, program);
|
||||
}
|
||||
|
||||
static int parse_options(int argc, char **argv, struct options *options)
|
||||
{
|
||||
static const struct option long_options[] = {
|
||||
{ "cgroup", required_argument, NULL, 'c' },
|
||||
{ "duration", required_argument, NULL, 'd' },
|
||||
{ "demo", no_argument, NULL, 'D' },
|
||||
{ "help", no_argument, NULL, 'h' },
|
||||
{},
|
||||
};
|
||||
int option;
|
||||
|
||||
while ((option = getopt_long(argc, argv, "c:d:Dh", long_options,
|
||||
NULL)) != -1) {
|
||||
switch (option) {
|
||||
case 'c': options->cgroup_path = optarg; break;
|
||||
case 'd':
|
||||
if (parse_uint(optarg, &options->duration_seconds))
|
||||
return -1;
|
||||
break;
|
||||
case 'D': options->demo = true; break;
|
||||
case 'h': usage(argv[0]); exit(0);
|
||||
default: return -1;
|
||||
}
|
||||
}
|
||||
return optind == argc && !(options->demo && options->cgroup_path) ? 0 : -1;
|
||||
}
|
||||
|
||||
static void *allocation_worker(void *argument)
|
||||
{
|
||||
struct allocation_context *context = argument;
|
||||
size_t first_stage = 24 * 1024 * 1024;
|
||||
size_t length = 128 * 1024 * 1024;
|
||||
unsigned char *memory;
|
||||
char byte = 'x';
|
||||
|
||||
memory = mmap(NULL, length, PROT_READ | PROT_WRITE,
|
||||
MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
|
||||
if (memory == MAP_FAILED)
|
||||
_exit(4);
|
||||
for (size_t offset = 0; offset < first_stage; offset += 4096)
|
||||
memory[offset] = 0xa5;
|
||||
if (write(context->ready_fd, &byte, 1) != 1 ||
|
||||
read(context->continue_fd, &byte, 1) != 1)
|
||||
_exit(5);
|
||||
for (size_t offset = first_stage; offset < length; offset += 4096)
|
||||
memory[offset] = 0xa5;
|
||||
_exit(6);
|
||||
}
|
||||
|
||||
static void allocate_until_killed(const char *cgroup_path, int ready_fd,
|
||||
int continue_fd)
|
||||
{
|
||||
char procs_path[512];
|
||||
char pid_text[32];
|
||||
pthread_t worker;
|
||||
|
||||
snprintf(procs_path, sizeof(procs_path), "%s/cgroup.procs", cgroup_path);
|
||||
snprintf(pid_text, sizeof(pid_text), "%d", getpid());
|
||||
if (write_text(procs_path, pid_text))
|
||||
_exit(3);
|
||||
allocation_context.ready_fd = ready_fd;
|
||||
allocation_context.continue_fd = continue_fd;
|
||||
if (pthread_create(&worker, NULL, allocation_worker,
|
||||
&allocation_context))
|
||||
_exit(4);
|
||||
pthread_detach(worker);
|
||||
pthread_exit(NULL);
|
||||
}
|
||||
|
||||
static int configure_demo_cgroup(const char *path, bool *created,
|
||||
bool *enabled_by_demo)
|
||||
{
|
||||
bool memory_enabled;
|
||||
char file[512];
|
||||
|
||||
if (memory_controller_enabled(&memory_enabled))
|
||||
return -1;
|
||||
if (!memory_enabled) {
|
||||
if (write_text("/sys/fs/cgroup/cgroup.subtree_control", "+memory"))
|
||||
return -1;
|
||||
*enabled_by_demo = true;
|
||||
}
|
||||
if (mkdir(path, 0755))
|
||||
return -1;
|
||||
*created = true;
|
||||
snprintf(file, sizeof(file), "%s/memory.max", path);
|
||||
if (write_text(file, "33554432"))
|
||||
return -1;
|
||||
snprintf(file, sizeof(file), "%s/memory.swap.max", path);
|
||||
if (write_text(file, "0"))
|
||||
return -1;
|
||||
snprintf(file, sizeof(file), "%s/memory.oom.group", path);
|
||||
return write_text(file, "1");
|
||||
}
|
||||
|
||||
static int trigger_cross_cgroup_reclaim(const char *cgroup_path)
|
||||
{
|
||||
char reclaim_path[512];
|
||||
|
||||
snprintf(reclaim_path, sizeof(reclaim_path), "%s/memory.reclaim",
|
||||
cgroup_path);
|
||||
if (!write_text(reclaim_path, "8388608") || errno == EAGAIN)
|
||||
return 0;
|
||||
return -1;
|
||||
}
|
||||
|
||||
int main(int argc, char **argv)
|
||||
{
|
||||
struct options options = {};
|
||||
struct oom_watch_bpf *skel = NULL;
|
||||
struct ring_buffer *ring = NULL;
|
||||
char demo_cgroup_path[256];
|
||||
unsigned long long deadline = 0;
|
||||
struct stat cgroup_stat = {};
|
||||
const char *selected_path = NULL;
|
||||
pid_t child = -1;
|
||||
int ready_pipe[2] = { -1, -1 };
|
||||
int continue_pipe[2] = { -1, -1 };
|
||||
int status = 0;
|
||||
int err = 1;
|
||||
bool demo_cgroup_created = false;
|
||||
bool memory_enabled_by_demo = false;
|
||||
|
||||
setvbuf(stdout, NULL, _IONBF, 0);
|
||||
if (parse_options(argc, argv, &options)) {
|
||||
usage(argv[0]);
|
||||
return 2;
|
||||
}
|
||||
if (options.demo) {
|
||||
snprintf(demo_cgroup_path, sizeof(demo_cgroup_path),
|
||||
"/sys/fs/cgroup/ebpf-oom-watch-%d", getpid());
|
||||
if (configure_demo_cgroup(demo_cgroup_path,
|
||||
&demo_cgroup_created,
|
||||
&memory_enabled_by_demo)) {
|
||||
fprintf(stderr, "failed to configure demo memory cgroup: %s\n",
|
||||
strerror(errno));
|
||||
goto cleanup;
|
||||
}
|
||||
selected_path = demo_cgroup_path;
|
||||
} else {
|
||||
selected_path = options.cgroup_path;
|
||||
}
|
||||
if (selected_path && stat(selected_path, &cgroup_stat)) {
|
||||
fprintf(stderr, "failed to stat cgroup %s: %s\n", selected_path,
|
||||
strerror(errno));
|
||||
goto cleanup;
|
||||
}
|
||||
|
||||
skel = oom_watch_bpf__open();
|
||||
if (!skel)
|
||||
goto cleanup;
|
||||
skel->rodata->target_cgroup_id = selected_path ? cgroup_stat.st_ino : 0;
|
||||
if (oom_watch_bpf__load(skel) || oom_watch_bpf__attach(skel)) {
|
||||
fprintf(stderr, "failed to load and attach OOM watcher\n");
|
||||
goto cleanup;
|
||||
}
|
||||
ring = ring_buffer__new(bpf_map__fd(skel->maps.events), handle_event,
|
||||
NULL, NULL);
|
||||
if (!ring)
|
||||
goto cleanup;
|
||||
|
||||
if (selected_path)
|
||||
printf("oom-watch tracing cgroup=%s cgroup_id=%llu\n",
|
||||
selected_path, (unsigned long long)cgroup_stat.st_ino);
|
||||
else
|
||||
printf("oom-watch tracing all cgroups\n");
|
||||
|
||||
if (options.demo) {
|
||||
struct pollfd ready = { .events = POLLIN };
|
||||
struct timespec leader_exit_delay = { .tv_nsec = 100000000 };
|
||||
char byte = 'x';
|
||||
|
||||
if (pipe(ready_pipe) || pipe(continue_pipe))
|
||||
goto cleanup;
|
||||
child = fork();
|
||||
if (child < 0)
|
||||
goto cleanup;
|
||||
if (!child) {
|
||||
close(ready_pipe[0]);
|
||||
close(continue_pipe[1]);
|
||||
allocate_until_killed(selected_path, ready_pipe[1],
|
||||
continue_pipe[0]);
|
||||
}
|
||||
close(ready_pipe[1]); ready_pipe[1] = -1;
|
||||
close(continue_pipe[0]); continue_pipe[0] = -1;
|
||||
ready.fd = ready_pipe[0];
|
||||
if (poll(&ready, 1, 5000) != 1 ||
|
||||
read(ready_pipe[0], &byte, 1) != 1 ||
|
||||
trigger_cross_cgroup_reclaim(selected_path))
|
||||
goto cleanup;
|
||||
nanosleep(&leader_exit_delay, NULL);
|
||||
if (write(continue_pipe[1], &byte, 1) != 1)
|
||||
goto cleanup;
|
||||
close(ready_pipe[0]); ready_pipe[0] = -1;
|
||||
close(continue_pipe[1]); continue_pipe[1] = -1;
|
||||
|
||||
for (int i = 0; i < 200; i++) {
|
||||
pid_t waited;
|
||||
|
||||
ring_buffer__poll(ring, 50);
|
||||
waited = waitpid(child, &status, WNOHANG);
|
||||
if (waited == child) {
|
||||
child = -1;
|
||||
break;
|
||||
}
|
||||
}
|
||||
for (int i = 0; i < 10 && exit_events < 1; i++)
|
||||
ring_buffer__poll(ring, 50);
|
||||
|
||||
printf("demo workload signaled=%d signal=%d\n",
|
||||
WIFSIGNALED(status),
|
||||
WIFSIGNALED(status) ? WTERMSIG(status) : 0);
|
||||
if (!WIFSIGNALED(status) || WTERMSIG(status) != SIGKILL ||
|
||||
victim_events != 1 || exit_events != 1 ||
|
||||
observed_cgroup_id != (unsigned long long)cgroup_stat.st_ino ||
|
||||
!observed_victim_pid || !observed_victim_tid ||
|
||||
observed_victim_pid == observed_victim_tid ||
|
||||
!observed_reclaims || !observed_cross_cgroup_reclaims ||
|
||||
skel->bss->dropped_victim_states)
|
||||
goto cleanup;
|
||||
printf("demo result=matched-profile-to-victim\n");
|
||||
} else {
|
||||
signal(SIGINT, handle_signal);
|
||||
signal(SIGTERM, handle_signal);
|
||||
if (options.duration_seconds)
|
||||
deadline = monotonic_ns() +
|
||||
(unsigned long long)options.duration_seconds *
|
||||
1000000000ULL;
|
||||
while (!stop && (!deadline || monotonic_ns() < deadline)) {
|
||||
int poll_result = ring_buffer__poll(ring, 100);
|
||||
|
||||
if (poll_result < 0 && poll_result != -EINTR) {
|
||||
fprintf(stderr, "ring buffer poll failed: %d\n",
|
||||
poll_result);
|
||||
goto cleanup;
|
||||
}
|
||||
}
|
||||
}
|
||||
printf("dropped_victim_states=%llu\n",
|
||||
(unsigned long long)skel->bss->dropped_victim_states);
|
||||
err = 0;
|
||||
|
||||
cleanup:
|
||||
if (child > 0) {
|
||||
kill(child, SIGKILL);
|
||||
waitpid(child, NULL, 0);
|
||||
}
|
||||
for (size_t i = 0; i < 2; i++) {
|
||||
if (ready_pipe[i] >= 0) close(ready_pipe[i]);
|
||||
if (continue_pipe[i] >= 0) close(continue_pipe[i]);
|
||||
}
|
||||
ring_buffer__free(ring);
|
||||
oom_watch_bpf__destroy(skel);
|
||||
if (demo_cgroup_created && rmdir(demo_cgroup_path) && !err)
|
||||
err = 1;
|
||||
if (memory_enabled_by_demo &&
|
||||
write_text("/sys/fs/cgroup/cgroup.subtree_control", "-memory") &&
|
||||
!err)
|
||||
err = 1;
|
||||
return err;
|
||||
}
|
||||
29
src/57-oom-watch/oom_watch.h
Normal file
29
src/57-oom-watch/oom_watch.h
Normal file
@@ -0,0 +1,29 @@
|
||||
/* SPDX-License-Identifier: (LGPL-2.1 OR BSD-2-Clause) */
|
||||
#ifndef __OOM_WATCH_H
|
||||
#define __OOM_WATCH_H
|
||||
|
||||
enum oom_watch_event_type {
|
||||
OOM_VICTIM_MARKED = 1,
|
||||
OOM_VICTIM_EXITED = 2,
|
||||
};
|
||||
|
||||
struct oom_watch_event {
|
||||
unsigned long long timestamp_ns;
|
||||
unsigned long long cgroup_id;
|
||||
unsigned long long reclaim_begin_count;
|
||||
unsigned long long reclaim_end_count;
|
||||
unsigned long long reclaimed_pages;
|
||||
unsigned long long cross_cgroup_reclaims;
|
||||
unsigned long long last_reclaim_ns;
|
||||
unsigned long long total_vm_kb;
|
||||
unsigned long long anon_rss_kb;
|
||||
unsigned long long file_rss_kb;
|
||||
unsigned int type;
|
||||
unsigned int victim_pid;
|
||||
unsigned int victim_tid;
|
||||
unsigned int triggering_tgid;
|
||||
signed int exit_code;
|
||||
char comm[16];
|
||||
};
|
||||
|
||||
#endif /* __OOM_WATCH_H */
|
||||
36
src/57-oom-watch/tests/test_oom_watch.py
Normal file
36
src/57-oom-watch/tests/test_oom_watch.py
Normal file
@@ -0,0 +1,36 @@
|
||||
#!/usr/bin/env python3
|
||||
import subprocess
|
||||
import sys
|
||||
import re
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def main():
|
||||
subtree_control = Path("/sys/fs/cgroup/cgroup.subtree_control")
|
||||
before = subtree_control.read_text()
|
||||
result = subprocess.run(
|
||||
[sys.argv[1], "--demo"],
|
||||
text=True,
|
||||
capture_output=True,
|
||||
check=False,
|
||||
timeout=30,
|
||||
)
|
||||
output = result.stdout + result.stderr
|
||||
after = subtree_control.read_text()
|
||||
if result.returncode:
|
||||
raise AssertionError(output)
|
||||
assert before == after, (before, after, output)
|
||||
assert "event=oom-victim" in output, output
|
||||
match = re.search(r"event=oom-victim pid=(\d+) tid=(\d+)", output)
|
||||
assert match and match.group(1) != match.group(2), output
|
||||
assert "reclaim_cycles=" in output, output
|
||||
assert "cross_cgroup_reclaims=" in output, output
|
||||
assert "event=victim-exit" in output, output
|
||||
assert "demo workload signaled=1 signal=9" in output, output
|
||||
assert "demo result=matched-profile-to-victim" in output, output
|
||||
assert "dropped_victim_states=0" in output, output
|
||||
print("OOM watch integration test: PASS")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
2
src/60-afxdp-dump/.gitignore
vendored
Normal file
2
src/60-afxdp-dump/.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
.output/
|
||||
afxdp_dump
|
||||
94
src/60-afxdp-dump/Makefile
Normal file
94
src/60-afxdp-dump/Makefile
Normal file
@@ -0,0 +1,94 @@
|
||||
# SPDX-License-Identifier: (LGPL-2.1 OR BSD-2-Clause)
|
||||
OUTPUT := .output
|
||||
CLANG ?= clang
|
||||
LIBBPF_SRC := $(abspath ../third_party/libbpf/src)
|
||||
BPFTOOL_SRC := $(abspath ../third_party/bpftool/src)
|
||||
LIBBPF_OBJ := $(abspath $(OUTPUT)/libbpf.a)
|
||||
BPFTOOL_OUTPUT ?= $(abspath $(OUTPUT)/bpftool)
|
||||
BPFTOOL ?= $(BPFTOOL_OUTPUT)/bootstrap/bpftool
|
||||
ARCH ?= $(shell uname -m | sed 's/x86_64/x86/' \
|
||||
| sed 's/arm.*/arm/' \
|
||||
| sed 's/aarch64/arm64/' \
|
||||
| sed 's/ppc64le/powerpc/' \
|
||||
| sed 's/mips.*/mips/' \
|
||||
| sed 's/riscv64/riscv/' \
|
||||
| sed 's/loongarch64/loongarch/')
|
||||
VMLINUX := ../third_party/vmlinux/$(ARCH)/vmlinux.h
|
||||
INCLUDES := -I$(OUTPUT) -I../third_party/libbpf/include/uapi -I$(dir $(VMLINUX)) -I.
|
||||
CFLAGS := -g -Wall -Wextra -D_GNU_SOURCE
|
||||
ALL_LDFLAGS := $(LDFLAGS) $(EXTRA_LDFLAGS)
|
||||
APPS = afxdp_dump
|
||||
|
||||
CLANG_BPF_SYS_INCLUDES ?= $(shell $(CLANG) -v -E - </dev/null 2>&1 \
|
||||
| sed -n '/<...> search starts here:/,/End of search list./{ s| \(/.*\)|-idirafter \1|p }')
|
||||
|
||||
ifeq ($(V),1)
|
||||
Q =
|
||||
msg =
|
||||
else
|
||||
Q = @
|
||||
msg = @printf ' %-8s %s%s\n' "$(1)" \
|
||||
"$(patsubst $(abspath $(OUTPUT))/%,%,$(2))" \
|
||||
"$(if $(3), $(3))";
|
||||
MAKEFLAGS += --no-print-directory
|
||||
endif
|
||||
|
||||
define allow-override
|
||||
$(if $(or $(findstring environment,$(origin $(1))),\
|
||||
$(findstring command line,$(origin $(1)))),,\
|
||||
$(eval $(1) = $(2)))
|
||||
endef
|
||||
|
||||
$(call allow-override,CC,$(CROSS_COMPILE)cc)
|
||||
|
||||
.PHONY: all clean test
|
||||
all: $(APPS)
|
||||
|
||||
clean:
|
||||
$(call msg,CLEAN)
|
||||
$(Q)rm -rf $(OUTPUT) $(APPS)
|
||||
|
||||
test: all
|
||||
@if [ "$$(id -u)" -ne 0 ]; then \
|
||||
echo "test requires root; run inside the documented KVM guest" >&2; \
|
||||
exit 1; \
|
||||
fi
|
||||
python3 tests/test_afxdp_dump.py ./afxdp_dump
|
||||
|
||||
$(OUTPUT) $(OUTPUT)/libbpf $(BPFTOOL_OUTPUT):
|
||||
$(call msg,MKDIR,$@)
|
||||
$(Q)mkdir -p $@
|
||||
|
||||
$(LIBBPF_OBJ): $(wildcard $(LIBBPF_SRC)/*.[ch] $(LIBBPF_SRC)/Makefile) | $(OUTPUT)/libbpf
|
||||
$(call msg,LIB,$@)
|
||||
$(Q)$(MAKE) -C $(LIBBPF_SRC) BUILD_STATIC_ONLY=1 \
|
||||
OBJDIR=$(dir $@)/libbpf DESTDIR=$(dir $@) \
|
||||
INCLUDEDIR= LIBDIR= UAPIDIR= install
|
||||
|
||||
$(BPFTOOL): | $(BPFTOOL_OUTPUT)
|
||||
$(call msg,BPFTOOL,$@)
|
||||
$(Q)$(MAKE) ARCH= CROSS_COMPILE= OUTPUT=$(BPFTOOL_OUTPUT)/ -C $(BPFTOOL_SRC) bootstrap
|
||||
|
||||
$(OUTPUT)/%.bpf.o: %.bpf.c $(LIBBPF_OBJ) $(wildcard %.h) $(VMLINUX) | $(OUTPUT) $(BPFTOOL)
|
||||
$(call msg,BPF,$@)
|
||||
$(Q)$(CLANG) -g -O2 -target bpf -D__TARGET_ARCH_$(ARCH) \
|
||||
$(INCLUDES) $(CLANG_BPF_SYS_INCLUDES) \
|
||||
-c $(filter %.c,$^) -o $(patsubst %.bpf.o,%.tmp.bpf.o,$@)
|
||||
$(Q)$(BPFTOOL) gen object $@ $(patsubst %.bpf.o,%.tmp.bpf.o,$@)
|
||||
|
||||
$(OUTPUT)/%.skel.h: $(OUTPUT)/%.bpf.o | $(OUTPUT) $(BPFTOOL)
|
||||
$(call msg,GEN-SKEL,$@)
|
||||
$(Q)$(BPFTOOL) gen skeleton $< > $@
|
||||
|
||||
$(patsubst %,$(OUTPUT)/%.o,$(APPS)): %.o: %.skel.h
|
||||
|
||||
$(OUTPUT)/%.o: %.c $(wildcard %.h) | $(OUTPUT)
|
||||
$(call msg,CC,$@)
|
||||
$(Q)$(CC) $(CFLAGS) $(INCLUDES) -c $(filter %.c,$^) -o $@
|
||||
|
||||
$(APPS): %: $(OUTPUT)/%.o $(LIBBPF_OBJ) | $(OUTPUT)
|
||||
$(call msg,BINARY,$@)
|
||||
$(Q)$(CC) $(CFLAGS) $^ $(ALL_LDFLAGS) -lelf -lz -o $@
|
||||
|
||||
.DELETE_ON_ERROR:
|
||||
.SECONDARY:
|
||||
666
src/60-afxdp-dump/README.md
Normal file
666
src/60-afxdp-dump/README.md
Normal file
@@ -0,0 +1,666 @@
|
||||
# eBPF Tutorial by Example: AF_XDP Packet Dump
|
||||
|
||||
How do you receive network packets directly into user space? AF_XDP provides a path for packet processing by sharing memory between the kernel and application. This tutorial builds a packet dumper that uses XDP to redirect selected UDP packets to an AF_XDP socket. This implementation uses copy mode for compatibility.
|
||||
|
||||
> Complete source code: <https://github.com/eunomia-bpf/bpf-developer-tutorial/tree/main/src/60-afxdp-dump>
|
||||
|
||||
## eBPF, XDP, and AF_XDP
|
||||
|
||||
eBPF lets verified programs run at Linux kernel hooks and send selected state to user space. XDP (eXpress Data Path) runs packet programs before the normal kernel networking stack, while AF_XDP, introduced in Linux 4.18, connects an XDP redirect to user-space packet rings. This implementation uses `bpf_map_lookup_elem()` on XSKMAP to check whether an AF_XDP socket is bound before redirecting, a capability enabled in Linux 5.3. This makes Linux 5.3 the minimum kernel version for this tool.
|
||||
|
||||
## How the Implementation Works
|
||||
|
||||
Our tool attaches an XDP program that filters UDP packets by destination port. Matching packets are redirected to an AF_XDP socket; everything else passes through to the normal stack. The XDP program checks if an AF_XDP socket is bound for the queue before redirecting.
|
||||
|
||||
## Header File
|
||||
|
||||
```c
|
||||
/* SPDX-License-Identifier: (LGPL-2.1 OR BSD-2-Clause) */
|
||||
#ifndef __AFXDP_DUMP_H
|
||||
#define __AFXDP_DUMP_H
|
||||
|
||||
#define AFXDP_MAX_QUEUES 64
|
||||
|
||||
#endif /* __AFXDP_DUMP_H */
|
||||
```
|
||||
|
||||
The header defines the maximum number of RX queues supported by the XSKMAP.
|
||||
|
||||
## BPF Program
|
||||
|
||||
```c
|
||||
// SPDX-License-Identifier: GPL-2.0
|
||||
#include "vmlinux.h"
|
||||
#include <bpf/bpf_helpers.h>
|
||||
#include <bpf/bpf_endian.h>
|
||||
#include "afxdp_dump.h"
|
||||
|
||||
char LICENSE[] SEC("license") = "GPL";
|
||||
|
||||
#define ETH_P_IP 0x0800
|
||||
#define IPPROTO_UDP 17
|
||||
#define IP_MF 0x2000
|
||||
#define IP_OFFSET 0x1fff
|
||||
|
||||
const volatile __u16 capture_port = 8080;
|
||||
|
||||
struct {
|
||||
__uint(type, BPF_MAP_TYPE_XSKMAP);
|
||||
__uint(max_entries, AFXDP_MAX_QUEUES);
|
||||
__type(key, __u32);
|
||||
__type(value, __u32);
|
||||
} xsk_map SEC(".maps");
|
||||
|
||||
__u64 redirected_packets;
|
||||
|
||||
SEC("xdp")
|
||||
int redirect_udp(struct xdp_md *ctx)
|
||||
{
|
||||
void *data = (void *)(long)ctx->data;
|
||||
void *data_end = (void *)(long)ctx->data_end;
|
||||
struct ethhdr *ethernet = data;
|
||||
struct iphdr *ip;
|
||||
struct udphdr *udp;
|
||||
__u32 queue = ctx->rx_queue_index;
|
||||
__u32 ip_header_length;
|
||||
__u32 ip_length;
|
||||
__u32 udp_length;
|
||||
|
||||
if ((void *)(ethernet + 1) > data_end ||
|
||||
ethernet->h_proto != bpf_htons(ETH_P_IP))
|
||||
return XDP_PASS;
|
||||
ip = (void *)(ethernet + 1);
|
||||
if ((void *)(ip + 1) > data_end || ip->version != 4 ||
|
||||
ip->protocol != IPPROTO_UDP ||
|
||||
ip->ihl < 5 || (bpf_ntohs(ip->frag_off) & (IP_MF | IP_OFFSET)))
|
||||
return XDP_PASS;
|
||||
ip_header_length = ip->ihl * 4;
|
||||
ip_length = bpf_ntohs(ip->tot_len);
|
||||
udp = (void *)ip + ip_header_length;
|
||||
if (ip_length < ip_header_length + sizeof(*udp) ||
|
||||
(void *)ip + ip_length > data_end ||
|
||||
(void *)(udp + 1) > data_end)
|
||||
return XDP_PASS;
|
||||
udp_length = bpf_ntohs(udp->len);
|
||||
if (udp_length < sizeof(*udp) ||
|
||||
udp_length > ip_length - ip_header_length ||
|
||||
udp->dest != bpf_htons(capture_port))
|
||||
return XDP_PASS;
|
||||
if (!bpf_map_lookup_elem(&xsk_map, &queue))
|
||||
return XDP_PASS;
|
||||
__sync_fetch_and_add(&redirected_packets, 1);
|
||||
return bpf_redirect_map(&xsk_map, queue, XDP_PASS);
|
||||
}
|
||||
```
|
||||
|
||||
The XDP program parses Ethernet, IP, and UDP headers with bounds checking at each level. It filters for IPv4 UDP packets to the configured port, skipping fragmented packets. The `bpf_map_lookup_elem()` check ensures we only redirect if an AF_XDP socket is bound for this queue. The fallback action in `bpf_redirect_map()` is `XDP_PASS`, so if redirect fails, the packet passes through normally.
|
||||
|
||||
The XSKMAP associates queue IDs with AF_XDP socket file descriptors. When `bpf_redirect_map()` succeeds, the packet is delivered directly to the AF_XDP socket rather than the kernel network stack.
|
||||
|
||||
## User Space Program
|
||||
|
||||
```c
|
||||
// SPDX-License-Identifier: (LGPL-2.1 OR BSD-2-Clause)
|
||||
#define _FILE_OFFSET_BITS 64
|
||||
#include <arpa/inet.h>
|
||||
#include <ctype.h>
|
||||
#include <errno.h>
|
||||
#include <getopt.h>
|
||||
#include <linux/if_ether.h>
|
||||
#include <linux/if_link.h>
|
||||
#include <linux/if_xdp.h>
|
||||
#include <linux/ip.h>
|
||||
#include <linux/udp.h>
|
||||
#include <net/if.h>
|
||||
#include <poll.h>
|
||||
#include <signal.h>
|
||||
#include <stdbool.h>
|
||||
#include <stdint.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <sys/mman.h>
|
||||
#include <sys/socket.h>
|
||||
#include <unistd.h>
|
||||
#include <bpf/bpf.h>
|
||||
#include <bpf/libbpf.h>
|
||||
#include "afxdp_dump.h"
|
||||
#include "afxdp_dump.skel.h"
|
||||
|
||||
#ifndef SOL_XDP
|
||||
#define SOL_XDP 283
|
||||
#endif
|
||||
#ifndef AF_XDP
|
||||
#define AF_XDP 44
|
||||
#endif
|
||||
#ifndef PF_XDP
|
||||
#define PF_XDP AF_XDP
|
||||
#endif
|
||||
|
||||
#define FRAME_SIZE 4096
|
||||
#define FRAME_COUNT 64
|
||||
#define RING_SIZE 64
|
||||
#define UMEM_SIZE ((size_t)FRAME_SIZE * FRAME_COUNT)
|
||||
#define PAYLOAD_PREVIEW 32
|
||||
#define IPV4_MORE_FRAGMENTS 0x2000
|
||||
#define IPV4_FRAGMENT_OFFSET 0x1fff
|
||||
|
||||
struct options {
|
||||
const char *interface;
|
||||
unsigned int queue;
|
||||
unsigned int port;
|
||||
unsigned int count;
|
||||
bool skb_mode;
|
||||
};
|
||||
|
||||
struct mapped_ring {
|
||||
void *mapping;
|
||||
size_t mapping_size;
|
||||
unsigned int *producer;
|
||||
unsigned int *consumer;
|
||||
unsigned int *flags;
|
||||
void *descriptors;
|
||||
unsigned int mask;
|
||||
};
|
||||
|
||||
struct xsk_state {
|
||||
int fd;
|
||||
void *umem;
|
||||
struct mapped_ring fill;
|
||||
struct mapped_ring completion;
|
||||
struct mapped_ring rx;
|
||||
};
|
||||
|
||||
static volatile sig_atomic_t stop;
|
||||
|
||||
static void handle_signal(int signal_number)
|
||||
{
|
||||
(void)signal_number;
|
||||
stop = 1;
|
||||
}
|
||||
|
||||
static void map_ring_fields(struct mapped_ring *ring, void *mapping,
|
||||
size_t mapping_size,
|
||||
const struct xdp_ring_offset *offset)
|
||||
{
|
||||
ring->mapping = mapping;
|
||||
ring->mapping_size = mapping_size;
|
||||
ring->producer = mapping + offset->producer;
|
||||
ring->consumer = mapping + offset->consumer;
|
||||
ring->flags = mapping + offset->flags;
|
||||
ring->descriptors = mapping + offset->desc;
|
||||
ring->mask = RING_SIZE - 1;
|
||||
}
|
||||
|
||||
static int map_xsk_ring(struct mapped_ring *ring, int fd,
|
||||
const struct xdp_ring_offset *offset,
|
||||
off_t page_offset, size_t descriptor_size)
|
||||
{
|
||||
size_t size = offset->desc + RING_SIZE * descriptor_size;
|
||||
void *mapping;
|
||||
|
||||
mapping = mmap(NULL, size, PROT_READ | PROT_WRITE,
|
||||
MAP_SHARED | MAP_POPULATE, fd, page_offset);
|
||||
if (mapping == MAP_FAILED)
|
||||
return -1;
|
||||
map_ring_fields(ring, mapping, size, offset);
|
||||
return 0;
|
||||
}
|
||||
|
||||
static void close_xsk(struct xsk_state *xsk)
|
||||
{
|
||||
struct mapped_ring *rings[] = {
|
||||
&xsk->rx, &xsk->completion, &xsk->fill,
|
||||
};
|
||||
|
||||
for (size_t i = 0; i < sizeof(rings) / sizeof(rings[0]); i++)
|
||||
if (rings[i]->mapping && rings[i]->mapping != MAP_FAILED)
|
||||
munmap(rings[i]->mapping, rings[i]->mapping_size);
|
||||
if (xsk->fd >= 0)
|
||||
close(xsk->fd);
|
||||
if (xsk->umem && xsk->umem != MAP_FAILED)
|
||||
munmap(xsk->umem, UMEM_SIZE);
|
||||
memset(xsk, 0, sizeof(*xsk));
|
||||
xsk->fd = -1;
|
||||
}
|
||||
|
||||
static int open_xsk(struct xsk_state *xsk, int ifindex, unsigned int queue)
|
||||
{
|
||||
struct xdp_umem_reg registration = {
|
||||
.len = UMEM_SIZE,
|
||||
.chunk_size = FRAME_SIZE,
|
||||
};
|
||||
struct sockaddr_xdp address = {
|
||||
.sxdp_family = PF_XDP,
|
||||
.sxdp_ifindex = ifindex,
|
||||
.sxdp_queue_id = queue,
|
||||
.sxdp_flags = XDP_COPY,
|
||||
};
|
||||
struct xdp_mmap_offsets offsets;
|
||||
socklen_t offsets_size = sizeof(offsets);
|
||||
unsigned int ring_size = RING_SIZE;
|
||||
unsigned long long *fill_addresses;
|
||||
|
||||
memset(xsk, 0, sizeof(*xsk));
|
||||
xsk->fd = -1;
|
||||
xsk->umem = mmap(NULL, UMEM_SIZE, PROT_READ | PROT_WRITE,
|
||||
MAP_PRIVATE | MAP_ANONYMOUS | MAP_NORESERVE, -1, 0);
|
||||
if (xsk->umem == MAP_FAILED)
|
||||
return -1;
|
||||
xsk->fd = socket(AF_XDP, SOCK_RAW | SOCK_CLOEXEC, 0);
|
||||
if (xsk->fd < 0)
|
||||
return -1;
|
||||
registration.addr = (uintptr_t)xsk->umem;
|
||||
if (setsockopt(xsk->fd, SOL_XDP, XDP_UMEM_REG, ®istration,
|
||||
sizeof(registration)) ||
|
||||
setsockopt(xsk->fd, SOL_XDP, XDP_UMEM_FILL_RING, &ring_size,
|
||||
sizeof(ring_size)) ||
|
||||
setsockopt(xsk->fd, SOL_XDP, XDP_UMEM_COMPLETION_RING, &ring_size,
|
||||
sizeof(ring_size)) ||
|
||||
setsockopt(xsk->fd, SOL_XDP, XDP_RX_RING, &ring_size,
|
||||
sizeof(ring_size)) ||
|
||||
getsockopt(xsk->fd, SOL_XDP, XDP_MMAP_OFFSETS, &offsets,
|
||||
&offsets_size))
|
||||
return -1;
|
||||
|
||||
if (map_xsk_ring(&xsk->fill, xsk->fd, &offsets.fr,
|
||||
XDP_UMEM_PGOFF_FILL_RING, sizeof(unsigned long long)) ||
|
||||
map_xsk_ring(&xsk->completion, xsk->fd, &offsets.cr,
|
||||
XDP_UMEM_PGOFF_COMPLETION_RING,
|
||||
sizeof(unsigned long long)) ||
|
||||
map_xsk_ring(&xsk->rx, xsk->fd, &offsets.rx,
|
||||
XDP_PGOFF_RX_RING, sizeof(struct xdp_desc)) ||
|
||||
bind(xsk->fd, (struct sockaddr *)&address, sizeof(address)))
|
||||
return -1;
|
||||
|
||||
fill_addresses = xsk->fill.descriptors;
|
||||
for (unsigned int i = 0; i < FRAME_COUNT; i++)
|
||||
fill_addresses[i & xsk->fill.mask] =
|
||||
(unsigned long long)i * FRAME_SIZE;
|
||||
__atomic_store_n(xsk->fill.producer, FRAME_COUNT, __ATOMIC_RELEASE);
|
||||
return 0;
|
||||
}
|
||||
|
||||
static unsigned long long data_address(unsigned long long address)
|
||||
{
|
||||
return (address & XSK_UNALIGNED_BUF_ADDR_MASK) +
|
||||
(address >> XSK_UNALIGNED_BUF_OFFSET_SHIFT);
|
||||
}
|
||||
|
||||
static int recycle_frame(struct xsk_state *xsk, unsigned long long address)
|
||||
{
|
||||
unsigned long long *fill_addresses = xsk->fill.descriptors;
|
||||
unsigned int producer, consumer;
|
||||
|
||||
producer = __atomic_load_n(xsk->fill.producer, __ATOMIC_RELAXED);
|
||||
consumer = __atomic_load_n(xsk->fill.consumer, __ATOMIC_ACQUIRE);
|
||||
if (producer - consumer == RING_SIZE)
|
||||
return -1;
|
||||
fill_addresses[producer & xsk->fill.mask] = address;
|
||||
__atomic_store_n(xsk->fill.producer, producer + 1, __ATOMIC_RELEASE);
|
||||
return 0;
|
||||
}
|
||||
|
||||
static void dump_packet(const void *packet, unsigned int length,
|
||||
unsigned int packet_number)
|
||||
{
|
||||
const struct ethhdr *ethernet = packet;
|
||||
const struct iphdr *ip;
|
||||
const struct udphdr *udp;
|
||||
const unsigned char *payload;
|
||||
char source[INET_ADDRSTRLEN], destination[INET_ADDRSTRLEN];
|
||||
char preview[PAYLOAD_PREVIEW + 1];
|
||||
unsigned int available_ip, available_payload, ip_header_length;
|
||||
unsigned int ip_length, payload_length, preview_length, udp_length;
|
||||
|
||||
if (length < sizeof(*ethernet) + sizeof(*ip) ||
|
||||
ethernet->h_proto != htons(ETH_P_IP))
|
||||
return;
|
||||
ip = packet + sizeof(*ethernet);
|
||||
ip_header_length = ip->ihl * 4;
|
||||
if (ip->version != 4 || ip->protocol != IPPROTO_UDP ||
|
||||
(ntohs(ip->frag_off) &
|
||||
(IPV4_MORE_FRAGMENTS | IPV4_FRAGMENT_OFFSET)) ||
|
||||
ip_header_length < sizeof(*ip) ||
|
||||
length < sizeof(*ethernet) + ip_header_length + sizeof(*udp))
|
||||
return;
|
||||
ip_length = ntohs(ip->tot_len);
|
||||
available_ip = length - sizeof(*ethernet);
|
||||
if (ip_length < ip_header_length + sizeof(*udp) ||
|
||||
ip_length > available_ip)
|
||||
return;
|
||||
udp = packet + sizeof(*ethernet) + ip_header_length;
|
||||
payload = (const unsigned char *)(udp + 1);
|
||||
udp_length = ntohs(udp->len);
|
||||
available_payload = length - (payload - (const unsigned char *)packet);
|
||||
if (udp_length < sizeof(*udp) ||
|
||||
udp_length > ip_length - ip_header_length ||
|
||||
udp_length - sizeof(*udp) > available_payload)
|
||||
return;
|
||||
payload_length = udp_length - sizeof(*udp);
|
||||
preview_length = payload_length < PAYLOAD_PREVIEW ?
|
||||
payload_length : PAYLOAD_PREVIEW;
|
||||
for (unsigned int i = 0; i < preview_length; i++)
|
||||
preview[i] = isprint(payload[i]) ? payload[i] : '.';
|
||||
preview[preview_length] = '\0';
|
||||
inet_ntop(AF_INET, &ip->saddr, source, sizeof(source));
|
||||
inet_ntop(AF_INET, &ip->daddr, destination, sizeof(destination));
|
||||
printf("packet=%u %s:%u -> %s:%u bytes=%u payload=\"%s\"\n",
|
||||
packet_number, source, ntohs(udp->source), destination,
|
||||
ntohs(udp->dest), length, preview);
|
||||
}
|
||||
|
||||
static int receive_packets(struct xsk_state *xsk, unsigned int count)
|
||||
{
|
||||
struct pollfd poll_fd = { .fd = xsk->fd, .events = POLLIN };
|
||||
unsigned int received = 0;
|
||||
|
||||
while (!stop && (!count || received < count)) {
|
||||
unsigned int consumer, producer;
|
||||
int poll_result = poll(&poll_fd, 1, 250);
|
||||
|
||||
if (poll_result < 0) {
|
||||
if (errno == EINTR)
|
||||
continue;
|
||||
return -1;
|
||||
}
|
||||
if (!poll_result)
|
||||
continue;
|
||||
consumer = __atomic_load_n(xsk->rx.consumer, __ATOMIC_RELAXED);
|
||||
producer = __atomic_load_n(xsk->rx.producer, __ATOMIC_ACQUIRE);
|
||||
while (consumer != producer && (!count || received < count)) {
|
||||
struct xdp_desc *descriptors = xsk->rx.descriptors;
|
||||
struct xdp_desc descriptor =
|
||||
descriptors[consumer & xsk->rx.mask];
|
||||
unsigned long long address = data_address(descriptor.addr);
|
||||
|
||||
if (descriptor.options & XDP_PKT_CONTD) {
|
||||
errno = EMSGSIZE;
|
||||
return -1;
|
||||
}
|
||||
if (address + descriptor.len > UMEM_SIZE) {
|
||||
errno = EFAULT;
|
||||
return -1;
|
||||
}
|
||||
received++;
|
||||
dump_packet(xsk->umem + address, descriptor.len, received);
|
||||
consumer++;
|
||||
__atomic_store_n(xsk->rx.consumer, consumer,
|
||||
__ATOMIC_RELEASE);
|
||||
if (recycle_frame(xsk, descriptor.addr)) {
|
||||
errno = ENOBUFS;
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int parse_uint(const char *text, unsigned int maximum,
|
||||
unsigned int *value)
|
||||
{
|
||||
char *end = NULL;
|
||||
unsigned long parsed;
|
||||
|
||||
errno = 0;
|
||||
parsed = strtoul(text, &end, 10);
|
||||
if (errno || !*text || *end || parsed > maximum)
|
||||
return -1;
|
||||
*value = parsed;
|
||||
return 0;
|
||||
}
|
||||
|
||||
static void usage(const char *program)
|
||||
{
|
||||
printf("Usage: %s --interface IFACE [--queue N] [--port PORT] [--count N] [--skb-mode]\n",
|
||||
program);
|
||||
}
|
||||
|
||||
static int parse_options(int argc, char **argv, struct options *options)
|
||||
{
|
||||
static const struct option long_options[] = {
|
||||
{ "interface", required_argument, NULL, 'i' },
|
||||
{ "queue", required_argument, NULL, 'q' },
|
||||
{ "port", required_argument, NULL, 'p' },
|
||||
{ "count", required_argument, NULL, 'n' },
|
||||
{ "skb-mode", no_argument, NULL, 'S' },
|
||||
{ "help", no_argument, NULL, 'h' },
|
||||
{},
|
||||
};
|
||||
int option;
|
||||
|
||||
while ((option = getopt_long(argc, argv, "i:q:p:n:Sh", long_options,
|
||||
NULL)) != -1) {
|
||||
switch (option) {
|
||||
case 'i': options->interface = optarg; break;
|
||||
case 'q':
|
||||
if (parse_uint(optarg, AFXDP_MAX_QUEUES - 1,
|
||||
&options->queue))
|
||||
return -1;
|
||||
break;
|
||||
case 'p':
|
||||
if (parse_uint(optarg, 65535, &options->port) ||
|
||||
!options->port)
|
||||
return -1;
|
||||
break;
|
||||
case 'n':
|
||||
if (parse_uint(optarg, 1000000, &options->count))
|
||||
return -1;
|
||||
break;
|
||||
case 'S': options->skb_mode = true; break;
|
||||
case 'h': usage(argv[0]); exit(0);
|
||||
default: return -1;
|
||||
}
|
||||
}
|
||||
return optind == argc && options->interface ? 0 : -1;
|
||||
}
|
||||
|
||||
static int attach_xdp(int ifindex, int program_fd, bool skb_mode,
|
||||
unsigned int *attached_flags)
|
||||
{
|
||||
unsigned int flags = XDP_FLAGS_UPDATE_IF_NOEXIST |
|
||||
(skb_mode ? XDP_FLAGS_SKB_MODE : XDP_FLAGS_DRV_MODE);
|
||||
int err;
|
||||
|
||||
err = bpf_xdp_attach(ifindex, program_fd, flags, NULL);
|
||||
if (err && !skb_mode && (err == -EOPNOTSUPP || err == -EINVAL)) {
|
||||
flags = XDP_FLAGS_UPDATE_IF_NOEXIST | XDP_FLAGS_SKB_MODE;
|
||||
err = bpf_xdp_attach(ifindex, program_fd, flags, NULL);
|
||||
}
|
||||
if (!err)
|
||||
*attached_flags = flags;
|
||||
return err;
|
||||
}
|
||||
|
||||
int main(int argc, char **argv)
|
||||
{
|
||||
struct options options = { .port = 8080 };
|
||||
struct afxdp_dump_bpf *skel = NULL;
|
||||
struct xsk_state xsk;
|
||||
unsigned int attached_flags = 0;
|
||||
int ifindex;
|
||||
int err = 1;
|
||||
bool attached = false;
|
||||
|
||||
setvbuf(stdout, NULL, _IONBF, 0);
|
||||
memset(&xsk, 0, sizeof(xsk));
|
||||
xsk.fd = -1;
|
||||
if (parse_options(argc, argv, &options)) {
|
||||
usage(argv[0]);
|
||||
return 2;
|
||||
}
|
||||
ifindex = if_nametoindex(options.interface);
|
||||
if (!ifindex) {
|
||||
fprintf(stderr, "unknown interface: %s\n", options.interface);
|
||||
return 2;
|
||||
}
|
||||
if (open_xsk(&xsk, ifindex, options.queue)) {
|
||||
fprintf(stderr, "failed to create AF_XDP socket: %s\n",
|
||||
strerror(errno));
|
||||
goto cleanup;
|
||||
}
|
||||
skel = afxdp_dump_bpf__open();
|
||||
if (!skel)
|
||||
goto cleanup;
|
||||
skel->rodata->capture_port = options.port;
|
||||
if (afxdp_dump_bpf__load(skel)) {
|
||||
fprintf(stderr, "failed to load XDP program\n");
|
||||
goto cleanup;
|
||||
}
|
||||
if (bpf_map__update_elem(skel->maps.xsk_map, &options.queue,
|
||||
sizeof(options.queue), &xsk.fd, sizeof(xsk.fd),
|
||||
BPF_ANY)) {
|
||||
fprintf(stderr, "failed to populate XSK map: %s\n", strerror(errno));
|
||||
goto cleanup;
|
||||
}
|
||||
{
|
||||
int attach_error = attach_xdp(ifindex,
|
||||
bpf_program__fd(skel->progs.redirect_udp),
|
||||
options.skb_mode, &attached_flags);
|
||||
|
||||
if (attach_error) {
|
||||
fprintf(stderr, "failed to attach XDP program: %s\n",
|
||||
strerror(-attach_error));
|
||||
goto cleanup;
|
||||
}
|
||||
}
|
||||
attached = true;
|
||||
signal(SIGINT, handle_signal);
|
||||
signal(SIGTERM, handle_signal);
|
||||
printf("afxdp-dump ready interface=%s queue=%u port=%u mode=%s count=%u\n",
|
||||
options.interface, options.queue, options.port,
|
||||
attached_flags & XDP_FLAGS_SKB_MODE ? "skb" : "driver",
|
||||
options.count);
|
||||
|
||||
if (receive_packets(&xsk, options.count)) {
|
||||
fprintf(stderr, "AF_XDP receive failed: %s\n", strerror(errno));
|
||||
goto cleanup;
|
||||
}
|
||||
printf("redirected=%llu\n",
|
||||
(unsigned long long)skel->bss->redirected_packets);
|
||||
err = 0;
|
||||
|
||||
cleanup:
|
||||
if (attached) {
|
||||
LIBBPF_OPTS(bpf_xdp_attach_opts, detach_options,
|
||||
.old_prog_fd = bpf_program__fd(
|
||||
skel->progs.redirect_udp));
|
||||
int detach_error = bpf_xdp_detach(
|
||||
ifindex, attached_flags & XDP_FLAGS_MODES,
|
||||
&detach_options);
|
||||
|
||||
if (detach_error) {
|
||||
fprintf(stderr, "failed to detach XDP program: %s\n",
|
||||
strerror(-detach_error));
|
||||
err = 1;
|
||||
}
|
||||
}
|
||||
afxdp_dump_bpf__destroy(skel);
|
||||
close_xsk(&xsk);
|
||||
return err;
|
||||
}
|
||||
```
|
||||
|
||||
The user space program sets up AF_XDP with several components:
|
||||
|
||||
**UMEM**: A 256KB region (64 frames of 4KB each) allocated with `mmap()` and registered with the kernel via `XDP_UMEM_REG`. Packets are received directly into these frames.
|
||||
|
||||
**Fill ring**: Pre-populated with all 64 frame addresses to tell the kernel where to place incoming packets.
|
||||
|
||||
**RX ring**: Polled for packet descriptors. When a packet arrives, the kernel posts a descriptor here containing the frame address and length.
|
||||
|
||||
**XSKMAP**: Updated with the AF_XDP socket file descriptor to enable packet redirection from XDP.
|
||||
|
||||
The `XDP_COPY` flag is used when binding the socket, which copies packets from driver buffers to UMEM rather than using DMA-based zero-copy. Copy mode supports a broader range of drivers and keeps this first AF_XDP example independent of zero-copy driver support.
|
||||
|
||||
## Compilation and Execution
|
||||
|
||||
Build the tool:
|
||||
|
||||
```bash
|
||||
cd src/60-afxdp-dump
|
||||
make
|
||||
```
|
||||
|
||||
Run with options:
|
||||
|
||||
```bash
|
||||
sudo ./afxdp_dump --interface eth0 --queue 0 --port 8080 --count 5
|
||||
```
|
||||
|
||||
Options:
|
||||
|
||||
- `--interface IFACE`: Network interface to capture on
|
||||
- `--queue N`: RX queue index (default 0)
|
||||
- `--port PORT`: UDP destination port to filter (default 8080)
|
||||
- `--count N`: Number of packets to capture (0 for unlimited)
|
||||
- `--skb-mode`: Force SKB mode instead of driver mode
|
||||
|
||||
Run the deterministic integration test, which exercises frame recycling beyond the 64 initially posted frames and verifies that a non-matching UDP packet still reaches the normal stack:
|
||||
|
||||
```bash
|
||||
sudo make test
|
||||
```
|
||||
|
||||
Representative test output:
|
||||
|
||||
```text
|
||||
afxdp-dump ready interface=axdp1267r queue=0 port=8080 mode=driver count=65
|
||||
packet=1 10.77.0.1:60414 -> 10.77.0.2:8080 bytes=53 payload="hello-afxdp"
|
||||
packet=65 10.77.0.1:60414 -> 10.77.0.2:8080 bytes=53 payload="hello-afxdp"
|
||||
redirected=65
|
||||
nonmatching-pass=verified
|
||||
AF_XDP dump integration test: PASS
|
||||
```
|
||||
|
||||
## Requirements
|
||||
|
||||
| Requirement | Details |
|
||||
|-------------|---------|
|
||||
| Kernel | Linux 5.3+ (XSKMAP lookup in XDP) |
|
||||
| Config | `CONFIG_BPF_SYSCALL`, `CONFIG_XDP_SOCKETS` |
|
||||
| Privileges | Root |
|
||||
| Interface | A network interface with the selected RX queue; native XDP is optional because the tool falls back to generic XDP |
|
||||
|
||||
## What Happens to Redirected Packets
|
||||
|
||||
Packets redirected to AF_XDP are consumed, not mirrored. They do not continue to the normal kernel networking stack. Applications listening on regular sockets will not see these packets. Non-matching packets (wrong port, wrong protocol) pass through normally and are processed by the kernel.
|
||||
|
||||
## Implementation Details
|
||||
|
||||
This is a basic single-buffer receive-only AF_XDP dumper:
|
||||
|
||||
- **Copy mode**: Uses `XDP_COPY` so the example does not depend on driver zero-copy support
|
||||
- **Single queue**: Binds to one RX queue
|
||||
- **64-frame UMEM**: Fixed buffer with bounded memory use
|
||||
- **Frame recycling**: Returns frames to fill ring after processing
|
||||
- **Safe attach**: Uses `UPDATE_IF_NOEXIST` and compare-and-detach cleanup
|
||||
|
||||
## XDP Attach Modes
|
||||
|
||||
The tool supports two XDP modes:
|
||||
|
||||
- **Driver mode** (native): Requires driver support for XDP
|
||||
- **SKB mode** (generic): Does not require native driver XDP support
|
||||
|
||||
We try driver mode first, then fall back to SKB mode:
|
||||
|
||||
```c
|
||||
err = bpf_xdp_attach(ifindex, program_fd, XDP_FLAGS_DRV_MODE, NULL);
|
||||
if (err && (err == -EOPNOTSUPP || err == -EINVAL)) {
|
||||
err = bpf_xdp_attach(ifindex, program_fd, XDP_FLAGS_SKB_MODE, NULL);
|
||||
}
|
||||
```
|
||||
|
||||
## Summary
|
||||
|
||||
This example follows the complete AF_XDP receive lifecycle: an XDP program selects one UDP flow, XSKMAP redirects it to a bound queue, user space reads descriptors from the RX ring, and processed frames return to the fill ring. Copy mode and automatic generic-XDP fallback keep the tool usable on systems without native zero-copy support.
|
||||
|
||||
> If you'd like to dive deeper into eBPF, check out our tutorial repository at <https://github.com/eunomia-bpf/bpf-developer-tutorial> or visit our website at <https://eunomia.dev/tutorials/>.
|
||||
|
||||
## References
|
||||
|
||||
- [AF_XDP kernel documentation](https://docs.kernel.org/networking/af_xdp.html)
|
||||
- [AF_XDP introduction commit](https://github.com/torvalds/linux/commit/c0c77d8fb787cfe0c3fca689c2a30d1dad4eaba7)
|
||||
- [XSKMAP lookup in XDP commit](https://github.com/torvalds/linux/commit/fada7fdc83c0)
|
||||
- [libxdp documentation](https://github.com/xdp-project/xdp-tools/blob/master/lib/libxdp/README.org)
|
||||
666
src/60-afxdp-dump/README.zh.md
Normal file
666
src/60-afxdp-dump/README.zh.md
Normal file
@@ -0,0 +1,666 @@
|
||||
# eBPF 实战教程:AF_XDP 数据包转储
|
||||
|
||||
如何将网络数据包直接接收到用户空间?AF_XDP 通过在内核和应用程序之间共享内存提供数据包处理路径。本教程构建一个数据包转储器,使用 XDP 将选定的 UDP 数据包重定向到 AF_XDP 套接字。此实现使用拷贝模式以获得兼容性。
|
||||
|
||||
> 完整源代码:<https://github.com/eunomia-bpf/bpf-developer-tutorial/tree/main/src/60-afxdp-dump>
|
||||
|
||||
## eBPF、XDP 与 AF_XDP
|
||||
|
||||
eBPF 让经过验证器检查的程序运行在 Linux 内核 hook 上,并把选出的状态发送到用户态。XDP(eXpress Data Path)在报文进入普通内核网络栈前运行程序,Linux 4.18 引入的 AF_XDP 则把 XDP 重定向与用户态报文 ring 连接起来。本例使用 `bpf_map_lookup_elem()` 查找 XSKMAP,在重定向前确认 AF_XDP 套接字已经绑定,这项能力在 Linux 5.3 中启用,因此本工具的最低内核版本是 Linux 5.3。
|
||||
|
||||
## 实现原理
|
||||
|
||||
我们的工具附加一个 XDP 程序,按目标端口过滤 UDP 数据包。匹配的数据包被重定向到 AF_XDP 套接字;其他所有数据包都传递到正常的网络栈。XDP 程序在重定向之前检查是否有 AF_XDP 套接字绑定到该队列。
|
||||
|
||||
## 头文件
|
||||
|
||||
```c
|
||||
/* SPDX-License-Identifier: (LGPL-2.1 OR BSD-2-Clause) */
|
||||
#ifndef __AFXDP_DUMP_H
|
||||
#define __AFXDP_DUMP_H
|
||||
|
||||
#define AFXDP_MAX_QUEUES 64
|
||||
|
||||
#endif /* __AFXDP_DUMP_H */
|
||||
```
|
||||
|
||||
头文件定义了 XSKMAP 支持的最大 RX 队列数。
|
||||
|
||||
## BPF 程序
|
||||
|
||||
```c
|
||||
// SPDX-License-Identifier: GPL-2.0
|
||||
#include "vmlinux.h"
|
||||
#include <bpf/bpf_helpers.h>
|
||||
#include <bpf/bpf_endian.h>
|
||||
#include "afxdp_dump.h"
|
||||
|
||||
char LICENSE[] SEC("license") = "GPL";
|
||||
|
||||
#define ETH_P_IP 0x0800
|
||||
#define IPPROTO_UDP 17
|
||||
#define IP_MF 0x2000
|
||||
#define IP_OFFSET 0x1fff
|
||||
|
||||
const volatile __u16 capture_port = 8080;
|
||||
|
||||
struct {
|
||||
__uint(type, BPF_MAP_TYPE_XSKMAP);
|
||||
__uint(max_entries, AFXDP_MAX_QUEUES);
|
||||
__type(key, __u32);
|
||||
__type(value, __u32);
|
||||
} xsk_map SEC(".maps");
|
||||
|
||||
__u64 redirected_packets;
|
||||
|
||||
SEC("xdp")
|
||||
int redirect_udp(struct xdp_md *ctx)
|
||||
{
|
||||
void *data = (void *)(long)ctx->data;
|
||||
void *data_end = (void *)(long)ctx->data_end;
|
||||
struct ethhdr *ethernet = data;
|
||||
struct iphdr *ip;
|
||||
struct udphdr *udp;
|
||||
__u32 queue = ctx->rx_queue_index;
|
||||
__u32 ip_header_length;
|
||||
__u32 ip_length;
|
||||
__u32 udp_length;
|
||||
|
||||
if ((void *)(ethernet + 1) > data_end ||
|
||||
ethernet->h_proto != bpf_htons(ETH_P_IP))
|
||||
return XDP_PASS;
|
||||
ip = (void *)(ethernet + 1);
|
||||
if ((void *)(ip + 1) > data_end || ip->version != 4 ||
|
||||
ip->protocol != IPPROTO_UDP ||
|
||||
ip->ihl < 5 || (bpf_ntohs(ip->frag_off) & (IP_MF | IP_OFFSET)))
|
||||
return XDP_PASS;
|
||||
ip_header_length = ip->ihl * 4;
|
||||
ip_length = bpf_ntohs(ip->tot_len);
|
||||
udp = (void *)ip + ip_header_length;
|
||||
if (ip_length < ip_header_length + sizeof(*udp) ||
|
||||
(void *)ip + ip_length > data_end ||
|
||||
(void *)(udp + 1) > data_end)
|
||||
return XDP_PASS;
|
||||
udp_length = bpf_ntohs(udp->len);
|
||||
if (udp_length < sizeof(*udp) ||
|
||||
udp_length > ip_length - ip_header_length ||
|
||||
udp->dest != bpf_htons(capture_port))
|
||||
return XDP_PASS;
|
||||
if (!bpf_map_lookup_elem(&xsk_map, &queue))
|
||||
return XDP_PASS;
|
||||
__sync_fetch_and_add(&redirected_packets, 1);
|
||||
return bpf_redirect_map(&xsk_map, queue, XDP_PASS);
|
||||
}
|
||||
```
|
||||
|
||||
XDP 程序在每个层级解析以太网、IP 和 UDP 头,并进行边界检查。它过滤发往配置端口的 IPv4 UDP 数据包,跳过分片数据包。`bpf_map_lookup_elem()` 检查确保我们只在此队列实际绑定了 AF_XDP 套接字时才重定向。`bpf_redirect_map()` 中的回退操作是 `XDP_PASS`,所以如果重定向失败,数据包正常传递。
|
||||
|
||||
XSKMAP 将队列 ID 与 AF_XDP 套接字文件描述符关联。当 `bpf_redirect_map()` 成功时,数据包直接传递到 AF_XDP 套接字而不是内核网络栈。
|
||||
|
||||
## 用户空间程序
|
||||
|
||||
```c
|
||||
// SPDX-License-Identifier: (LGPL-2.1 OR BSD-2-Clause)
|
||||
#define _FILE_OFFSET_BITS 64
|
||||
#include <arpa/inet.h>
|
||||
#include <ctype.h>
|
||||
#include <errno.h>
|
||||
#include <getopt.h>
|
||||
#include <linux/if_ether.h>
|
||||
#include <linux/if_link.h>
|
||||
#include <linux/if_xdp.h>
|
||||
#include <linux/ip.h>
|
||||
#include <linux/udp.h>
|
||||
#include <net/if.h>
|
||||
#include <poll.h>
|
||||
#include <signal.h>
|
||||
#include <stdbool.h>
|
||||
#include <stdint.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <sys/mman.h>
|
||||
#include <sys/socket.h>
|
||||
#include <unistd.h>
|
||||
#include <bpf/bpf.h>
|
||||
#include <bpf/libbpf.h>
|
||||
#include "afxdp_dump.h"
|
||||
#include "afxdp_dump.skel.h"
|
||||
|
||||
#ifndef SOL_XDP
|
||||
#define SOL_XDP 283
|
||||
#endif
|
||||
#ifndef AF_XDP
|
||||
#define AF_XDP 44
|
||||
#endif
|
||||
#ifndef PF_XDP
|
||||
#define PF_XDP AF_XDP
|
||||
#endif
|
||||
|
||||
#define FRAME_SIZE 4096
|
||||
#define FRAME_COUNT 64
|
||||
#define RING_SIZE 64
|
||||
#define UMEM_SIZE ((size_t)FRAME_SIZE * FRAME_COUNT)
|
||||
#define PAYLOAD_PREVIEW 32
|
||||
#define IPV4_MORE_FRAGMENTS 0x2000
|
||||
#define IPV4_FRAGMENT_OFFSET 0x1fff
|
||||
|
||||
struct options {
|
||||
const char *interface;
|
||||
unsigned int queue;
|
||||
unsigned int port;
|
||||
unsigned int count;
|
||||
bool skb_mode;
|
||||
};
|
||||
|
||||
struct mapped_ring {
|
||||
void *mapping;
|
||||
size_t mapping_size;
|
||||
unsigned int *producer;
|
||||
unsigned int *consumer;
|
||||
unsigned int *flags;
|
||||
void *descriptors;
|
||||
unsigned int mask;
|
||||
};
|
||||
|
||||
struct xsk_state {
|
||||
int fd;
|
||||
void *umem;
|
||||
struct mapped_ring fill;
|
||||
struct mapped_ring completion;
|
||||
struct mapped_ring rx;
|
||||
};
|
||||
|
||||
static volatile sig_atomic_t stop;
|
||||
|
||||
static void handle_signal(int signal_number)
|
||||
{
|
||||
(void)signal_number;
|
||||
stop = 1;
|
||||
}
|
||||
|
||||
static void map_ring_fields(struct mapped_ring *ring, void *mapping,
|
||||
size_t mapping_size,
|
||||
const struct xdp_ring_offset *offset)
|
||||
{
|
||||
ring->mapping = mapping;
|
||||
ring->mapping_size = mapping_size;
|
||||
ring->producer = mapping + offset->producer;
|
||||
ring->consumer = mapping + offset->consumer;
|
||||
ring->flags = mapping + offset->flags;
|
||||
ring->descriptors = mapping + offset->desc;
|
||||
ring->mask = RING_SIZE - 1;
|
||||
}
|
||||
|
||||
static int map_xsk_ring(struct mapped_ring *ring, int fd,
|
||||
const struct xdp_ring_offset *offset,
|
||||
off_t page_offset, size_t descriptor_size)
|
||||
{
|
||||
size_t size = offset->desc + RING_SIZE * descriptor_size;
|
||||
void *mapping;
|
||||
|
||||
mapping = mmap(NULL, size, PROT_READ | PROT_WRITE,
|
||||
MAP_SHARED | MAP_POPULATE, fd, page_offset);
|
||||
if (mapping == MAP_FAILED)
|
||||
return -1;
|
||||
map_ring_fields(ring, mapping, size, offset);
|
||||
return 0;
|
||||
}
|
||||
|
||||
static void close_xsk(struct xsk_state *xsk)
|
||||
{
|
||||
struct mapped_ring *rings[] = {
|
||||
&xsk->rx, &xsk->completion, &xsk->fill,
|
||||
};
|
||||
|
||||
for (size_t i = 0; i < sizeof(rings) / sizeof(rings[0]); i++)
|
||||
if (rings[i]->mapping && rings[i]->mapping != MAP_FAILED)
|
||||
munmap(rings[i]->mapping, rings[i]->mapping_size);
|
||||
if (xsk->fd >= 0)
|
||||
close(xsk->fd);
|
||||
if (xsk->umem && xsk->umem != MAP_FAILED)
|
||||
munmap(xsk->umem, UMEM_SIZE);
|
||||
memset(xsk, 0, sizeof(*xsk));
|
||||
xsk->fd = -1;
|
||||
}
|
||||
|
||||
static int open_xsk(struct xsk_state *xsk, int ifindex, unsigned int queue)
|
||||
{
|
||||
struct xdp_umem_reg registration = {
|
||||
.len = UMEM_SIZE,
|
||||
.chunk_size = FRAME_SIZE,
|
||||
};
|
||||
struct sockaddr_xdp address = {
|
||||
.sxdp_family = PF_XDP,
|
||||
.sxdp_ifindex = ifindex,
|
||||
.sxdp_queue_id = queue,
|
||||
.sxdp_flags = XDP_COPY,
|
||||
};
|
||||
struct xdp_mmap_offsets offsets;
|
||||
socklen_t offsets_size = sizeof(offsets);
|
||||
unsigned int ring_size = RING_SIZE;
|
||||
unsigned long long *fill_addresses;
|
||||
|
||||
memset(xsk, 0, sizeof(*xsk));
|
||||
xsk->fd = -1;
|
||||
xsk->umem = mmap(NULL, UMEM_SIZE, PROT_READ | PROT_WRITE,
|
||||
MAP_PRIVATE | MAP_ANONYMOUS | MAP_NORESERVE, -1, 0);
|
||||
if (xsk->umem == MAP_FAILED)
|
||||
return -1;
|
||||
xsk->fd = socket(AF_XDP, SOCK_RAW | SOCK_CLOEXEC, 0);
|
||||
if (xsk->fd < 0)
|
||||
return -1;
|
||||
registration.addr = (uintptr_t)xsk->umem;
|
||||
if (setsockopt(xsk->fd, SOL_XDP, XDP_UMEM_REG, ®istration,
|
||||
sizeof(registration)) ||
|
||||
setsockopt(xsk->fd, SOL_XDP, XDP_UMEM_FILL_RING, &ring_size,
|
||||
sizeof(ring_size)) ||
|
||||
setsockopt(xsk->fd, SOL_XDP, XDP_UMEM_COMPLETION_RING, &ring_size,
|
||||
sizeof(ring_size)) ||
|
||||
setsockopt(xsk->fd, SOL_XDP, XDP_RX_RING, &ring_size,
|
||||
sizeof(ring_size)) ||
|
||||
getsockopt(xsk->fd, SOL_XDP, XDP_MMAP_OFFSETS, &offsets,
|
||||
&offsets_size))
|
||||
return -1;
|
||||
|
||||
if (map_xsk_ring(&xsk->fill, xsk->fd, &offsets.fr,
|
||||
XDP_UMEM_PGOFF_FILL_RING, sizeof(unsigned long long)) ||
|
||||
map_xsk_ring(&xsk->completion, xsk->fd, &offsets.cr,
|
||||
XDP_UMEM_PGOFF_COMPLETION_RING,
|
||||
sizeof(unsigned long long)) ||
|
||||
map_xsk_ring(&xsk->rx, xsk->fd, &offsets.rx,
|
||||
XDP_PGOFF_RX_RING, sizeof(struct xdp_desc)) ||
|
||||
bind(xsk->fd, (struct sockaddr *)&address, sizeof(address)))
|
||||
return -1;
|
||||
|
||||
fill_addresses = xsk->fill.descriptors;
|
||||
for (unsigned int i = 0; i < FRAME_COUNT; i++)
|
||||
fill_addresses[i & xsk->fill.mask] =
|
||||
(unsigned long long)i * FRAME_SIZE;
|
||||
__atomic_store_n(xsk->fill.producer, FRAME_COUNT, __ATOMIC_RELEASE);
|
||||
return 0;
|
||||
}
|
||||
|
||||
static unsigned long long data_address(unsigned long long address)
|
||||
{
|
||||
return (address & XSK_UNALIGNED_BUF_ADDR_MASK) +
|
||||
(address >> XSK_UNALIGNED_BUF_OFFSET_SHIFT);
|
||||
}
|
||||
|
||||
static int recycle_frame(struct xsk_state *xsk, unsigned long long address)
|
||||
{
|
||||
unsigned long long *fill_addresses = xsk->fill.descriptors;
|
||||
unsigned int producer, consumer;
|
||||
|
||||
producer = __atomic_load_n(xsk->fill.producer, __ATOMIC_RELAXED);
|
||||
consumer = __atomic_load_n(xsk->fill.consumer, __ATOMIC_ACQUIRE);
|
||||
if (producer - consumer == RING_SIZE)
|
||||
return -1;
|
||||
fill_addresses[producer & xsk->fill.mask] = address;
|
||||
__atomic_store_n(xsk->fill.producer, producer + 1, __ATOMIC_RELEASE);
|
||||
return 0;
|
||||
}
|
||||
|
||||
static void dump_packet(const void *packet, unsigned int length,
|
||||
unsigned int packet_number)
|
||||
{
|
||||
const struct ethhdr *ethernet = packet;
|
||||
const struct iphdr *ip;
|
||||
const struct udphdr *udp;
|
||||
const unsigned char *payload;
|
||||
char source[INET_ADDRSTRLEN], destination[INET_ADDRSTRLEN];
|
||||
char preview[PAYLOAD_PREVIEW + 1];
|
||||
unsigned int available_ip, available_payload, ip_header_length;
|
||||
unsigned int ip_length, payload_length, preview_length, udp_length;
|
||||
|
||||
if (length < sizeof(*ethernet) + sizeof(*ip) ||
|
||||
ethernet->h_proto != htons(ETH_P_IP))
|
||||
return;
|
||||
ip = packet + sizeof(*ethernet);
|
||||
ip_header_length = ip->ihl * 4;
|
||||
if (ip->version != 4 || ip->protocol != IPPROTO_UDP ||
|
||||
(ntohs(ip->frag_off) &
|
||||
(IPV4_MORE_FRAGMENTS | IPV4_FRAGMENT_OFFSET)) ||
|
||||
ip_header_length < sizeof(*ip) ||
|
||||
length < sizeof(*ethernet) + ip_header_length + sizeof(*udp))
|
||||
return;
|
||||
ip_length = ntohs(ip->tot_len);
|
||||
available_ip = length - sizeof(*ethernet);
|
||||
if (ip_length < ip_header_length + sizeof(*udp) ||
|
||||
ip_length > available_ip)
|
||||
return;
|
||||
udp = packet + sizeof(*ethernet) + ip_header_length;
|
||||
payload = (const unsigned char *)(udp + 1);
|
||||
udp_length = ntohs(udp->len);
|
||||
available_payload = length - (payload - (const unsigned char *)packet);
|
||||
if (udp_length < sizeof(*udp) ||
|
||||
udp_length > ip_length - ip_header_length ||
|
||||
udp_length - sizeof(*udp) > available_payload)
|
||||
return;
|
||||
payload_length = udp_length - sizeof(*udp);
|
||||
preview_length = payload_length < PAYLOAD_PREVIEW ?
|
||||
payload_length : PAYLOAD_PREVIEW;
|
||||
for (unsigned int i = 0; i < preview_length; i++)
|
||||
preview[i] = isprint(payload[i]) ? payload[i] : '.';
|
||||
preview[preview_length] = '\0';
|
||||
inet_ntop(AF_INET, &ip->saddr, source, sizeof(source));
|
||||
inet_ntop(AF_INET, &ip->daddr, destination, sizeof(destination));
|
||||
printf("packet=%u %s:%u -> %s:%u bytes=%u payload=\"%s\"\n",
|
||||
packet_number, source, ntohs(udp->source), destination,
|
||||
ntohs(udp->dest), length, preview);
|
||||
}
|
||||
|
||||
static int receive_packets(struct xsk_state *xsk, unsigned int count)
|
||||
{
|
||||
struct pollfd poll_fd = { .fd = xsk->fd, .events = POLLIN };
|
||||
unsigned int received = 0;
|
||||
|
||||
while (!stop && (!count || received < count)) {
|
||||
unsigned int consumer, producer;
|
||||
int poll_result = poll(&poll_fd, 1, 250);
|
||||
|
||||
if (poll_result < 0) {
|
||||
if (errno == EINTR)
|
||||
continue;
|
||||
return -1;
|
||||
}
|
||||
if (!poll_result)
|
||||
continue;
|
||||
consumer = __atomic_load_n(xsk->rx.consumer, __ATOMIC_RELAXED);
|
||||
producer = __atomic_load_n(xsk->rx.producer, __ATOMIC_ACQUIRE);
|
||||
while (consumer != producer && (!count || received < count)) {
|
||||
struct xdp_desc *descriptors = xsk->rx.descriptors;
|
||||
struct xdp_desc descriptor =
|
||||
descriptors[consumer & xsk->rx.mask];
|
||||
unsigned long long address = data_address(descriptor.addr);
|
||||
|
||||
if (descriptor.options & XDP_PKT_CONTD) {
|
||||
errno = EMSGSIZE;
|
||||
return -1;
|
||||
}
|
||||
if (address + descriptor.len > UMEM_SIZE) {
|
||||
errno = EFAULT;
|
||||
return -1;
|
||||
}
|
||||
received++;
|
||||
dump_packet(xsk->umem + address, descriptor.len, received);
|
||||
consumer++;
|
||||
__atomic_store_n(xsk->rx.consumer, consumer,
|
||||
__ATOMIC_RELEASE);
|
||||
if (recycle_frame(xsk, descriptor.addr)) {
|
||||
errno = ENOBUFS;
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int parse_uint(const char *text, unsigned int maximum,
|
||||
unsigned int *value)
|
||||
{
|
||||
char *end = NULL;
|
||||
unsigned long parsed;
|
||||
|
||||
errno = 0;
|
||||
parsed = strtoul(text, &end, 10);
|
||||
if (errno || !*text || *end || parsed > maximum)
|
||||
return -1;
|
||||
*value = parsed;
|
||||
return 0;
|
||||
}
|
||||
|
||||
static void usage(const char *program)
|
||||
{
|
||||
printf("Usage: %s --interface IFACE [--queue N] [--port PORT] [--count N] [--skb-mode]\n",
|
||||
program);
|
||||
}
|
||||
|
||||
static int parse_options(int argc, char **argv, struct options *options)
|
||||
{
|
||||
static const struct option long_options[] = {
|
||||
{ "interface", required_argument, NULL, 'i' },
|
||||
{ "queue", required_argument, NULL, 'q' },
|
||||
{ "port", required_argument, NULL, 'p' },
|
||||
{ "count", required_argument, NULL, 'n' },
|
||||
{ "skb-mode", no_argument, NULL, 'S' },
|
||||
{ "help", no_argument, NULL, 'h' },
|
||||
{},
|
||||
};
|
||||
int option;
|
||||
|
||||
while ((option = getopt_long(argc, argv, "i:q:p:n:Sh", long_options,
|
||||
NULL)) != -1) {
|
||||
switch (option) {
|
||||
case 'i': options->interface = optarg; break;
|
||||
case 'q':
|
||||
if (parse_uint(optarg, AFXDP_MAX_QUEUES - 1,
|
||||
&options->queue))
|
||||
return -1;
|
||||
break;
|
||||
case 'p':
|
||||
if (parse_uint(optarg, 65535, &options->port) ||
|
||||
!options->port)
|
||||
return -1;
|
||||
break;
|
||||
case 'n':
|
||||
if (parse_uint(optarg, 1000000, &options->count))
|
||||
return -1;
|
||||
break;
|
||||
case 'S': options->skb_mode = true; break;
|
||||
case 'h': usage(argv[0]); exit(0);
|
||||
default: return -1;
|
||||
}
|
||||
}
|
||||
return optind == argc && options->interface ? 0 : -1;
|
||||
}
|
||||
|
||||
static int attach_xdp(int ifindex, int program_fd, bool skb_mode,
|
||||
unsigned int *attached_flags)
|
||||
{
|
||||
unsigned int flags = XDP_FLAGS_UPDATE_IF_NOEXIST |
|
||||
(skb_mode ? XDP_FLAGS_SKB_MODE : XDP_FLAGS_DRV_MODE);
|
||||
int err;
|
||||
|
||||
err = bpf_xdp_attach(ifindex, program_fd, flags, NULL);
|
||||
if (err && !skb_mode && (err == -EOPNOTSUPP || err == -EINVAL)) {
|
||||
flags = XDP_FLAGS_UPDATE_IF_NOEXIST | XDP_FLAGS_SKB_MODE;
|
||||
err = bpf_xdp_attach(ifindex, program_fd, flags, NULL);
|
||||
}
|
||||
if (!err)
|
||||
*attached_flags = flags;
|
||||
return err;
|
||||
}
|
||||
|
||||
int main(int argc, char **argv)
|
||||
{
|
||||
struct options options = { .port = 8080 };
|
||||
struct afxdp_dump_bpf *skel = NULL;
|
||||
struct xsk_state xsk;
|
||||
unsigned int attached_flags = 0;
|
||||
int ifindex;
|
||||
int err = 1;
|
||||
bool attached = false;
|
||||
|
||||
setvbuf(stdout, NULL, _IONBF, 0);
|
||||
memset(&xsk, 0, sizeof(xsk));
|
||||
xsk.fd = -1;
|
||||
if (parse_options(argc, argv, &options)) {
|
||||
usage(argv[0]);
|
||||
return 2;
|
||||
}
|
||||
ifindex = if_nametoindex(options.interface);
|
||||
if (!ifindex) {
|
||||
fprintf(stderr, "unknown interface: %s\n", options.interface);
|
||||
return 2;
|
||||
}
|
||||
if (open_xsk(&xsk, ifindex, options.queue)) {
|
||||
fprintf(stderr, "failed to create AF_XDP socket: %s\n",
|
||||
strerror(errno));
|
||||
goto cleanup;
|
||||
}
|
||||
skel = afxdp_dump_bpf__open();
|
||||
if (!skel)
|
||||
goto cleanup;
|
||||
skel->rodata->capture_port = options.port;
|
||||
if (afxdp_dump_bpf__load(skel)) {
|
||||
fprintf(stderr, "failed to load XDP program\n");
|
||||
goto cleanup;
|
||||
}
|
||||
if (bpf_map__update_elem(skel->maps.xsk_map, &options.queue,
|
||||
sizeof(options.queue), &xsk.fd, sizeof(xsk.fd),
|
||||
BPF_ANY)) {
|
||||
fprintf(stderr, "failed to populate XSK map: %s\n", strerror(errno));
|
||||
goto cleanup;
|
||||
}
|
||||
{
|
||||
int attach_error = attach_xdp(ifindex,
|
||||
bpf_program__fd(skel->progs.redirect_udp),
|
||||
options.skb_mode, &attached_flags);
|
||||
|
||||
if (attach_error) {
|
||||
fprintf(stderr, "failed to attach XDP program: %s\n",
|
||||
strerror(-attach_error));
|
||||
goto cleanup;
|
||||
}
|
||||
}
|
||||
attached = true;
|
||||
signal(SIGINT, handle_signal);
|
||||
signal(SIGTERM, handle_signal);
|
||||
printf("afxdp-dump ready interface=%s queue=%u port=%u mode=%s count=%u\n",
|
||||
options.interface, options.queue, options.port,
|
||||
attached_flags & XDP_FLAGS_SKB_MODE ? "skb" : "driver",
|
||||
options.count);
|
||||
|
||||
if (receive_packets(&xsk, options.count)) {
|
||||
fprintf(stderr, "AF_XDP receive failed: %s\n", strerror(errno));
|
||||
goto cleanup;
|
||||
}
|
||||
printf("redirected=%llu\n",
|
||||
(unsigned long long)skel->bss->redirected_packets);
|
||||
err = 0;
|
||||
|
||||
cleanup:
|
||||
if (attached) {
|
||||
LIBBPF_OPTS(bpf_xdp_attach_opts, detach_options,
|
||||
.old_prog_fd = bpf_program__fd(
|
||||
skel->progs.redirect_udp));
|
||||
int detach_error = bpf_xdp_detach(
|
||||
ifindex, attached_flags & XDP_FLAGS_MODES,
|
||||
&detach_options);
|
||||
|
||||
if (detach_error) {
|
||||
fprintf(stderr, "failed to detach XDP program: %s\n",
|
||||
strerror(-detach_error));
|
||||
err = 1;
|
||||
}
|
||||
}
|
||||
afxdp_dump_bpf__destroy(skel);
|
||||
close_xsk(&xsk);
|
||||
return err;
|
||||
}
|
||||
```
|
||||
|
||||
用户空间程序设置 AF_XDP 涉及几个组件:
|
||||
|
||||
**UMEM**:一个 256KB 区域(64 个 4KB 帧),使用 `mmap()` 分配并通过 `XDP_UMEM_REG` 注册到内核。数据包直接接收到这些帧中。
|
||||
|
||||
**Fill ring(填充环)**:预填充所有 64 个帧地址,告诉内核将传入数据包放在哪里。
|
||||
|
||||
**RX ring(接收环)**:轮询数据包描述符。当数据包到达时,内核在此处发布包含帧地址和长度的描述符。
|
||||
|
||||
**XSKMAP**:使用 AF_XDP 套接字文件描述符更新,以启用从 XDP 的数据包重定向。
|
||||
|
||||
绑定套接字时使用 `XDP_COPY` 标志,它把报文从驱动缓冲区复制到 UMEM,而不是使用基于 DMA 的零拷贝。拷贝模式可以覆盖更多驱动,也让这篇入门 AF_XDP 教程不依赖驱动的零拷贝支持。
|
||||
|
||||
## 编译和执行
|
||||
|
||||
构建工具:
|
||||
|
||||
```bash
|
||||
cd src/60-afxdp-dump
|
||||
make
|
||||
```
|
||||
|
||||
使用选项运行:
|
||||
|
||||
```bash
|
||||
sudo ./afxdp_dump --interface eth0 --queue 0 --port 8080 --count 5
|
||||
```
|
||||
|
||||
选项:
|
||||
|
||||
- `--interface IFACE`:要捕获的网络接口
|
||||
- `--queue N`:RX 队列索引(默认 0)
|
||||
- `--port PORT`:要过滤的 UDP 目标端口(默认 8080)
|
||||
- `--count N`:要捕获的数据包数量(0 表示无限)
|
||||
- `--skb-mode`:强制使用 SKB 模式而不是驱动模式
|
||||
|
||||
运行确定性的集成测试,它会在最初投递的 64 个 frame 之外继续验证 frame 回收,并确认不匹配的 UDP 报文仍然进入普通网络栈:
|
||||
|
||||
```bash
|
||||
sudo make test
|
||||
```
|
||||
|
||||
测试输出如下:
|
||||
|
||||
```text
|
||||
afxdp-dump ready interface=axdp1267r queue=0 port=8080 mode=driver count=65
|
||||
packet=1 10.77.0.1:60414 -> 10.77.0.2:8080 bytes=53 payload="hello-afxdp"
|
||||
packet=65 10.77.0.1:60414 -> 10.77.0.2:8080 bytes=53 payload="hello-afxdp"
|
||||
redirected=65
|
||||
nonmatching-pass=verified
|
||||
AF_XDP dump integration test: PASS
|
||||
```
|
||||
|
||||
## 环境要求
|
||||
|
||||
| 要求 | 详情 |
|
||||
|------|------|
|
||||
| 内核 | Linux 5.3+(XDP 中的 XSKMAP 查找) |
|
||||
| 配置 | `CONFIG_BPF_SYSCALL`、`CONFIG_XDP_SOCKETS` |
|
||||
| 权限 | Root |
|
||||
| 接口 | 具有所选 RX 队列的网络接口,工具会回退到 generic XDP,因此 native XDP 是可选项 |
|
||||
|
||||
## 重定向数据包的去向
|
||||
|
||||
重定向到 AF_XDP 的数据包是被消费的,而不是被镜像。它们不会继续进入正常的内核网络栈。在常规套接字上监听的应用程序将看不到这些数据包。不匹配的数据包(错误的端口、错误的协议)正常传递并由内核处理。
|
||||
|
||||
## 实现细节
|
||||
|
||||
这是一个基本的单缓冲区仅接收 AF_XDP 转储器:
|
||||
|
||||
- **拷贝模式**:使用 `XDP_COPY`,不依赖驱动的零拷贝支持
|
||||
- **单队列**:绑定到一个 RX 队列
|
||||
- **64 帧 UMEM**:使用固定大小控制内存占用
|
||||
- **帧回收**:处理后将帧返回 fill ring
|
||||
- **安全附加**:使用 `UPDATE_IF_NOEXIST` 和比较-分离清理
|
||||
|
||||
## XDP 附加模式
|
||||
|
||||
该工具支持两种 XDP 模式:
|
||||
|
||||
- **驱动模式**(原生):需要驱动支持 XDP
|
||||
- **SKB 模式**(通用):无需驱动提供 native XDP 支持
|
||||
|
||||
我们首先尝试驱动模式,然后回退到 SKB 模式:
|
||||
|
||||
```c
|
||||
err = bpf_xdp_attach(ifindex, program_fd, XDP_FLAGS_DRV_MODE, NULL);
|
||||
if (err && (err == -EOPNOTSUPP || err == -EINVAL)) {
|
||||
err = bpf_xdp_attach(ifindex, program_fd, XDP_FLAGS_SKB_MODE, NULL);
|
||||
}
|
||||
```
|
||||
|
||||
## 总结
|
||||
|
||||
这个例子走完了 AF_XDP 接收路径,XDP 程序选择一个 UDP 流,XSKMAP 把它重定向到已经绑定的队列,用户态从 RX ring 读取描述符,再把处理过的 frame 放回 fill ring。拷贝模式和 generic XDP 自动回退让工具可以运行在没有 native zero-copy 支持的系统上。
|
||||
|
||||
> 如果你想深入了解 eBPF,请查看我们的教程代码仓库 <https://github.com/eunomia-bpf/bpf-developer-tutorial> 或访问我们的网站 <https://eunomia.dev/tutorials/>。
|
||||
|
||||
## 参考资料
|
||||
|
||||
- [AF_XDP 内核文档](https://docs.kernel.org/networking/af_xdp.html)
|
||||
- [AF_XDP 引入 commit](https://github.com/torvalds/linux/commit/c0c77d8fb787cfe0c3fca689c2a30d1dad4eaba7)
|
||||
- [XDP 中的 XSKMAP 查找 commit](https://github.com/torvalds/linux/commit/fada7fdc83c0)
|
||||
- [libxdp 文档](https://github.com/xdp-project/xdp-tools/blob/master/lib/libxdp/README.org)
|
||||
62
src/60-afxdp-dump/afxdp_dump.bpf.c
Normal file
62
src/60-afxdp-dump/afxdp_dump.bpf.c
Normal file
@@ -0,0 +1,62 @@
|
||||
// SPDX-License-Identifier: GPL-2.0
|
||||
#include "vmlinux.h"
|
||||
#include <bpf/bpf_helpers.h>
|
||||
#include <bpf/bpf_endian.h>
|
||||
#include "afxdp_dump.h"
|
||||
|
||||
char LICENSE[] SEC("license") = "GPL";
|
||||
|
||||
#define ETH_P_IP 0x0800
|
||||
#define IPPROTO_UDP 17
|
||||
#define IP_MF 0x2000
|
||||
#define IP_OFFSET 0x1fff
|
||||
|
||||
const volatile __u16 capture_port = 8080;
|
||||
|
||||
struct {
|
||||
__uint(type, BPF_MAP_TYPE_XSKMAP);
|
||||
__uint(max_entries, AFXDP_MAX_QUEUES);
|
||||
__type(key, __u32);
|
||||
__type(value, __u32);
|
||||
} xsk_map SEC(".maps");
|
||||
|
||||
__u64 redirected_packets;
|
||||
|
||||
SEC("xdp")
|
||||
int redirect_udp(struct xdp_md *ctx)
|
||||
{
|
||||
void *data = (void *)(long)ctx->data;
|
||||
void *data_end = (void *)(long)ctx->data_end;
|
||||
struct ethhdr *ethernet = data;
|
||||
struct iphdr *ip;
|
||||
struct udphdr *udp;
|
||||
__u32 queue = ctx->rx_queue_index;
|
||||
__u32 ip_header_length;
|
||||
__u32 ip_length;
|
||||
__u32 udp_length;
|
||||
|
||||
if ((void *)(ethernet + 1) > data_end ||
|
||||
ethernet->h_proto != bpf_htons(ETH_P_IP))
|
||||
return XDP_PASS;
|
||||
ip = (void *)(ethernet + 1);
|
||||
if ((void *)(ip + 1) > data_end || ip->version != 4 ||
|
||||
ip->protocol != IPPROTO_UDP ||
|
||||
ip->ihl < 5 || (bpf_ntohs(ip->frag_off) & (IP_MF | IP_OFFSET)))
|
||||
return XDP_PASS;
|
||||
ip_header_length = ip->ihl * 4;
|
||||
ip_length = bpf_ntohs(ip->tot_len);
|
||||
udp = (void *)ip + ip_header_length;
|
||||
if (ip_length < ip_header_length + sizeof(*udp) ||
|
||||
(void *)ip + ip_length > data_end ||
|
||||
(void *)(udp + 1) > data_end)
|
||||
return XDP_PASS;
|
||||
udp_length = bpf_ntohs(udp->len);
|
||||
if (udp_length < sizeof(*udp) ||
|
||||
udp_length > ip_length - ip_header_length ||
|
||||
udp->dest != bpf_htons(capture_port))
|
||||
return XDP_PASS;
|
||||
if (!bpf_map_lookup_elem(&xsk_map, &queue))
|
||||
return XDP_PASS;
|
||||
__sync_fetch_and_add(&redirected_packets, 1);
|
||||
return bpf_redirect_map(&xsk_map, queue, XDP_PASS);
|
||||
}
|
||||
459
src/60-afxdp-dump/afxdp_dump.c
Normal file
459
src/60-afxdp-dump/afxdp_dump.c
Normal file
@@ -0,0 +1,459 @@
|
||||
// SPDX-License-Identifier: (LGPL-2.1 OR BSD-2-Clause)
|
||||
#define _FILE_OFFSET_BITS 64
|
||||
#include <arpa/inet.h>
|
||||
#include <ctype.h>
|
||||
#include <errno.h>
|
||||
#include <getopt.h>
|
||||
#include <linux/if_ether.h>
|
||||
#include <linux/if_link.h>
|
||||
#include <linux/if_xdp.h>
|
||||
#include <linux/ip.h>
|
||||
#include <linux/udp.h>
|
||||
#include <net/if.h>
|
||||
#include <poll.h>
|
||||
#include <signal.h>
|
||||
#include <stdbool.h>
|
||||
#include <stdint.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <sys/mman.h>
|
||||
#include <sys/socket.h>
|
||||
#include <unistd.h>
|
||||
#include <bpf/bpf.h>
|
||||
#include <bpf/libbpf.h>
|
||||
#include "afxdp_dump.h"
|
||||
#include "afxdp_dump.skel.h"
|
||||
|
||||
#ifndef SOL_XDP
|
||||
#define SOL_XDP 283
|
||||
#endif
|
||||
#ifndef AF_XDP
|
||||
#define AF_XDP 44
|
||||
#endif
|
||||
#ifndef PF_XDP
|
||||
#define PF_XDP AF_XDP
|
||||
#endif
|
||||
|
||||
#define FRAME_SIZE 4096
|
||||
#define FRAME_COUNT 64
|
||||
#define RING_SIZE 64
|
||||
#define UMEM_SIZE ((size_t)FRAME_SIZE * FRAME_COUNT)
|
||||
#define PAYLOAD_PREVIEW 32
|
||||
#define IPV4_MORE_FRAGMENTS 0x2000
|
||||
#define IPV4_FRAGMENT_OFFSET 0x1fff
|
||||
|
||||
struct options {
|
||||
const char *interface;
|
||||
unsigned int queue;
|
||||
unsigned int port;
|
||||
unsigned int count;
|
||||
bool skb_mode;
|
||||
};
|
||||
|
||||
struct mapped_ring {
|
||||
void *mapping;
|
||||
size_t mapping_size;
|
||||
unsigned int *producer;
|
||||
unsigned int *consumer;
|
||||
unsigned int *flags;
|
||||
void *descriptors;
|
||||
unsigned int mask;
|
||||
};
|
||||
|
||||
struct xsk_state {
|
||||
int fd;
|
||||
void *umem;
|
||||
struct mapped_ring fill;
|
||||
struct mapped_ring completion;
|
||||
struct mapped_ring rx;
|
||||
};
|
||||
|
||||
static volatile sig_atomic_t stop;
|
||||
|
||||
static void handle_signal(int signal_number)
|
||||
{
|
||||
(void)signal_number;
|
||||
stop = 1;
|
||||
}
|
||||
|
||||
static void map_ring_fields(struct mapped_ring *ring, void *mapping,
|
||||
size_t mapping_size,
|
||||
const struct xdp_ring_offset *offset)
|
||||
{
|
||||
ring->mapping = mapping;
|
||||
ring->mapping_size = mapping_size;
|
||||
ring->producer = mapping + offset->producer;
|
||||
ring->consumer = mapping + offset->consumer;
|
||||
ring->flags = mapping + offset->flags;
|
||||
ring->descriptors = mapping + offset->desc;
|
||||
ring->mask = RING_SIZE - 1;
|
||||
}
|
||||
|
||||
static int map_xsk_ring(struct mapped_ring *ring, int fd,
|
||||
const struct xdp_ring_offset *offset,
|
||||
off_t page_offset, size_t descriptor_size)
|
||||
{
|
||||
size_t size = offset->desc + RING_SIZE * descriptor_size;
|
||||
void *mapping;
|
||||
|
||||
mapping = mmap(NULL, size, PROT_READ | PROT_WRITE,
|
||||
MAP_SHARED | MAP_POPULATE, fd, page_offset);
|
||||
if (mapping == MAP_FAILED)
|
||||
return -1;
|
||||
map_ring_fields(ring, mapping, size, offset);
|
||||
return 0;
|
||||
}
|
||||
|
||||
static void close_xsk(struct xsk_state *xsk)
|
||||
{
|
||||
struct mapped_ring *rings[] = {
|
||||
&xsk->rx, &xsk->completion, &xsk->fill,
|
||||
};
|
||||
|
||||
for (size_t i = 0; i < sizeof(rings) / sizeof(rings[0]); i++)
|
||||
if (rings[i]->mapping && rings[i]->mapping != MAP_FAILED)
|
||||
munmap(rings[i]->mapping, rings[i]->mapping_size);
|
||||
if (xsk->fd >= 0)
|
||||
close(xsk->fd);
|
||||
if (xsk->umem && xsk->umem != MAP_FAILED)
|
||||
munmap(xsk->umem, UMEM_SIZE);
|
||||
memset(xsk, 0, sizeof(*xsk));
|
||||
xsk->fd = -1;
|
||||
}
|
||||
|
||||
static int open_xsk(struct xsk_state *xsk, int ifindex, unsigned int queue)
|
||||
{
|
||||
struct xdp_umem_reg registration = {
|
||||
.len = UMEM_SIZE,
|
||||
.chunk_size = FRAME_SIZE,
|
||||
};
|
||||
struct sockaddr_xdp address = {
|
||||
.sxdp_family = PF_XDP,
|
||||
.sxdp_ifindex = ifindex,
|
||||
.sxdp_queue_id = queue,
|
||||
.sxdp_flags = XDP_COPY,
|
||||
};
|
||||
struct xdp_mmap_offsets offsets;
|
||||
socklen_t offsets_size = sizeof(offsets);
|
||||
unsigned int ring_size = RING_SIZE;
|
||||
unsigned long long *fill_addresses;
|
||||
|
||||
memset(xsk, 0, sizeof(*xsk));
|
||||
xsk->fd = -1;
|
||||
xsk->umem = mmap(NULL, UMEM_SIZE, PROT_READ | PROT_WRITE,
|
||||
MAP_PRIVATE | MAP_ANONYMOUS | MAP_NORESERVE, -1, 0);
|
||||
if (xsk->umem == MAP_FAILED)
|
||||
return -1;
|
||||
xsk->fd = socket(AF_XDP, SOCK_RAW | SOCK_CLOEXEC, 0);
|
||||
if (xsk->fd < 0)
|
||||
return -1;
|
||||
registration.addr = (uintptr_t)xsk->umem;
|
||||
if (setsockopt(xsk->fd, SOL_XDP, XDP_UMEM_REG, ®istration,
|
||||
sizeof(registration)) ||
|
||||
setsockopt(xsk->fd, SOL_XDP, XDP_UMEM_FILL_RING, &ring_size,
|
||||
sizeof(ring_size)) ||
|
||||
setsockopt(xsk->fd, SOL_XDP, XDP_UMEM_COMPLETION_RING, &ring_size,
|
||||
sizeof(ring_size)) ||
|
||||
setsockopt(xsk->fd, SOL_XDP, XDP_RX_RING, &ring_size,
|
||||
sizeof(ring_size)) ||
|
||||
getsockopt(xsk->fd, SOL_XDP, XDP_MMAP_OFFSETS, &offsets,
|
||||
&offsets_size))
|
||||
return -1;
|
||||
|
||||
if (map_xsk_ring(&xsk->fill, xsk->fd, &offsets.fr,
|
||||
XDP_UMEM_PGOFF_FILL_RING, sizeof(unsigned long long)) ||
|
||||
map_xsk_ring(&xsk->completion, xsk->fd, &offsets.cr,
|
||||
XDP_UMEM_PGOFF_COMPLETION_RING,
|
||||
sizeof(unsigned long long)) ||
|
||||
map_xsk_ring(&xsk->rx, xsk->fd, &offsets.rx,
|
||||
XDP_PGOFF_RX_RING, sizeof(struct xdp_desc)) ||
|
||||
bind(xsk->fd, (struct sockaddr *)&address, sizeof(address)))
|
||||
return -1;
|
||||
|
||||
fill_addresses = xsk->fill.descriptors;
|
||||
for (unsigned int i = 0; i < FRAME_COUNT; i++)
|
||||
fill_addresses[i & xsk->fill.mask] =
|
||||
(unsigned long long)i * FRAME_SIZE;
|
||||
__atomic_store_n(xsk->fill.producer, FRAME_COUNT, __ATOMIC_RELEASE);
|
||||
return 0;
|
||||
}
|
||||
|
||||
static unsigned long long data_address(unsigned long long address)
|
||||
{
|
||||
return (address & XSK_UNALIGNED_BUF_ADDR_MASK) +
|
||||
(address >> XSK_UNALIGNED_BUF_OFFSET_SHIFT);
|
||||
}
|
||||
|
||||
static int recycle_frame(struct xsk_state *xsk, unsigned long long address)
|
||||
{
|
||||
unsigned long long *fill_addresses = xsk->fill.descriptors;
|
||||
unsigned int producer, consumer;
|
||||
|
||||
producer = __atomic_load_n(xsk->fill.producer, __ATOMIC_RELAXED);
|
||||
consumer = __atomic_load_n(xsk->fill.consumer, __ATOMIC_ACQUIRE);
|
||||
if (producer - consumer == RING_SIZE)
|
||||
return -1;
|
||||
fill_addresses[producer & xsk->fill.mask] = address;
|
||||
__atomic_store_n(xsk->fill.producer, producer + 1, __ATOMIC_RELEASE);
|
||||
return 0;
|
||||
}
|
||||
|
||||
static void dump_packet(const void *packet, unsigned int length,
|
||||
unsigned int packet_number)
|
||||
{
|
||||
const struct ethhdr *ethernet = packet;
|
||||
const struct iphdr *ip;
|
||||
const struct udphdr *udp;
|
||||
const unsigned char *payload;
|
||||
char source[INET_ADDRSTRLEN], destination[INET_ADDRSTRLEN];
|
||||
char preview[PAYLOAD_PREVIEW + 1];
|
||||
unsigned int available_ip, available_payload, ip_header_length;
|
||||
unsigned int ip_length, payload_length, preview_length, udp_length;
|
||||
|
||||
if (length < sizeof(*ethernet) + sizeof(*ip) ||
|
||||
ethernet->h_proto != htons(ETH_P_IP))
|
||||
return;
|
||||
ip = packet + sizeof(*ethernet);
|
||||
ip_header_length = ip->ihl * 4;
|
||||
if (ip->version != 4 || ip->protocol != IPPROTO_UDP ||
|
||||
(ntohs(ip->frag_off) &
|
||||
(IPV4_MORE_FRAGMENTS | IPV4_FRAGMENT_OFFSET)) ||
|
||||
ip_header_length < sizeof(*ip) ||
|
||||
length < sizeof(*ethernet) + ip_header_length + sizeof(*udp))
|
||||
return;
|
||||
ip_length = ntohs(ip->tot_len);
|
||||
available_ip = length - sizeof(*ethernet);
|
||||
if (ip_length < ip_header_length + sizeof(*udp) ||
|
||||
ip_length > available_ip)
|
||||
return;
|
||||
udp = packet + sizeof(*ethernet) + ip_header_length;
|
||||
payload = (const unsigned char *)(udp + 1);
|
||||
udp_length = ntohs(udp->len);
|
||||
available_payload = length - (payload - (const unsigned char *)packet);
|
||||
if (udp_length < sizeof(*udp) ||
|
||||
udp_length > ip_length - ip_header_length ||
|
||||
udp_length - sizeof(*udp) > available_payload)
|
||||
return;
|
||||
payload_length = udp_length - sizeof(*udp);
|
||||
preview_length = payload_length < PAYLOAD_PREVIEW ?
|
||||
payload_length : PAYLOAD_PREVIEW;
|
||||
for (unsigned int i = 0; i < preview_length; i++)
|
||||
preview[i] = isprint(payload[i]) ? payload[i] : '.';
|
||||
preview[preview_length] = '\0';
|
||||
inet_ntop(AF_INET, &ip->saddr, source, sizeof(source));
|
||||
inet_ntop(AF_INET, &ip->daddr, destination, sizeof(destination));
|
||||
printf("packet=%u %s:%u -> %s:%u bytes=%u payload=\"%s\"\n",
|
||||
packet_number, source, ntohs(udp->source), destination,
|
||||
ntohs(udp->dest), length, preview);
|
||||
}
|
||||
|
||||
static int receive_packets(struct xsk_state *xsk, unsigned int count)
|
||||
{
|
||||
struct pollfd poll_fd = { .fd = xsk->fd, .events = POLLIN };
|
||||
unsigned int received = 0;
|
||||
|
||||
while (!stop && (!count || received < count)) {
|
||||
unsigned int consumer, producer;
|
||||
int poll_result = poll(&poll_fd, 1, 250);
|
||||
|
||||
if (poll_result < 0) {
|
||||
if (errno == EINTR)
|
||||
continue;
|
||||
return -1;
|
||||
}
|
||||
if (!poll_result)
|
||||
continue;
|
||||
consumer = __atomic_load_n(xsk->rx.consumer, __ATOMIC_RELAXED);
|
||||
producer = __atomic_load_n(xsk->rx.producer, __ATOMIC_ACQUIRE);
|
||||
while (consumer != producer && (!count || received < count)) {
|
||||
struct xdp_desc *descriptors = xsk->rx.descriptors;
|
||||
struct xdp_desc descriptor =
|
||||
descriptors[consumer & xsk->rx.mask];
|
||||
unsigned long long address = data_address(descriptor.addr);
|
||||
|
||||
if (descriptor.options & XDP_PKT_CONTD) {
|
||||
errno = EMSGSIZE;
|
||||
return -1;
|
||||
}
|
||||
if (address + descriptor.len > UMEM_SIZE) {
|
||||
errno = EFAULT;
|
||||
return -1;
|
||||
}
|
||||
received++;
|
||||
dump_packet(xsk->umem + address, descriptor.len, received);
|
||||
consumer++;
|
||||
__atomic_store_n(xsk->rx.consumer, consumer,
|
||||
__ATOMIC_RELEASE);
|
||||
if (recycle_frame(xsk, descriptor.addr)) {
|
||||
errno = ENOBUFS;
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int parse_uint(const char *text, unsigned int maximum,
|
||||
unsigned int *value)
|
||||
{
|
||||
char *end = NULL;
|
||||
unsigned long parsed;
|
||||
|
||||
errno = 0;
|
||||
parsed = strtoul(text, &end, 10);
|
||||
if (errno || !*text || *end || parsed > maximum)
|
||||
return -1;
|
||||
*value = parsed;
|
||||
return 0;
|
||||
}
|
||||
|
||||
static void usage(const char *program)
|
||||
{
|
||||
printf("Usage: %s --interface IFACE [--queue N] [--port PORT] [--count N] [--skb-mode]\n",
|
||||
program);
|
||||
}
|
||||
|
||||
static int parse_options(int argc, char **argv, struct options *options)
|
||||
{
|
||||
static const struct option long_options[] = {
|
||||
{ "interface", required_argument, NULL, 'i' },
|
||||
{ "queue", required_argument, NULL, 'q' },
|
||||
{ "port", required_argument, NULL, 'p' },
|
||||
{ "count", required_argument, NULL, 'n' },
|
||||
{ "skb-mode", no_argument, NULL, 'S' },
|
||||
{ "help", no_argument, NULL, 'h' },
|
||||
{},
|
||||
};
|
||||
int option;
|
||||
|
||||
while ((option = getopt_long(argc, argv, "i:q:p:n:Sh", long_options,
|
||||
NULL)) != -1) {
|
||||
switch (option) {
|
||||
case 'i': options->interface = optarg; break;
|
||||
case 'q':
|
||||
if (parse_uint(optarg, AFXDP_MAX_QUEUES - 1,
|
||||
&options->queue))
|
||||
return -1;
|
||||
break;
|
||||
case 'p':
|
||||
if (parse_uint(optarg, 65535, &options->port) ||
|
||||
!options->port)
|
||||
return -1;
|
||||
break;
|
||||
case 'n':
|
||||
if (parse_uint(optarg, 1000000, &options->count))
|
||||
return -1;
|
||||
break;
|
||||
case 'S': options->skb_mode = true; break;
|
||||
case 'h': usage(argv[0]); exit(0);
|
||||
default: return -1;
|
||||
}
|
||||
}
|
||||
return optind == argc && options->interface ? 0 : -1;
|
||||
}
|
||||
|
||||
static int attach_xdp(int ifindex, int program_fd, bool skb_mode,
|
||||
unsigned int *attached_flags)
|
||||
{
|
||||
unsigned int flags = XDP_FLAGS_UPDATE_IF_NOEXIST |
|
||||
(skb_mode ? XDP_FLAGS_SKB_MODE : XDP_FLAGS_DRV_MODE);
|
||||
int err;
|
||||
|
||||
err = bpf_xdp_attach(ifindex, program_fd, flags, NULL);
|
||||
if (err && !skb_mode && (err == -EOPNOTSUPP || err == -EINVAL)) {
|
||||
flags = XDP_FLAGS_UPDATE_IF_NOEXIST | XDP_FLAGS_SKB_MODE;
|
||||
err = bpf_xdp_attach(ifindex, program_fd, flags, NULL);
|
||||
}
|
||||
if (!err)
|
||||
*attached_flags = flags;
|
||||
return err;
|
||||
}
|
||||
|
||||
int main(int argc, char **argv)
|
||||
{
|
||||
struct options options = { .port = 8080 };
|
||||
struct afxdp_dump_bpf *skel = NULL;
|
||||
struct xsk_state xsk;
|
||||
unsigned int attached_flags = 0;
|
||||
int ifindex;
|
||||
int err = 1;
|
||||
bool attached = false;
|
||||
|
||||
setvbuf(stdout, NULL, _IONBF, 0);
|
||||
memset(&xsk, 0, sizeof(xsk));
|
||||
xsk.fd = -1;
|
||||
if (parse_options(argc, argv, &options)) {
|
||||
usage(argv[0]);
|
||||
return 2;
|
||||
}
|
||||
ifindex = if_nametoindex(options.interface);
|
||||
if (!ifindex) {
|
||||
fprintf(stderr, "unknown interface: %s\n", options.interface);
|
||||
return 2;
|
||||
}
|
||||
if (open_xsk(&xsk, ifindex, options.queue)) {
|
||||
fprintf(stderr, "failed to create AF_XDP socket: %s\n",
|
||||
strerror(errno));
|
||||
goto cleanup;
|
||||
}
|
||||
skel = afxdp_dump_bpf__open();
|
||||
if (!skel)
|
||||
goto cleanup;
|
||||
skel->rodata->capture_port = options.port;
|
||||
if (afxdp_dump_bpf__load(skel)) {
|
||||
fprintf(stderr, "failed to load XDP program\n");
|
||||
goto cleanup;
|
||||
}
|
||||
if (bpf_map__update_elem(skel->maps.xsk_map, &options.queue,
|
||||
sizeof(options.queue), &xsk.fd, sizeof(xsk.fd),
|
||||
BPF_ANY)) {
|
||||
fprintf(stderr, "failed to populate XSK map: %s\n", strerror(errno));
|
||||
goto cleanup;
|
||||
}
|
||||
{
|
||||
int attach_error = attach_xdp(ifindex,
|
||||
bpf_program__fd(skel->progs.redirect_udp),
|
||||
options.skb_mode, &attached_flags);
|
||||
|
||||
if (attach_error) {
|
||||
fprintf(stderr, "failed to attach XDP program: %s\n",
|
||||
strerror(-attach_error));
|
||||
goto cleanup;
|
||||
}
|
||||
}
|
||||
attached = true;
|
||||
signal(SIGINT, handle_signal);
|
||||
signal(SIGTERM, handle_signal);
|
||||
printf("afxdp-dump ready interface=%s queue=%u port=%u mode=%s count=%u\n",
|
||||
options.interface, options.queue, options.port,
|
||||
attached_flags & XDP_FLAGS_SKB_MODE ? "skb" : "driver",
|
||||
options.count);
|
||||
|
||||
if (receive_packets(&xsk, options.count)) {
|
||||
fprintf(stderr, "AF_XDP receive failed: %s\n", strerror(errno));
|
||||
goto cleanup;
|
||||
}
|
||||
printf("redirected=%llu\n",
|
||||
(unsigned long long)skel->bss->redirected_packets);
|
||||
err = 0;
|
||||
|
||||
cleanup:
|
||||
if (attached) {
|
||||
LIBBPF_OPTS(bpf_xdp_attach_opts, detach_options,
|
||||
.old_prog_fd = bpf_program__fd(
|
||||
skel->progs.redirect_udp));
|
||||
int detach_error = bpf_xdp_detach(
|
||||
ifindex, attached_flags & XDP_FLAGS_MODES,
|
||||
&detach_options);
|
||||
|
||||
if (detach_error) {
|
||||
fprintf(stderr, "failed to detach XDP program: %s\n",
|
||||
strerror(-detach_error));
|
||||
err = 1;
|
||||
}
|
||||
}
|
||||
afxdp_dump_bpf__destroy(skel);
|
||||
close_xsk(&xsk);
|
||||
return err;
|
||||
}
|
||||
7
src/60-afxdp-dump/afxdp_dump.h
Normal file
7
src/60-afxdp-dump/afxdp_dump.h
Normal file
@@ -0,0 +1,7 @@
|
||||
/* SPDX-License-Identifier: (LGPL-2.1 OR BSD-2-Clause) */
|
||||
#ifndef __AFXDP_DUMP_H
|
||||
#define __AFXDP_DUMP_H
|
||||
|
||||
#define AFXDP_MAX_QUEUES 64
|
||||
|
||||
#endif /* __AFXDP_DUMP_H */
|
||||
118
src/60-afxdp-dump/tests/test_afxdp_dump.py
Normal file
118
src/60-afxdp-dump/tests/test_afxdp_dump.py
Normal file
@@ -0,0 +1,118 @@
|
||||
#!/usr/bin/env python3
|
||||
import os
|
||||
import pathlib
|
||||
import select
|
||||
import socket
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
|
||||
SUFFIX = os.getpid() % 10000
|
||||
RX = f"axdp{SUFFIX}r"
|
||||
TX = f"axdp{SUFFIX}t"
|
||||
NETNS = f"afxdp-tx-ns-{os.getpid()}"
|
||||
|
||||
|
||||
def run(*args, check=True):
|
||||
return subprocess.run(args, text=True, capture_output=True, check=check)
|
||||
|
||||
|
||||
def cleanup():
|
||||
run("ip", "link", "del", RX, check=False)
|
||||
run("ip", "netns", "del", NETNS, check=False)
|
||||
|
||||
|
||||
def setup():
|
||||
cleanup()
|
||||
run("ip", "netns", "add", NETNS)
|
||||
run("ip", "link", "add", RX, "type", "veth", "peer", "name", TX)
|
||||
run("ip", "link", "set", TX, "netns", NETNS)
|
||||
run("ip", "addr", "add", "10.77.0.2/24", "dev", RX)
|
||||
run("ip", "link", "set", RX, "up")
|
||||
run(
|
||||
"ip", "netns", "exec", NETNS,
|
||||
"ip", "addr", "add", "10.77.0.1/24", "dev", TX,
|
||||
)
|
||||
run("ip", "netns", "exec", NETNS, "ip", "link", "set", TX, "up")
|
||||
run("ip", "netns", "exec", NETNS, "ip", "link", "set", "lo", "up")
|
||||
|
||||
|
||||
def send_packets(count):
|
||||
code = (
|
||||
"import socket,time; "
|
||||
"s=socket.socket(socket.AF_INET,socket.SOCK_DGRAM); "
|
||||
f"[(s.sendto(b'hello-afxdp',('10.77.0.2',8080)),time.sleep(0.005)) "
|
||||
f"for _ in range({count})]; s.close()"
|
||||
)
|
||||
run("ip", "netns", "exec", NETNS, "python3", "-c", code)
|
||||
|
||||
|
||||
def verify_nonmatching_passes():
|
||||
receiver = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
||||
try:
|
||||
receiver.bind(("10.77.0.2", 8081))
|
||||
receiver.settimeout(3)
|
||||
code = (
|
||||
"import socket; "
|
||||
"s=socket.socket(socket.AF_INET,socket.SOCK_DGRAM); "
|
||||
"s.sendto(b'pass-afxdp',('10.77.0.2',8081)); s.close()"
|
||||
)
|
||||
run("ip", "netns", "exec", NETNS, "python3", "-c", code)
|
||||
payload, _ = receiver.recvfrom(64)
|
||||
assert payload == b"pass-afxdp", payload
|
||||
finally:
|
||||
receiver.close()
|
||||
|
||||
|
||||
def main():
|
||||
binary = str(pathlib.Path(sys.argv[1]).resolve())
|
||||
process = None
|
||||
try:
|
||||
setup()
|
||||
command = [
|
||||
binary,
|
||||
"--interface", RX,
|
||||
"--queue", "0",
|
||||
"--port", "8080",
|
||||
"--count", "65",
|
||||
] + sys.argv[2:]
|
||||
process = subprocess.Popen(
|
||||
command,
|
||||
text=True,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE,
|
||||
)
|
||||
readable, _, _ = select.select([process.stdout], [], [], 5)
|
||||
if not readable:
|
||||
process.kill()
|
||||
stdout, stderr = process.communicate(timeout=5)
|
||||
raise AssertionError("timed out waiting for ready line\n" + stdout + stderr)
|
||||
ready = process.stdout.readline()
|
||||
if "afxdp-dump ready" not in ready:
|
||||
stdout, stderr = process.communicate(timeout=5)
|
||||
raise AssertionError(ready + stdout + stderr)
|
||||
verify_nonmatching_passes()
|
||||
send_packets(65)
|
||||
stdout, stderr = process.communicate(timeout=20)
|
||||
output = ready + stdout + stderr
|
||||
if process.returncode:
|
||||
raise AssertionError(output)
|
||||
assert "10.77.0.1:" in output and "-> 10.77.0.2:8080" in output, output
|
||||
assert 'payload="hello-afxdp"' in output, output
|
||||
assert "packet=65 " in output and "redirected=65" in output, output
|
||||
lines = output.splitlines()
|
||||
print(ready, end="")
|
||||
print(next(line for line in lines if line.startswith("packet=1 ")))
|
||||
print(next(line for line in lines if line.startswith("packet=65 ")))
|
||||
print(next(line for line in lines if line == "redirected=65"))
|
||||
print("nonmatching-pass=verified")
|
||||
print("AF_XDP dump integration test: PASS")
|
||||
finally:
|
||||
if process and process.poll() is None:
|
||||
process.kill()
|
||||
process.wait()
|
||||
cleanup()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user