fix: align tutorial kernel requirements

This commit is contained in:
LinuxDev9002
2026-07-22 02:10:38 -07:00
parent 2219cbc64f
commit 05147a5cf3
9 changed files with 279 additions and 158 deletions

View File

@@ -1,6 +1,6 @@
level=Depth
type=Security
kernel_min=5.8
kernel_min=5.12
kernel_min_basis=required-feature
architectures=x86_64
btf=required

View File

@@ -373,7 +373,7 @@ The egress path first validates IPv4, UDP, fragmentation state, resolver address
On ingress, parsing starts with the reverse transport tuple. `parse_response_question()` adds the transaction ID, requires a live pending entry, and verifies a successful response containing the configured question. `parse_direct_a_answer()` accepts the compact `0xc00c` name pointer, class IN, type A, four-byte address, and a TTL from 1 to 86400 seconds. Once the answer passes, the pending query is consumed and the IP becomes eligible for connections.
`enforce_dns_policy()` stays narrow. TCP connects to other destination ports pass immediately. A connect to the protected port looks up `ctx->user_ip4`; a live entry returns `1`, while a missing or expired entry returns `0`, which surfaces to the application as `EPERM`. The compare-and-swap on `expired_reported` keeps the expiry notification to one event even when several threads race on the stale address.
`enforce_dns_policy()` stays narrow. TCP connects to other destination ports pass immediately. A connect to the protected port looks up `ctx->user_ip4`; a live entry returns `1`, while a missing or expired entry returns `0`, which surfaces to the application as `EPERM`. A 64-bit compare-and-swap on `expired_reported` keeps the expiry notification to one event even when several threads race on the stale address. BPF atomic compare-and-exchange arrived in Linux 5.12, which sets this tool's minimum kernel version.
## Loading the Policy and Exercising Its Trust Chain
@@ -791,70 +791,109 @@ static int expected_demo_events(void)
event_counts[DNS_EXPIRED] == 1 ? 0 : -1;
}
static int run_demo(struct ring_buffer *ring, const struct options *options,
const unsigned char *qname, unsigned int qname_length)
struct demo_context {
struct sockaddr_in server_address;
struct sockaddr_in response_client_address;
unsigned char dns_message[512];
size_t query_length;
int dns_server;
int dns_client;
int listener;
};
static int open_demo_sockets(struct demo_context *demo,
const struct options *options)
{
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;
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;
demo->server_address = (struct sockaddr_in) {
.sin_family = AF_INET,
.sin_addr.s_addr = htonl(INADDR_LOOPBACK),
.sin_port = htons(options->dns_port),
};
demo->dns_server = bind_udp(&demo->server_address);
demo->dns_client = bind_udp(&client_address);
demo->listener = create_tcp_listener(options->port);
return demo->dns_server < 0 || demo->dns_client < 0 || demo->listener < 0 ?
-1 : 0;
}
static int test_rejected_dns_answers(struct ring_buffer *ring,
const struct options *options,
const unsigned char *qname,
unsigned int qname_length,
struct demo_context *demo)
{
if (expect_blocked_connect(ring, options->port, "before-dns"))
goto cleanup;
if (send_unsolicited_dns(dns_server, dns_client, qname, qname_length))
goto cleanup;
if (poll_demo_events(ring) ||
expect_blocked_connect(ring, options->port, "unsolicited-response"))
goto cleanup;
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;
if (poll_demo_events(ring) ||
return -1;
if (send_unsolicited_dns(demo->dns_server, demo->dns_client, qname,
qname_length) ||
poll_demo_events(ring) ||
expect_blocked_connect(ring, options->port,
"unsolicited-response"))
return -1;
if (begin_dns_exchange(demo->dns_server, demo->dns_client,
&demo->server_address, qname, qname_length,
demo->dns_message, &demo->query_length,
&demo->response_client_address) ||
send_dns_answer(demo->dns_server, demo->dns_client,
&demo->response_client_address, demo->dns_message,
demo->query_length, DNS_ID + 1, 30) ||
poll_demo_events(ring) ||
expect_blocked_connect(ring, options->port, "wrong-transaction-id"))
goto cleanup;
return -1;
return 0;
}
if (send_dns_answer(dns_server, dns_client, &response_client_address,
dns_message, query_length, DNS_ID, 1))
goto cleanup;
if (poll_demo_events(ring) ||
expect_allowed_connect(ring, listener, options->port))
goto cleanup;
static int test_live_and_expired_answer(struct ring_buffer *ring,
const struct options *options,
struct demo_context *demo)
{
struct timespec wait_time = { .tv_sec = 1, .tv_nsec = 300000000 };
if (send_dns_answer(demo->dns_server, demo->dns_client,
&demo->response_client_address, demo->dns_message,
demo->query_length, DNS_ID, 1) ||
poll_demo_events(ring) ||
expect_allowed_connect(ring, demo->listener, options->port))
return -1;
nanosleep(&wait_time, NULL);
if (poll_demo_events(ring) ||
expect_blocked_connect(ring, options->port, "expired-answer"))
goto cleanup;
return -1;
return expected_demo_events();
}
if (expected_demo_events())
goto cleanup;
err = 0;
static void close_demo_sockets(struct demo_context *demo)
{
if (demo->listener >= 0)
close(demo->listener);
if (demo->dns_client >= 0)
close(demo->dns_client);
if (demo->dns_server >= 0)
close(demo->dns_server);
}
cleanup:
if (listener >= 0) close(listener);
if (dns_client >= 0) close(dns_client);
if (dns_server >= 0) close(dns_server);
static int run_demo(struct ring_buffer *ring, const struct options *options,
const unsigned char *qname, unsigned int qname_length)
{
struct demo_context demo = {
.dns_server = -1,
.dns_client = -1,
.listener = -1,
};
int err;
err = open_demo_sockets(&demo, options);
if (!err)
err = test_rejected_dns_answers(ring, options, qname,
qname_length, &demo);
if (!err)
err = test_live_and_expired_answer(ring, options, &demo);
close_demo_sockets(&demo);
return err;
}
@@ -1035,7 +1074,7 @@ The three early `denied` events show that neither receiving DNS-shaped traffic n
| Requirement | Details |
|---|---|
| Kernel | Linux 5.8 or newer; the newest dependency is BPF ring buffer support |
| Kernel | Linux 5.12 or newer; the newest dependency is BPF atomic compare-and-exchange |
| Kernel config | `CONFIG_BPF`, `CONFIG_BPF_SYSCALL`, `CONFIG_BPF_JIT`, `CONFIG_CGROUP_BPF`, `CONFIG_DEBUG_INFO_BTF`, `CONFIG_INET` |
| cgroup | cgroup v2, with the target workload placed below the attached directory |
| Privileges | Root, or an equivalent set of BPF and network-administration capabilities |
@@ -1055,5 +1094,6 @@ This example turns an observed DNS result into a time-bounded connect policy. Th
- [BPF ring buffer](https://docs.kernel.org/bpf/ringbuf.html)
- [BPF LRU hash maps](https://docs.kernel.org/bpf/map_hash.html)
- [BPF atomic compare-and-exchange commit](https://github.com/torvalds/linux/commit/5ffa25502b5ab3d639829a2d1e316cff7f59a41e)
- [Control Group v2](https://docs.kernel.org/admin-guide/cgroup-v2.html)
- [RFC 1035: Domain Names — Implementation and Specification](https://www.rfc-editor.org/rfc/rfc1035.html)

View File

@@ -373,7 +373,7 @@ egress 路径先验证 IPv4、UDP、分片状态、解析器地址和端口,
ingress 路径从反向 transport tuple 开始解析。`parse_response_question()` 补上 transaction ID,要求 pending entry 仍然有效,并验证这是包含指定 question 的成功响应。`parse_direct_a_answer()` 接受常见的 `0xc00c` 压缩 name pointer、IN class、A type、4 字节地址,以及 1 到 86400 秒的 TTL。应答通过以后,pending query 会被消费,解析出的 IP 才获得连接资格。
`enforce_dns_policy()` 的范围很窄。发往其他目的端口的 TCP 连接直接通过;发往受保护端口的连接使用 `ctx->user_ip4` 查表,有效条目返回 `1`,缺失或过期条目返回 `0`,应用会收到 `EPERM`。`expired_reported` 上的 compare-and-swap 让多个线程同时访问旧地址时只产生一次 expired 通知。
`enforce_dns_policy()` 的范围很窄。发往其他目的端口的 TCP 连接直接通过;发往受保护端口的连接使用 `ctx->user_ip4` 查表,有效条目返回 `1`,缺失或过期条目返回 `0`,应用会收到 `EPERM`。`expired_reported` 上的 64 位 compare-and-swap 让多个线程同时访问旧地址时只产生一次 expired 通知。BPF atomic compare-and-exchange 在 Linux 5.12 引入,这也确定了工具的最低内核版本。
## 加载策略并验证完整信任链
@@ -791,70 +791,109 @@ static int expected_demo_events(void)
event_counts[DNS_EXPIRED] == 1 ? 0 : -1;
}
static int run_demo(struct ring_buffer *ring, const struct options *options,
const unsigned char *qname, unsigned int qname_length)
struct demo_context {
struct sockaddr_in server_address;
struct sockaddr_in response_client_address;
unsigned char dns_message[512];
size_t query_length;
int dns_server;
int dns_client;
int listener;
};
static int open_demo_sockets(struct demo_context *demo,
const struct options *options)
{
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;
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;
demo->server_address = (struct sockaddr_in) {
.sin_family = AF_INET,
.sin_addr.s_addr = htonl(INADDR_LOOPBACK),
.sin_port = htons(options->dns_port),
};
demo->dns_server = bind_udp(&demo->server_address);
demo->dns_client = bind_udp(&client_address);
demo->listener = create_tcp_listener(options->port);
return demo->dns_server < 0 || demo->dns_client < 0 || demo->listener < 0 ?
-1 : 0;
}
static int test_rejected_dns_answers(struct ring_buffer *ring,
const struct options *options,
const unsigned char *qname,
unsigned int qname_length,
struct demo_context *demo)
{
if (expect_blocked_connect(ring, options->port, "before-dns"))
goto cleanup;
if (send_unsolicited_dns(dns_server, dns_client, qname, qname_length))
goto cleanup;
if (poll_demo_events(ring) ||
expect_blocked_connect(ring, options->port, "unsolicited-response"))
goto cleanup;
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;
if (poll_demo_events(ring) ||
return -1;
if (send_unsolicited_dns(demo->dns_server, demo->dns_client, qname,
qname_length) ||
poll_demo_events(ring) ||
expect_blocked_connect(ring, options->port,
"unsolicited-response"))
return -1;
if (begin_dns_exchange(demo->dns_server, demo->dns_client,
&demo->server_address, qname, qname_length,
demo->dns_message, &demo->query_length,
&demo->response_client_address) ||
send_dns_answer(demo->dns_server, demo->dns_client,
&demo->response_client_address, demo->dns_message,
demo->query_length, DNS_ID + 1, 30) ||
poll_demo_events(ring) ||
expect_blocked_connect(ring, options->port, "wrong-transaction-id"))
goto cleanup;
return -1;
return 0;
}
if (send_dns_answer(dns_server, dns_client, &response_client_address,
dns_message, query_length, DNS_ID, 1))
goto cleanup;
if (poll_demo_events(ring) ||
expect_allowed_connect(ring, listener, options->port))
goto cleanup;
static int test_live_and_expired_answer(struct ring_buffer *ring,
const struct options *options,
struct demo_context *demo)
{
struct timespec wait_time = { .tv_sec = 1, .tv_nsec = 300000000 };
if (send_dns_answer(demo->dns_server, demo->dns_client,
&demo->response_client_address, demo->dns_message,
demo->query_length, DNS_ID, 1) ||
poll_demo_events(ring) ||
expect_allowed_connect(ring, demo->listener, options->port))
return -1;
nanosleep(&wait_time, NULL);
if (poll_demo_events(ring) ||
expect_blocked_connect(ring, options->port, "expired-answer"))
goto cleanup;
return -1;
return expected_demo_events();
}
if (expected_demo_events())
goto cleanup;
err = 0;
static void close_demo_sockets(struct demo_context *demo)
{
if (demo->listener >= 0)
close(demo->listener);
if (demo->dns_client >= 0)
close(demo->dns_client);
if (demo->dns_server >= 0)
close(demo->dns_server);
}
cleanup:
if (listener >= 0) close(listener);
if (dns_client >= 0) close(dns_client);
if (dns_server >= 0) close(dns_server);
static int run_demo(struct ring_buffer *ring, const struct options *options,
const unsigned char *qname, unsigned int qname_length)
{
struct demo_context demo = {
.dns_server = -1,
.dns_client = -1,
.listener = -1,
};
int err;
err = open_demo_sockets(&demo, options);
if (!err)
err = test_rejected_dns_answers(ring, options, qname,
qname_length, &demo);
if (!err)
err = test_live_and_expired_answer(ring, options, &demo);
close_demo_sockets(&demo);
return err;
}
@@ -1035,7 +1074,7 @@ demo step=expired-answer result=blocked
| 要求 | 说明 |
|---|---|
| 内核 | Linux 5.8 或更高版本,最新依赖来自 BPF ring buffer |
| 内核 | Linux 5.12 或更高版本,最新依赖来自 BPF atomic compare-and-exchange |
| 内核配置 | `CONFIG_BPF`、`CONFIG_BPF_SYSCALL`、`CONFIG_BPF_JIT`、`CONFIG_CGROUP_BPF`、`CONFIG_DEBUG_INFO_BTF`、`CONFIG_INET` |
| cgroup | cgroup v2,目标工作负载位于挂载目录之下 |
| 权限 | root,或者等价的 BPF 与网络管理 capability |
@@ -1055,5 +1094,6 @@ demo step=expired-answer result=blocked
- [BPF ring buffer](https://docs.kernel.org/bpf/ringbuf.html)
- [BPF LRU hash map](https://docs.kernel.org/bpf/map_hash.html)
- [BPF atomic compare-and-exchange 引入 commit](https://github.com/torvalds/linux/commit/5ffa25502b5ab3d639829a2d1e316cff7f59a41e)
- [Control Group v2](https://docs.kernel.org/admin-guide/cgroup-v2.html)
- [RFC 1035:Domain Names — Implementation and Specification](https://www.rfc-editor.org/rfc/rfc1035.html)

View File

@@ -409,70 +409,109 @@ static int expected_demo_events(void)
event_counts[DNS_EXPIRED] == 1 ? 0 : -1;
}
static int run_demo(struct ring_buffer *ring, const struct options *options,
const unsigned char *qname, unsigned int qname_length)
struct demo_context {
struct sockaddr_in server_address;
struct sockaddr_in response_client_address;
unsigned char dns_message[512];
size_t query_length;
int dns_server;
int dns_client;
int listener;
};
static int open_demo_sockets(struct demo_context *demo,
const struct options *options)
{
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;
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;
demo->server_address = (struct sockaddr_in) {
.sin_family = AF_INET,
.sin_addr.s_addr = htonl(INADDR_LOOPBACK),
.sin_port = htons(options->dns_port),
};
demo->dns_server = bind_udp(&demo->server_address);
demo->dns_client = bind_udp(&client_address);
demo->listener = create_tcp_listener(options->port);
return demo->dns_server < 0 || demo->dns_client < 0 || demo->listener < 0 ?
-1 : 0;
}
static int test_rejected_dns_answers(struct ring_buffer *ring,
const struct options *options,
const unsigned char *qname,
unsigned int qname_length,
struct demo_context *demo)
{
if (expect_blocked_connect(ring, options->port, "before-dns"))
goto cleanup;
if (send_unsolicited_dns(dns_server, dns_client, qname, qname_length))
goto cleanup;
if (poll_demo_events(ring) ||
expect_blocked_connect(ring, options->port, "unsolicited-response"))
goto cleanup;
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;
if (poll_demo_events(ring) ||
return -1;
if (send_unsolicited_dns(demo->dns_server, demo->dns_client, qname,
qname_length) ||
poll_demo_events(ring) ||
expect_blocked_connect(ring, options->port,
"unsolicited-response"))
return -1;
if (begin_dns_exchange(demo->dns_server, demo->dns_client,
&demo->server_address, qname, qname_length,
demo->dns_message, &demo->query_length,
&demo->response_client_address) ||
send_dns_answer(demo->dns_server, demo->dns_client,
&demo->response_client_address, demo->dns_message,
demo->query_length, DNS_ID + 1, 30) ||
poll_demo_events(ring) ||
expect_blocked_connect(ring, options->port, "wrong-transaction-id"))
goto cleanup;
return -1;
return 0;
}
if (send_dns_answer(dns_server, dns_client, &response_client_address,
dns_message, query_length, DNS_ID, 1))
goto cleanup;
if (poll_demo_events(ring) ||
expect_allowed_connect(ring, listener, options->port))
goto cleanup;
static int test_live_and_expired_answer(struct ring_buffer *ring,
const struct options *options,
struct demo_context *demo)
{
struct timespec wait_time = { .tv_sec = 1, .tv_nsec = 300000000 };
if (send_dns_answer(demo->dns_server, demo->dns_client,
&demo->response_client_address, demo->dns_message,
demo->query_length, DNS_ID, 1) ||
poll_demo_events(ring) ||
expect_allowed_connect(ring, demo->listener, options->port))
return -1;
nanosleep(&wait_time, NULL);
if (poll_demo_events(ring) ||
expect_blocked_connect(ring, options->port, "expired-answer"))
goto cleanup;
return -1;
return expected_demo_events();
}
if (expected_demo_events())
goto cleanup;
err = 0;
static void close_demo_sockets(struct demo_context *demo)
{
if (demo->listener >= 0)
close(demo->listener);
if (demo->dns_client >= 0)
close(demo->dns_client);
if (demo->dns_server >= 0)
close(demo->dns_server);
}
cleanup:
if (listener >= 0) close(listener);
if (dns_client >= 0) close(dns_client);
if (dns_server >= 0) close(dns_server);
static int run_demo(struct ring_buffer *ring, const struct options *options,
const unsigned char *qname, unsigned int qname_length)
{
struct demo_context demo = {
.dns_server = -1,
.dns_client = -1,
.listener = -1,
};
int err;
err = open_demo_sockets(&demo, options);
if (!err)
err = test_rejected_dns_answers(ring, options, qname,
qname_length, &demo);
if (!err)
err = test_live_and_expired_answer(ring, options, &demo);
close_demo_sockets(&demo);
return err;
}

View File

@@ -1,6 +1,6 @@
level=Depth
type=Networking
kernel_min=5.3
kernel_min=5.7
kernel_min_basis=required-feature
architectures=x86_64
btf=required

View File

@@ -10,7 +10,7 @@ This tutorial builds that path from the AF_XDP ABI rather than hiding it behind
eBPF runs verifier-checked programs at kernel hooks, and XDP places one of those hooks at the earliest receive point in the Linux network path. AF_XDP, added in Linux 4.18, connects an XDP redirect to a socket backed by user-registered memory. An XSKMAP supplies the missing association between an RX queue number and the AF_XDP socket that serves it.
The roles are deliberately separate. The XDP program parses just enough of the packet to decide whether it belongs to the tool. The AF_XDP socket owns the shared-memory rings and carries selected bytes to user space. This example also checks XSKMAP with `bpf_map_lookup_elem()` before redirecting, support added in Linux 5.3, so Linux 5.3 is its minimum kernel version.
The roles are deliberately separate. The XDP program parses just enough of the packet to decide whether it belongs to the tool. The AF_XDP socket owns the shared-memory rings and carries selected bytes to user space. This example checks XSKMAP with `bpf_map_lookup_elem()` before redirecting, support added in Linux 5.3. Its compare-and-detach cleanup uses the expected-program FD added in Linux 5.7, which sets the complete tool's minimum kernel version.
Follow one packet and one frame. User space allocates 64 frames of 4096 bytes in UMEM and posts their addresses to the fill ring. A UDP packet for the configured port reaches the XDP hook. The program validates Ethernet, IPv4, and UDP lengths, finds the socket registered for `ctx->rx_queue_index`, and returns `XDP_REDIRECT`. In copy mode the kernel copies the packet into one posted frame and publishes an `xdp_desc` on the RX ring. User space reads the descriptor, prints the packet, advances the consumer index, and puts the same address back on the fill ring.
@@ -647,7 +647,7 @@ AF_XDP dump integration test: PASS
| Requirement | Details |
|---|---|
| Kernel | Linux 5.3 or newer; AF_XDP arrived in 4.18 and this example also uses XSKMAP lookup from XDP |
| Kernel | Linux 5.7 or newer; AF_XDP arrived in 4.18, XSKMAP lookup from XDP in 5.3, and safe expected-FD detach in 5.7 |
| Kernel config | `CONFIG_BPF`, `CONFIG_BPF_SYSCALL`, `CONFIG_BPF_JIT`, `CONFIG_XDP_SOCKETS`, `CONFIG_DEBUG_INFO_BTF` |
| Privileges | Root, or equivalent BPF and network-administration capabilities |
| Interface | An interface with the selected RX queue; native XDP is optional because generic XDP is available |
@@ -668,3 +668,4 @@ This example exposes the complete AF_XDP receive contract. XDP selects one UDP f
- [Linux AF_XDP documentation](https://docs.kernel.org/networking/af_xdp.html)
- [AF_XDP introduction commit](https://github.com/torvalds/linux/commit/c0c77d8fb787cfe0c3fca689c2a30d1dad4eaba7)
- [XSKMAP lookup from XDP commit](https://github.com/torvalds/linux/commit/fada7fdc83c0)
- [Expected-program FD for XDP replacement and detach](https://github.com/torvalds/linux/commit/92234c8f15c8d96ad7e52afdc5994cba6be68eb9)

View File

@@ -10,7 +10,7 @@
eBPF 可以让经过验证器检查的程序运行在内核 hook 上,XDP 则把其中一个 hook 放在 Linux 网络接收路径的最前端。Linux 4.18 引入的 AF_XDP 把 XDP redirect 连接到由用户态注册内存支撑的 socket,XSKMAP 再建立 RX queue number 与对应 AF_XDP socket 之间的关系。
两边的职责很明确。XDP 程序只解析足够的信息,判断报文是否属于这个工具;AF_XDP socket 管理共享内存 ring,并把选中的字节交给用户态。本例还会在 redirect 之前用 `bpf_map_lookup_elem()` 检查 XSKMAP,这项能力从 Linux 5.3 开始可用,因此最低内核版本是 5.3。
两边的职责很明确。XDP 程序只解析足够的信息,判断报文是否属于这个工具;AF_XDP socket 管理共享内存 ring,并把选中的字节交给用户态。本例还会在 redirect 之前用 `bpf_map_lookup_elem()` 检查 XSKMAP,这项能力从 Linux 5.3 开始可用。清理阶段的 compare-and-detach 使用 Linux 5.7 加入的 expected-program FD,因此完整工具的最低内核版本是 5.7。
跟着一个报文和一个 frame 走一遍。用户态在 UMEM 中分配 64 个 4096 字节 frame,把它们的地址发布到 fill ring。发往指定端口的 UDP 报文到达 XDP hook 后,程序检查 Ethernet、IPv4 与 UDP 长度,通过 `ctx->rx_queue_index` 找到注册 socket,再返回 `XDP_REDIRECT`。copy mode 下,内核把报文复制到一个已发布的 frame,并把 `xdp_desc` 放入 RX ring。用户态读取 descriptor、打印报文、推进 consumer index,最后把同一个地址放回 fill ring。
@@ -647,7 +647,7 @@ AF_XDP dump integration test: PASS
| 要求 | 说明 |
|---|---|
| 内核 | Linux 5.3 或更高版本,AF_XDP 在 4.18 引入,本例还使用 XDP 对 XSKMAP 的 lookup |
| 内核 | Linux 5.7 或更高版本,AF_XDP 在 4.18 引入,XDP 对 XSKMAP 的 lookup 在 5.3 引入,安全的 expected-FD detach 在 5.7 引入 |
| 内核配置 | `CONFIG_BPF`、`CONFIG_BPF_SYSCALL`、`CONFIG_BPF_JIT`、`CONFIG_XDP_SOCKETS`、`CONFIG_DEBUG_INFO_BTF` |
| 权限 | root,或者等价的 BPF 与网络管理 capability |
| 网络接口 | 接口包含所选 RX queue,native XDP 可选,工具也支持 generic XDP |
@@ -668,3 +668,4 @@ AF_XDP dump integration test: PASS
- [Linux AF_XDP 文档](https://docs.kernel.org/networking/af_xdp.html)
- [AF_XDP 引入 commit](https://github.com/torvalds/linux/commit/c0c77d8fb787cfe0c3fca689c2a30d1dad4eaba7)
- [XDP 支持 XSKMAP lookup 的 commit](https://github.com/torvalds/linux/commit/fada7fdc83c0)
- [XDP replace 与 detach 的 expected-program FD](https://github.com/torvalds/linux/commit/92234c8f15c8d96ad7e52afdc5994cba6be68eb9)

View File

@@ -61,10 +61,10 @@ This table is generated from each tutorial's `.config` metadata. `Minimum kernel
| [eBPF Tutorial: Tracing Slow vfs_read Calls with fsession](52-fsession-latency/README.md) | 7.0 | Tutorial docs | x86_64 | Required | CONFIG_BPF=y, CONFIG_BPF_SYSCALL=y, CONFIG_BPF_JIT=y, CONFIG_BPF_EVENTS=y, CONFIG_DEBUG_INFO_BTF=y, CONFIG_DYNAMIC_FTRACE_WITH_DIRECT_CALLS=y | None | Required | CI build |
| [eBPF Tutorial: Building an Egress Pacer with BPF Qdisc](53-egress-pacer/README.md) | 6.16 | Tutorial docs | x86_64 | Required | CONFIG_BPF=y, CONFIG_BPF_SYSCALL=y, CONFIG_BPF_JIT=y, CONFIG_DEBUG_INFO_BTF=y, CONFIG_NET_SCHED=y, CONFIG_NET_SCH_BPF=y | Network interface | Required | CI build |
| [eBPF Tutorial: Inspecting the Executable Image After exec](54-exec-image-inspector/README.md) | 6.19 | Required feature | x86_64 | Required | CONFIG_BPF=y, CONFIG_BPF_SYSCALL=y, CONFIG_BPF_JIT=y, CONFIG_BPF_LSM=y, CONFIG_SECURITY=y, CONFIG_DEBUG_INFO_BTF=y | None | Required | CI build |
| [eBPF Tutorial by Example: Build a DNS-Derived IP Allowlist with cgroup BPF](55-dns-egress/README.md) | 5.8 | Required feature | x86_64 | Required | CONFIG_BPF=y, CONFIG_BPF_SYSCALL=y, CONFIG_BPF_JIT=y, CONFIG_CGROUP_BPF=y, CONFIG_DEBUG_INFO_BTF=y, CONFIG_INET=y | None | Required | CI build |
| [eBPF Tutorial by Example: Build a DNS-Derived IP Allowlist with cgroup BPF](55-dns-egress/README.md) | 5.12 | Required feature | x86_64 | Required | CONFIG_BPF=y, CONFIG_BPF_SYSCALL=y, CONFIG_BPF_JIT=y, CONFIG_CGROUP_BPF=y, CONFIG_DEBUG_INFO_BTF=y, CONFIG_INET=y | None | Required | CI build |
| [eBPF Tutorial by Example: Index TC Flows in Two Refcounted rbtrees](56-tc-flow-index/README.md) | 6.16 | Required feature | x86_64 | Required | CONFIG_BPF=y, CONFIG_BPF_SYSCALL=y, CONFIG_BPF_JIT=y, CONFIG_DEBUG_INFO_BTF=y, CONFIG_NET_SCHED=y, CONFIG_NET_CLS_BPF=y | Network interface | Required | CI build |
| [eBPF Tutorial by Example: Profile memcg Reclaim Before an OOM Kill](57-oom-watch/README.md) | 7.1 | Required feature | x86_64 | Required | CONFIG_BPF=y, CONFIG_BPF_SYSCALL=y, CONFIG_BPF_JIT=y, CONFIG_BPF_EVENTS=y, CONFIG_DEBUG_INFO_BTF=y, CONFIG_MEMCG=y | None | Required | CI build |
| [eBPF Tutorial by Example: Receive UDP Packets with AF_XDP](60-afxdp-dump/README.md) | 5.3 | Required feature | x86_64 | Required | CONFIG_BPF=y, CONFIG_BPF_SYSCALL=y, CONFIG_BPF_JIT=y, CONFIG_DEBUG_INFO_BTF=y, CONFIG_XDP_SOCKETS=y | Network interface | Required | CI build |
| [eBPF Tutorial by Example: Receive UDP Packets with AF_XDP](60-afxdp-dump/README.md) | 5.7 | Required feature | x86_64 | Required | CONFIG_BPF=y, CONFIG_BPF_SYSCALL=y, CONFIG_BPF_JIT=y, CONFIG_DEBUG_INFO_BTF=y, CONFIG_XDP_SOCKETS=y | Network interface | Required | CI build |
| [eBPF Tutorial: cgroup-based Policy Control](cgroup/README.md) | 5.8 | Required feature | x86_64, arm64 | Required | CONFIG_BPF=y, CONFIG_BPF_SYSCALL=y, CONFIG_CGROUPS=y, CONFIG_CGROUP_BPF=y, CONFIG_CGROUP_DEVICE=y, CONFIG_DEBUG_INFO_BTF=y | None | Required | CI build |
| [eBPF Tutorial by Example: BPF Arena for Zero-Copy Shared Memory](features/bpf_arena/README.md) | 6.9 | Tutorial docs | x86_64, arm64 | Required | CONFIG_BPF=y, CONFIG_BPF_SYSCALL=y, CONFIG_BPF_JIT=y, CONFIG_DEBUG_INFO_BTF=y | None | Required | CI build |
| [eBPF Tutorial: BPF Iterators for Kernel Data Export](features/bpf_iters/README.md) | 5.8 | Tutorial docs | x86_64, arm64 | Required | CONFIG_BPF=y, CONFIG_BPF_SYSCALL=y, CONFIG_DEBUG_INFO_BTF=y | None | Required | CI build |

View File

@@ -61,10 +61,10 @@
| [eBPF 教程:使用 fsession 追踪慢速 vfs_read 调用](52-fsession-latency/README.zh.md) | 7.0 | 教程文档 | x86_64 | 需要 | CONFIG_BPF=y, CONFIG_BPF_SYSCALL=y, CONFIG_BPF_JIT=y, CONFIG_BPF_EVENTS=y, CONFIG_DEBUG_INFO_BTF=y, CONFIG_DYNAMIC_FTRACE_WITH_DIRECT_CALLS=y | 无 | 需要 | CI 构建测试 |
| [eBPF 教程:用 BPF Qdisc 实现出口限速](53-egress-pacer/README.zh.md) | 6.16 | 教程文档 | x86_64 | 需要 | CONFIG_BPF=y, CONFIG_BPF_SYSCALL=y, CONFIG_BPF_JIT=y, CONFIG_DEBUG_INFO_BTF=y, CONFIG_NET_SCHED=y, CONFIG_NET_SCH_BPF=y | 网络接口 | 需要 | CI 构建测试 |
| [eBPF 教程:检查 exec 后实际安装的可执行镜像](54-exec-image-inspector/README.zh.md) | 6.19 | 必需特性 | x86_64 | 需要 | CONFIG_BPF=y, CONFIG_BPF_SYSCALL=y, CONFIG_BPF_JIT=y, CONFIG_BPF_LSM=y, CONFIG_SECURITY=y, CONFIG_DEBUG_INFO_BTF=y | 无 | 需要 | CI 构建测试 |
| [eBPF 实战教程:用 cgroup BPF 构建 DNS 派生的 IP 允许列表](55-dns-egress/README.zh.md) | 5.8 | 必需特性 | x86_64 | 需要 | CONFIG_BPF=y, CONFIG_BPF_SYSCALL=y, CONFIG_BPF_JIT=y, CONFIG_CGROUP_BPF=y, CONFIG_DEBUG_INFO_BTF=y, CONFIG_INET=y | 无 | 需要 | CI 构建测试 |
| [eBPF 实战教程:用 cgroup BPF 构建 DNS 派生的 IP 允许列表](55-dns-egress/README.zh.md) | 5.12 | 必需特性 | x86_64 | 需要 | CONFIG_BPF=y, CONFIG_BPF_SYSCALL=y, CONFIG_BPF_JIT=y, CONFIG_CGROUP_BPF=y, CONFIG_DEBUG_INFO_BTF=y, CONFIG_INET=y | 无 | 需要 | CI 构建测试 |
| [eBPF 实战教程:用两棵 refcounted rbtree 索引 TC 流量](56-tc-flow-index/README.zh.md) | 6.16 | 必需特性 | x86_64 | 需要 | CONFIG_BPF=y, CONFIG_BPF_SYSCALL=y, CONFIG_BPF_JIT=y, CONFIG_DEBUG_INFO_BTF=y, CONFIG_NET_SCHED=y, CONFIG_NET_CLS_BPF=y | 网络接口 | 需要 | CI 构建测试 |
| [eBPF 实战教程:在 OOM kill 之前分析 memcg reclaim](57-oom-watch/README.zh.md) | 7.1 | 必需特性 | x86_64 | 需要 | CONFIG_BPF=y, CONFIG_BPF_SYSCALL=y, CONFIG_BPF_JIT=y, CONFIG_BPF_EVENTS=y, CONFIG_DEBUG_INFO_BTF=y, CONFIG_MEMCG=y | 无 | 需要 | CI 构建测试 |
| [eBPF 实战教程:用 AF_XDP 在用户态接收 UDP 报文](60-afxdp-dump/README.zh.md) | 5.3 | 必需特性 | x86_64 | 需要 | CONFIG_BPF=y, CONFIG_BPF_SYSCALL=y, CONFIG_BPF_JIT=y, CONFIG_DEBUG_INFO_BTF=y, CONFIG_XDP_SOCKETS=y | 网络接口 | 需要 | CI 构建测试 |
| [eBPF 实战教程:用 AF_XDP 在用户态接收 UDP 报文](60-afxdp-dump/README.zh.md) | 5.7 | 必需特性 | x86_64 | 需要 | CONFIG_BPF=y, CONFIG_BPF_SYSCALL=y, CONFIG_BPF_JIT=y, CONFIG_DEBUG_INFO_BTF=y, CONFIG_XDP_SOCKETS=y | 网络接口 | 需要 | CI 构建测试 |
| [eBPF 实例教程:基于 cgroup 的策略控制](cgroup/README.zh.md) | 5.8 | 必需特性 | x86_64, arm64 | 需要 | CONFIG_BPF=y, CONFIG_BPF_SYSCALL=y, CONFIG_CGROUPS=y, CONFIG_CGROUP_BPF=y, CONFIG_CGROUP_DEVICE=y, CONFIG_DEBUG_INFO_BTF=y | 无 | 需要 | CI 构建测试 |
| [eBPF 实例教程:BPF Arena 零拷贝共享内存](features/bpf_arena/README.zh.md) | 6.9 | 教程文档 | x86_64, arm64 | 需要 | CONFIG_BPF=y, CONFIG_BPF_SYSCALL=y, CONFIG_BPF_JIT=y, CONFIG_DEBUG_INFO_BTF=y | 无 | 需要 | CI 构建测试 |
| [eBPF 教程:BPF 迭代器用于内核数据导出](features/bpf_iters/README.zh.md) | 5.8 | 教程文档 | x86_64, arm64 | 需要 | CONFIG_BPF=y, CONFIG_BPF_SYSCALL=y, CONFIG_DEBUG_INFO_BTF=y | 无 | 需要 | CI 构建测试 |