This commit is contained in:
yunwei37
2024-09-26 01:17:19 +00:00
parent 9449556590
commit 5371b7c6a9
25 changed files with 245906 additions and 231346 deletions

View File

@@ -217,12 +217,11 @@
<h3 id="了解如何开发-ebpf-程序10-15h"><a class="header" href="#了解如何开发-ebpf-程序10-15h">了解如何开发 eBPF 程序10-15h</a></h3>
<p>了解并尝试一下 eBPF 开发框架:</p>
<ul>
<li>bpftrace 教程对于最简单的应用来说bpftrace 可能是最方便的https://eunomia.dev/zh/tutorials/bpftrace-tutorial/ 试试1h</li>
<li>bpftrace 教程对于最简单的应用来说bpftrace 可能是最方便的:<a href="https://eunomia.dev/zh/tutorials/bpftrace-tutorial/">https://eunomia.dev/zh/tutorials/bpftrace-tutorial/</a> 试试1h</li>
<li>BCC 开发各类小工具的例子:<a href="https://github.com/iovisor/bcc/blob/master/docs/tutorial_bcc_python_developer.md">https://github.com/iovisor/bcc/blob/master/docs/tutorial_bcc_python_developer.md</a> 跑一遍3-4h</li>
<li>libbpf 的一些例子:<a href="https://github.com/libbpf/libbpf-bootstrap">https://github.com/libbpf/libbpf-bootstrap</a> 选感兴趣的运行一下并阅读一下源代码2h</li>
<li>基于 libbpf 和 eunomia-bpf 的教程:<a href="https://github.com/eunomia-bpf/bpf-developer-tutorial">https://github.com/eunomia-bpf/bpf-developer-tutorial</a> (阅读 1-10 的部分3-4h</li>
<li>基于 C 语言 libbpf, Go 语言或者 Rust 语言和 eunomia-bpf 的教程:<a href="https://github.com/eunomia-bpf/bpf-developer-tutorial">https://github.com/eunomia-bpf/bpf-developer-tutorial</a> (阅读 1-20 的部分3-8h</li>
</ul>
<p>其他开发框架Go 语言或者 Rust 语言请自行搜索并且尝试0-2h</p>
<p>有任何问题或者想了解的东西,不管是不是和本项目相关,都可以在本项目的 discussions 里面开始讨论。</p>
<p>回答一些问题并且进行一些尝试2-5h</p>
<ol>
@@ -283,15 +282,13 @@ eBPF 程序每次执行时候都需要进行编译,编译则需要用户配置
字节码装载到内核中。同样的,<code>libbpf-bootstrap</code> 也有非常完备的入门教程,用户可以在<a href="https://nakryiko.com/posts/libbpf-bootstrap/">该处</a>
得到详细的入门操作介绍。</p>
<h3 id="eunomia-bpf"><a class="header" href="#eunomia-bpf">eunomia-bpf</a></h3>
<p>开发、构建和分发 eBPF 一直以来都是一个高门槛的工作,使用 BCC、bpftrace 等工具开发效率高、可移植性好,但是分发部署时需要安装 LLVM、Clang 等编译环境,每次运行的时候执行本地或远程编译过程,资源消耗较大;使用原生的 CO-RE libbpf 时又需要编写不少用户态加载代码来帮助 eBPF 程序正确加载和从内核中获取上报的信息,同时对于 eBPF 程序的分发、管理也没有很好地解决方案。</p>
<p><a href="https://github.com/eunomia-bpf/eunomia-bpf">eunomia-bpf</a> 是一个开源的 eBPF 动态加载运行时和开发工具链,是为了简化 eBPF 程序的开发、构建、分发、运行而设计的,基于 libbpf 的 CO-RE 轻量级开发框架。</p>
<p>使用 eunomia-bpf ,可以:</p>
<ul>
<li>在编写 eBPF 程序或工具时只编写内核态代码,自动获取内核态导出信息,并作为模块动态加载;</li>
<li>使用 WASM 进行用户态交互程序的开发,在 WASM 虚拟机内部控制整个 eBPF 程序的加载和执行,以及处理相关数据;</li>
<li>eunomia-bpf 可以将预编译的 eBPF 程序打包为通用的 JSON 或 WASM 模块,跨架构和内核版本进行分发,无需重新编译即可动态加载运行。</li>
</ul>
<p>eunomia-bpf 由一个编译工具链和一个运行时库组成, 对比传统的 BCC、原生 libbpf 等框架,大幅简化了 eBPF 程序的开发流程,在大多数时候只需编写内核态代码,即可轻松构建、打包、发布完整的 eBPF 应用,同时内核态 eBPF 代码保证和主流的 libbpflibbpfgolibbpf-rs 等开发框架的 100% 兼容性。需要编写用户态代码的时候,也可以借助 Webassembly 实现通过多种语言进行用户态开发。和 bpftrace 等脚本工具相比, eunomia-bpf 保留了类似的便捷性, 同时不仅局限于 trace 方面, 可以用于更多的场景, 如网络、安全等等</p>
<p>eunomia-bpf 由一个编译工具链和一个运行时库组成, 对比传统的 BCC、原生 libbpf 等框架,简化了 eBPF 程序的开发流程,在大多数时候只需编写内核态代码,即可轻松构建、打包、发布完整的 eBPF 应用。</p>
<blockquote>
<ul>
<li>eunomia-bpf 项目 Github 地址: <a href="https://github.com/eunomia-bpf/eunomia-bpf">https://github.com/eunomia-bpf/eunomia-bpf</a></li>

View File

@@ -267,7 +267,7 @@ Packing ebpf object and config into package.json...
</code></pre>
<p>然后使用 ecli 运行编译后的程序:</p>
<pre><code class="language-console">$ sudo ./ecli run package.json
Runing eBPF program...
Running eBPF program...
</code></pre>
<p>运行这段程序后,可以通过查看 /sys/kernel/debug/tracing/trace_pipe 文件来查看 eBPF 程序的输出:</p>
<pre><code class="language-console">$ sudo cat /sys/kernel/debug/tracing/trace_pipe | grep "BPF triggered sys_enter_write"

View File

@@ -173,19 +173,81 @@
<div id="content" class="content">
<main>
<h1 id="ebpf-入门实践教程二十一使用-xdp-实现可编程包处理"><a class="header" href="#ebpf-入门实践教程二十一使用-xdp-实现可编程包处理">eBPF 入门实践教程二十一:使用 xdp 实现可编程包处理</a></h1>
<h2 id="背景"><a class="header" href="#背景">背景</a></h2>
<p>xdpeXpress Data Path是 Linux 内核中新兴的一种绕过内核的、可编程的包处理方案。相较于 cBPFxdp 的挂载点非常底层,位于网络设备驱动的软中断处理过程,甚至早于 skb_buff 结构的分配。因此,在 xdp 上挂载 eBPF 程序适用于很多简单但次数极多的包处理操作(如防御 Dos 攻击可以达到很高的性能24Mpps/core</p>
<h2 id="xdp-概述"><a class="header" href="#xdp-概述">XDP 概述</a></h2>
<p>xdp 不是第一个支持可编程包处理的系统,在此之前,以 DPDKData Plane Development Kit为代表的内核旁路方案甚至能够取得更高的性能其思路为完全绕过内核由用户态的网络应用接管网络设备从而避免了用户态和内核态的切换开销。然而这样的方式具有很多天然的缺陷</p>
<h1 id="ebpf-入门实践教程二十一-使用-xdp-进行可编程数据包处理"><a class="header" href="#ebpf-入门实践教程二十一-使用-xdp-进行可编程数据包处理">eBPF 入门实践教程二十一: 使用 XDP 进行可编程数据包处理</a></h1>
<p>在本教程中,我们将介绍 XDPeXpress Data Path并通过一个简单的例子帮助你入门。之后我们将探讨更高级的 XDP 应用,例如负载均衡器、防火墙及其他实际应用。如果你对 eBPF 或 XDP 感兴趣,请在 <a href="https://github.com/eunomia-bpf/bpf-developer-tutorial">Github</a> 上为我们点赞!</p>
<h2 id="什么是-xdp"><a class="header" href="#什么是-xdp">什么是 XDP</a></h2>
<p>XDP 是 Linux 内核中的一种高性能可编程数据路径,专为网络接口级的数据包处理而设计。通过将 eBPF 程序直接附加到网络设备驱动程序上XDP 能够在数据包到达内核网络栈之前拦截并处理它们。这使得 XDP 能够进行极低延迟和高效的数据包处理,非常适合如 DDoS 防护、负载均衡和流量过滤等任务。实际上XDP 每核心的吞吐量可以高达 <strong>每秒 2400 万包Mpps</strong></p>
<h3 id="为什么选择-xdp"><a class="header" href="#为什么选择-xdp">为什么选择 XDP</a></h3>
<p>XDP 运行在比传统 Linux 网络组件(如 cBPF更低的层级在网络设备驱动程序的软中断上下文中执行。它能够在数据包被内核标准网络栈处理之前对其进行处理避免了创建 Linux 中表示网络数据包的 <code>skb_buff</code> 结构。这种早期处理为简单但频繁的操作(如丢弃恶意数据包或负载均衡服务器)带来了显著的性能提升。</p>
<p>与其他数据包处理机制相比XDP 在性能和可用性之间取得了平衡,它利用了 Linux 内核的安全性和可靠性,同时通过可编程的 eBPF 提供了灵活性。</p>
<h2 id="xdp-与其他方法的比较"><a class="header" href="#xdp-与其他方法的比较">XDP 与其他方法的比较</a></h2>
<p>在 XDP 出现之前,一些解决方案通过完全绕过内核来加速数据包处理。其中一个显著的例子是 <strong>DPDK</strong>数据平面开发工具包。DPDK 允许用户空间应用程序直接控制网络设备,从而实现非常高的性能。然而,这种方法也存在一些权衡:</p>
<ol>
<li>
<p><strong>缺乏内核集成</strong>DPDK 及其他内核绕过解决方案无法利用现有的内核网络功能,开发者必须在用户空间重新实现许多协议和功能。</p>
</li>
<li>
<p><strong>安全边界</strong>:这些绕过技术破坏了内核的安全模型,使得难以利用内核提供的安全工具。</p>
</li>
<li>
<p><strong>用户空间与内核的转换开销</strong>:当用户空间数据包处理需要与传统内核网络交互时(例如基于套接字的应用程序),数据包必须重新注入到内核中,增加了开销和复杂性。</p>
</li>
<li>
<p><strong>专用 CPU 使用</strong>为了处理高流量DPDK 和类似解决方案通常需要专用的 CPU 核心来处理数据包,这限制了通用系统的可扩展性和效率。</p>
</li>
</ol>
<p>另一个替代 XDP 的方法是使用 Linux 网络栈中的 <strong>内核模块</strong><strong>挂钩</strong>。虽然这种方法可以很好地集成现有的内核功能,但它需要大量的内核修改,且由于在数据包处理管道的后期运行,无法提供与 XDP 相同的性能优势。</p>
<h3 id="xdp--ebpf-的优势"><a class="header" href="#xdp--ebpf-的优势">XDP + eBPF 的优势</a></h3>
<p>XDP 与 eBPF 结合提供了介于内核绕过方案(如 DPDK和内核集成方案之间的中间地带。以下是 XDP + eBPF 脱颖而出的原因:</p>
<ul>
<li>无法与内核中成熟的网络模块集成,而不得不在用户态将其重新实现;</li>
<li>破坏了内核的安全边界,使得内核提供的很多网络工具变得不可用;</li>
<li>在与常规的 socket 交互时,需要从用户态重新将包注入到内核;</li>
<li>需要占用一个或多个单独的 CPU 来进行包处理;</li>
<li>
<p><strong>高性能</strong>通过在网络接口卡NIC驱动程序级别拦截数据包XDP 可以实现接近线速的性能,用于丢弃、重定向或负载均衡数据包,同时保持低资源消耗。</p>
</li>
<li>
<p><strong>内核集成</strong>:与 DPDK 不同XDP 在 Linux 内核中工作,允许与现有的内核网络栈和工具(如 <code>iptables</code><code>nftables</code> 或套接字)无缝交互。无需在用户空间重新实现网络协议。</p>
</li>
<li>
<p><strong>安全性</strong>eBPF 虚拟机确保用户定义的 XDP 程序是被隔离的不会对内核造成不稳定影响。eBPF 的安全模型防止恶意或有缺陷的代码损害系统,提供了一个安全的可编程数据包处理环境。</p>
</li>
<li>
<p><strong>不需要专用 CPU</strong>XDP 允许数据包处理而无需将整个 CPU 核心专用于网络任务。这提高了系统的整体效率,允许更灵活的资源分配。</p>
</li>
</ul>
<p>总的来说XDP + eBPF 提供了一种强大的可编程数据包处理解决方案,结合了高性能与内核集成的灵活性和安全性。它消除了完全绕过内核方案的缺点,同时保留了内核安全性和功能的优势。</p>
<h2 id="xdp-的项目和应用案例"><a class="header" href="#xdp-的项目和应用案例">XDP 的项目和应用案例</a></h2>
<p>XDP 已经在许多高调的项目中得到应用,这些项目展示了它在实际网络场景中的强大功能和灵活性:</p>
<h3 id="1-cilium"><a class="header" href="#1-cilium">1. <strong>Cilium</strong></a></h3>
<ul>
<li><strong>描述</strong>Cilium 是一个为云原生环境(尤其是 Kubernetes设计的开源网络、安全和可观测性工具。它利用 XDP 实现高性能的数据包过滤和负载均衡。</li>
<li><strong>应用案例</strong>Cilium 将数据包过滤和安全策略卸载到 XDP实现高吞吐量和低延迟的容器化环境流量管理同时不牺牲可扩展性。</li>
<li><strong>链接</strong><a href="https://cilium.io/">Cilium</a></li>
</ul>
<h3 id="2-katran"><a class="header" href="#2-katran">2. <strong>Katran</strong></a></h3>
<ul>
<li><strong>描述</strong>Katran 是由 Facebook 开发的第 4 层负载均衡器,优化了高可扩展性和性能。它使用 XDP 处理数据包转发,开销极小。</li>
<li><strong>应用案例</strong>Katran 每秒处理数百万个数据包,高效地将流量分配到后端服务器上,利用 XDP 在大规模数据中心中实现低延迟和高性能的负载均衡。</li>
<li><strong>链接</strong><a href="https://github.com/facebookincubator/katran">Katran GitHub</a></li>
</ul>
<h3 id="3-cloudflare-的-xdp-ddos-保护"><a class="header" href="#3-cloudflare-的-xdp-ddos-保护">3. <strong>Cloudflare 的 XDP DDoS 保护</strong></a></h3>
<ul>
<li><strong>描述</strong>Cloudflare 已经实现了基于 XDP 的实时 DDoS 缓解。通过在 NIC 级别处理数据包Cloudflare 能够在恶意流量进入网络栈之前过滤掉攻击流量,最小化 DDoS 攻击对其系统的影响。</li>
<li><strong>应用案例</strong>Cloudflare 利用 XDP 在管道早期丢弃恶意数据包,保护其基础设施免受大规模 DDoS 攻击,同时保持对合法流量的高可用性。</li>
<li><strong>链接</strong><a href="https://blog.cloudflare.com/l4drop-xdp-ebpf-based-ddos-mitigations/">Cloudflare 博客关于 XDP</a></li>
</ul>
<p>这些项目展示了 XDP 在不同领域的可扩展和高效的数据包处理能力,从安全和负载均衡到云原生网络。</p>
<h3 id="为什么选择-xdp-而不是其他方法"><a class="header" href="#为什么选择-xdp-而不是其他方法">为什么选择 XDP 而不是其他方法?</a></h3>
<p>与传统方法(如 <code>iptables</code><code>nftables</code><code>tc</code>相比XDP 提供了几个明显的优势:</p>
<ul>
<li>
<p><strong>速度与低开销</strong>XDP 直接在 NIC 驱动程序中运行,绕过了内核的大部分开销,使数据包处理更快。</p>
</li>
<li>
<p><strong>可定制性</strong>XDP 允许开发人员通过 eBPF 创建自定义的数据包处理程序,提供比传统工具(如 <code>iptables</code>)更大的灵活性和细粒度控制。</p>
</li>
<li>
<p><strong>资源效率</strong>XDP 不需要像 DPDK 等用户空间解决方案那样将整个 CPU 核心专用于数据包处理,因此它是高性能网络的更高效选择。</p>
</li>
</ul>
<p>除此之外,利用内核模块和内核网络协议栈中的 hook 点也是一种思路,然而前者对内核的改动大,出错的代价高昂;后者在整套包处理流程中位置偏后,其效率不够理想。</p>
<p>总而言之xdp + eBPF 为可编程包处理系统提出了一种更为稳健的思路,在某种程度上权衡了上述方案的种种优点和不足,获取较高性能的同时又不会对内核的包处理流程进行过多的改变,同时借助 eBPF 虚拟机的优势将用户定义的包处理过程进行隔离和限制,提高了安全性。</p>
<h2 id="编写-ebpf-程序"><a class="header" href="#编写-ebpf-程序">编写 eBPF 程序</a></h2>
<pre><code class="language-C">#include "vmlinux.h"
#include &lt;bpf/bpf_helpers.h&gt;

View File

@@ -173,7 +173,7 @@
<div id="content" class="content">
<main>
<h1 id="在-andorid-上使用-ebpf-程序"><a class="header" href="#在-andorid-上使用-ebpf-程序">在 Andorid 上使用 eBPF 程序</a></h1>
<h1 id="在-android-上使用-ebpf-程序"><a class="header" href="#在-android-上使用-ebpf-程序">在 Android 上使用 eBPF 程序</a></h1>
<blockquote>
<p>本文主要记录了笔者在 Android Studio Emulator 中测试高版本 Android Kernel 对基于 libbpf 的 CO-RE 技术支持程度的探索过程、结果和遇到的问题。
测试采用的方式是在 Android Shell 环境下构建 Debian 环境,并基于此尝试构建 eunomia-bpf 工具链、运行其测试用例。</p>

View File

@@ -1,53 +0,0 @@
// xdp_lb.bpf.c
#include <linux/bpf.h>
#include <bpf/bpf_helpers.h>
#include <linux/if_ether.h>
#include <linux/ip.h>
#include <linux/in.h>
struct {
__uint(type, BPF_MAP_TYPE_ARRAY);
__uint(max_entries, 2); // Two backends
__type(key, __u32);
__type(value, __u32); // Backend IPs
} backends SEC(".maps");
SEC("xdp")
int xdp_load_balancer(struct xdp_md *ctx) {
void *data_end = (void *)(long)ctx->data_end;
void *data = (void *)(long)ctx->data;
struct ethhdr *eth = data;
if ((void *)(eth + 1) > data_end)
return XDP_PASS;
if (eth->h_proto != __constant_htons(ETH_P_IP))
return XDP_PASS;
struct iphdr *iph = (struct iphdr *)(eth + 1);
if ((void *)(iph + 1) > data_end)
return XDP_PASS;
if (iph->protocol != IPPROTO_TCP && iph->protocol != IPPROTO_UDP)
return XDP_PASS;
__u32 key = 0;
static __u32 cnt = 0;
__u32 *backend_ip = bpf_map_lookup_elem(&backends, &key);
if (!backend_ip)
return XDP_PASS;
cnt = (cnt + 1) % 2; // Round-robin
key = cnt;
backend_ip = bpf_map_lookup_elem(&backends, &key);
if (backend_ip) {
iph->daddr = *backend_ip; // Redirect to the backend IP
iph->check = 0; // Needs recomputation in real cases
}
return XDP_TX; // Transmit modified packet back
}
char _license[] SEC("license") = "GPL";

View File

@@ -1,83 +0,0 @@
// xdp_lb.c
#include <stdio.h>
#include <stdlib.h>
#include <bpf/libbpf.h>
#include <bpf/bpf.h>
#include <net/if.h>
#include <linux/if_link.h>
#include "xdp_lb.skel.h" // This header is auto-generated by bpftool
#define IFACE "eth0" // Replace with your network interface
static int set_up_backends(struct xdp_lb_bpf *skel) {
__u32 backend1 = htonl(0xC0A80102); // 192.168.1.2
__u32 backend2 = htonl(0xC0A80103); // 192.168.1.3
__u32 key = 0;
if (bpf_map_update_elem(bpf_map__fd(skel->maps.backends), &key, &backend1, BPF_ANY) < 0) {
fprintf(stderr, "Failed to update backend 1\n");
return -1;
}
key = 1;
if (bpf_map_update_elem(bpf_map__fd(skel->maps.backends), &key, &backend2, BPF_ANY) < 0) {
fprintf(stderr, "Failed to update backend 2\n");
return -1;
}
return 0;
}
int main() {
struct xdp_lb_bpf *skel;
int err, ifindex;
// Load and verify the eBPF skeleton
skel = xdp_lb_bpf__open();
if (!skel) {
fprintf(stderr, "Failed to open and load skeleton\n");
return 1;
}
// Load eBPF program
err = xdp_lb_bpf__load(skel);
if (err) {
fprintf(stderr, "Failed to load BPF program: %d\n", err);
return 1;
}
// Set up the backend IP addresses
if (set_up_backends(skel) < 0) {
fprintf(stderr, "Failed to set up backend IP addresses\n");
return 1;
}
// Get interface index
ifindex = if_nametoindex(IFACE);
if (ifindex == 0) {
perror("if_nametoindex");
return 1;
}
// Attach the XDP program
err = bpf_xdp_attach(ifindex, bpf_program__fd(skel->progs.xdp_load_balancer), XDP_FLAGS_SKB_MODE, NULL);
if (err) {
fprintf(stderr, "Failed to attach XDP program: %d\n", err);
return 1;
}
printf("XDP Load Balancer is running on interface %s...\n", IFACE);
sleep(60); // Keep running for 60 seconds
// Detach the XDP program before exiting
err = bpf_xdp_detach(ifindex, XDP_FLAGS_SKB_MODE, NULL);
if (err) {
fprintf(stderr, "Failed to detach XDP program: %d\n", err);
return 1;
}
// Clean up
xdp_lb_bpf__destroy(skel);
return 0;
}

6
41-xdp-tcpdump/.gitignore vendored Normal file
View File

@@ -0,0 +1,6 @@
.output
uprobe
merge-btf
*.btf
xdp_lb
xdp-tcpdump

View File

@@ -24,7 +24,7 @@ INCLUDES := -I$(OUTPUT) -I../third_party/libbpf/include/uapi -I$(dir $(VMLINUX))
CFLAGS := -g -Wall
ALL_LDFLAGS := $(LDFLAGS) $(EXTRA_LDFLAGS)
APPS = funclatency
APPS = xdp-tcpdump
CARGO ?= $(shell which cargo)
ifeq ($(strip $(CARGO)),)

View File

@@ -0,0 +1,107 @@
#include "vmlinux.h"
#include <bpf/bpf_helpers.h>
#include <bpf/bpf_endian.h>
#define ETH_P_IP 0x0800
// Define the ring buffer map
struct {
__uint(type, BPF_MAP_TYPE_RINGBUF);
__uint(max_entries, 1 << 24); // 16 MB buffer
} rb SEC(".maps");
// Helper function to check if the packet is TCP
static bool is_tcp(struct ethhdr *eth, void *data_end)
{
// Ensure Ethernet header is within bounds
if ((void *)(eth + 1) > data_end)
return false;
// Only handle IPv4 packets
if (bpf_ntohs(eth->h_proto) != ETH_P_IP)
return false;
struct iphdr *ip = (struct iphdr *)(eth + 1);
// Ensure IP header is within bounds
if ((void *)(ip + 1) > data_end)
return false;
// Check if the protocol is TCP
if (ip->protocol != IPPROTO_TCP)
return false;
return true;
}
SEC("xdp")
int xdp_pass(struct xdp_md *ctx)
{
// Pointers to packet data
void *data = (void *)(long)ctx->data;
void *data_end = (void *)(long)ctx->data_end;
// Parse Ethernet header
struct ethhdr *eth = data;
// Check if the packet is a TCP packet
if (!is_tcp(eth, data_end)) {
return XDP_PASS;
}
// Cast to IP header
struct iphdr *ip = (struct iphdr *)(eth + 1);
// Calculate IP header length
int ip_hdr_len = ip->ihl * 4;
if (ip_hdr_len < sizeof(struct iphdr)) {
return XDP_PASS;
}
// Ensure IP header is within packet bounds
if ((void *)ip + ip_hdr_len > data_end) {
return XDP_PASS;
}
// Parse TCP header
struct tcphdr *tcp = (struct tcphdr *)((unsigned char *)ip + ip_hdr_len);
// Ensure TCP header is within packet bounds
if ((void *)(tcp + 1) > data_end) {
return XDP_PASS;
}
// Define the number of bytes you want to capture from the TCP header
// Typically, the TCP header is 20 bytes, but with options, it can be longer
// Here, we'll capture the first 32 bytes to include possible options
const int tcp_header_bytes = 32;
// Ensure that the desired number of bytes does not exceed packet bounds
if ((void *)tcp + tcp_header_bytes > data_end) {
return XDP_PASS;
}
// Reserve space in the ring buffer
void *ringbuf_space = bpf_ringbuf_reserve(&rb, tcp_header_bytes, 0);
if (!ringbuf_space) {
return XDP_PASS; // If reservation fails, skip processing
}
// Copy the TCP header bytes into the ring buffer
// Using a loop to ensure compliance with eBPF verifier
for (int i = 0; i < tcp_header_bytes; i++) {
// Accessing each byte safely within bounds
unsigned char byte = *((unsigned char *)tcp + i);
((unsigned char *)ringbuf_space)[i] = byte;
}
// Submit the data to the ring buffer
bpf_ringbuf_submit(ringbuf_space, 0);
// Optional: Print a debug message (will appear in kernel logs)
bpf_printk("Captured TCP header (%d bytes)", tcp_header_bytes);
return XDP_PASS;
}
char __license[] SEC("license") = "GPL";

View File

@@ -0,0 +1,165 @@
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <unistd.h>
#include <net/if.h>
#include <bpf/libbpf.h>
#include <bpf/bpf.h>
#include "xdp-tcpdump.skel.h" // Generated skeleton header
// Callback function to handle events from the ring buffer
static int handle_event(void *ctx, void *data, size_t data_sz)
{
if (data_sz < 20) { // Minimum TCP header size
fprintf(stderr, "Received incomplete TCP header\n");
return 0;
}
// Parse the raw TCP header bytes
struct tcphdr {
uint16_t source;
uint16_t dest;
uint32_t seq;
uint32_t ack_seq;
uint16_t res1:4,
doff:4,
fin:1,
syn:1,
rst:1,
psh:1,
ack:1,
urg:1,
ece:1,
cwr:1;
uint16_t window;
uint16_t check;
uint16_t urg_ptr;
// Options and padding may follow
} __attribute__((packed));
if (data_sz < sizeof(struct tcphdr)) {
fprintf(stderr, "Data size (%zu) less than TCP header size\n", data_sz);
return 0;
}
struct tcphdr *tcp = (struct tcphdr *)data;
// Convert fields from network byte order to host byte order
uint16_t source_port = ntohs(tcp->source);
uint16_t dest_port = ntohs(tcp->dest);
uint32_t seq = ntohl(tcp->seq);
uint32_t ack_seq = ntohl(tcp->ack_seq);
uint16_t window = ntohs(tcp->window);
// Extract flags
uint8_t flags = 0;
flags |= (tcp->fin) ? 0x01 : 0x00;
flags |= (tcp->syn) ? 0x02 : 0x00;
flags |= (tcp->rst) ? 0x04 : 0x00;
flags |= (tcp->psh) ? 0x08 : 0x00;
flags |= (tcp->ack) ? 0x10 : 0x00;
flags |= (tcp->urg) ? 0x20 : 0x00;
flags |= (tcp->ece) ? 0x40 : 0x00;
flags |= (tcp->cwr) ? 0x80 : 0x00;
printf("Captured TCP Header:\n");
printf(" Source Port: %u\n", source_port);
printf(" Destination Port: %u\n", dest_port);
printf(" Sequence Number: %u\n", seq);
printf(" Acknowledgment Number: %u\n", ack_seq);
printf(" Data Offset: %u\n", tcp->doff);
printf(" Flags: 0x%02x\n", flags);
printf(" Window Size: %u\n", window);
printf("\n");
return 0;
}
int main(int argc, char **argv)
{
struct xdp_tcpdump_bpf *skel;
struct ring_buffer *rb = NULL;
int ifindex;
int err;
if (argc != 2)
{
fprintf(stderr, "Usage: %s <ifname>\n", argv[0]);
return 1;
}
const char *ifname = argv[1];
ifindex = if_nametoindex(ifname);
if (ifindex == 0)
{
fprintf(stderr, "Invalid interface name %s\n", ifname);
return 1;
}
/* Open and load BPF application */
skel = xdp_tcpdump_bpf__open();
if (!skel)
{
fprintf(stderr, "Failed to open BPF skeleton\n");
return 1;
}
/* Load & verify BPF programs */
err = xdp_tcpdump_bpf__load(skel);
if (err)
{
fprintf(stderr, "Failed to load and verify BPF skeleton: %d\n", err);
goto cleanup;
}
/* Attach XDP program */
err = xdp_tcpdump_bpf__attach(skel);
if (err)
{
fprintf(stderr, "Failed to attach BPF skeleton: %d\n", err);
goto cleanup;
}
/* Attach the XDP program to the specified interface */
skel->links.xdp_pass = bpf_program__attach_xdp(skel->progs.xdp_pass, ifindex);
if (!skel->links.xdp_pass)
{
err = -errno;
fprintf(stderr, "Failed to attach XDP program: %s\n", strerror(errno));
goto cleanup;
}
printf("Successfully attached XDP program to interface %s\n", ifname);
/* Set up ring buffer polling */
rb = ring_buffer__new(bpf_map__fd(skel->maps.rb), handle_event, NULL, NULL);
if (!rb)
{
fprintf(stderr, "Failed to create ring buffer\n");
err = -1;
goto cleanup;
}
printf("Start polling ring buffer\n");
/* Poll the ring buffer */
while (1)
{
err = ring_buffer__poll(rb, -1);
if (err == -EINTR)
continue;
if (err < 0)
{
fprintf(stderr, "Error polling ring buffer: %d\n", err);
break;
}
}
cleanup:
ring_buffer__free(rb);
xdp_tcpdump_bpf__destroy(skel);
return -err;
}

View File

@@ -0,0 +1,14 @@
#include "vmlinux.h"
#include <bpf/bpf_helpers.h>
SEC("xdp")
int xdp_pass(struct xdp_md* ctx) {
void* data = (void*)(long)ctx->data;
void* data_end = (void*)(long)ctx->data_end;
int pkt_sz = data_end - data;
bpf_printk("packet size is %d", pkt_sz);
return XDP_PASS;
}
char __license[] SEC("license") = "GPL";

Binary file not shown.

159
42-xdp-loadbalancer/setup.sh Executable file
View File

@@ -0,0 +1,159 @@
#!/bin/bash
set -xe
part_mac="DE:AD:BE:EF:00:"
create_bridge () {
if ! ip link show $1 &> /dev/null; then
ip link add name $1 type bridge
ip link set dev $1 up
else
echo "Bridge $1 already exists."
fi
}
create_pair () {
if ! ip link show $1 &> /dev/null; then
ip link add name $1 type veth peer name $2
ip link set $1 address "$part_mac""$5"
ip addr add $3 brd + dev $1
ip link set $2 master $4
ip link set dev $1 up
ip link set dev $2 up
else
echo "Veth pair $1 <--> $2 already exists."
fi
}
create_pair_ns () {
if ! ip link show $2 &> /dev/null; then
ip link add name $1 type veth peer name $2
ip link set $2 master $4
ip link set dev $2 up
ip netns add $5
ip link set $1 netns $5
ip netns exec $5 ip addr add $3 brd + dev $1
ip netns exec $5 ip link set $1 address "$part_mac""$6"
ip netns exec $5 ip link set dev $1 up
ip netns exec $5 ip link set lo up # Bring up loopback interface
else
echo "Veth pair $1 <--> $2 already exists in namespace $5."
fi
}
# Create bridge br0
create_bridge br0
# Create veth pairs and assign IPs
create_pair veth0 veth1 "10.0.0.1/24" br0 01
# Create veth pairs in namespaces h2, h3, and lb
create_pair_ns veth2 veth3 "10.0.0.2/24" br0 h2 02
create_pair_ns veth4 veth5 "10.0.0.3/24" br0 h3 03
# Create the lb namespace
create_pair_ns veth6 veth7 "10.0.0.10/24" br0 lb 10
# Enable IP forwarding on the host
sudo sysctl -w net.ipv4.ip_forward=1
# Set the FORWARD chain policy to ACCEPT in iptables to ensure packets are forwarded
sudo iptables -P FORWARD ACCEPT
# maybe you can do similar things
# sudo ip netns exec h2 bpftool load xdp_pass.o veth2
# sudo ip netns exec h3 bpftool load xdp_pass.o veth4
# Helper function for error exit on ping failure
function ping_or_fail() {
if ! sudo ip netns exec $1 ping -c 3 $2; then
echo "Ping from $1 to $2 failed!"
exit 1
fi
}
# Ping test with failure checks
function check_connectivity() {
echo "Testing connectivity between namespaces and Load Balancer..."
# Ping from h2 to h3 and h3 to h2
ping_or_fail h2 10.0.0.3
ping_or_fail h3 10.0.0.2
# Ping from h2 to Load Balancer and h3 to Load Balancer
ping_or_fail h2 10.0.0.10
ping_or_fail h3 10.0.0.10
# Ping from Load Balancer to h2 and h3
ping_or_fail lb 10.0.0.2
ping_or_fail lb 10.0.0.3
# Ping from Local Machine to Load Balancer
ping -c 3 10.0.0.10 || { echo "Ping from Local Machine to Load Balancer failed!"; exit 1; }
echo "All ping tests passed!"
}
# Debugging helper functions
# Check if all interfaces are up and running
check_interfaces () {
for ns in h2 h3 lb; do
echo "Checking interfaces in namespace $ns..."
sudo ip netns exec $ns ip addr show
sudo ip netns exec $ns ip link show
done
echo "Checking bridge br0..."
ip addr show br0
ip link show br0
}
# Check IP forwarding settings
check_ip_forwarding () {
echo "Checking IP forwarding status on the host..."
sudo sysctl net.ipv4.ip_forward
echo "Checking IP forwarding status in namespace $ns..."
sudo ip netns exec $ns sysctl net.ipv4.ip_forward
}
# Check ARP table
check_arp_table () {
echo "Checking ARP table on the host..."
arp -n
for ns in h2 h3 lb; do
echo "Checking ARP table in namespace $ns..."
sudo ip netns exec $ns ip neigh show
done
}
# Check routing tables
check_routing_table () {
echo "Checking routing table on the host..."
ip route show
for ns in h2 h3 lb; do
echo "Checking routing table in namespace $ns..."
sudo ip netns exec $ns ip route show
done
}
# Check if firewall rules are blocking traffic
check_firewall_rules () {
echo "Checking firewall rules on the host..."
sudo iptables -L
}
# Run checks to verify the network
check_interfaces
check_ip_forwarding
check_arp_table
check_routing_table
check_firewall_rules
check_connectivity
echo "Setup and checks completed!"

36
42-xdp-loadbalancer/teardown.sh Executable file
View File

@@ -0,0 +1,36 @@
#!/bin/bash
set -xe
rm_bridge () {
if ip link show $1 &> /dev/null; then
ip link set dev $1 down
ip link delete $1 type bridge
fi
}
rm_pair () {
if ip link show $1 &> /dev/null; then
ip link delete $1 type veth
fi
}
rm_ns () {
if ip netns list | grep -w "$1" &> /dev/null; then
ip netns delete $1
fi
}
# Remove bridge br0
rm_bridge br0
# Remove veth pairs
rm_pair veth0
rm_pair veth2
rm_pair veth4
rm_pair veth6
# Remove namespaces
rm_ns h2
rm_ns h3
rm_ns lb

View File

@@ -0,0 +1,117 @@
// xdp_lb.bpf.c
#include <bpf/bpf_endian.h>
#include <linux/bpf.h>
#include <bpf/bpf_helpers.h>
#include <linux/if_ether.h>
#include <linux/ip.h>
#include <linux/in.h>
#include <linux/tcp.h>
#include "xx_hash.h"
struct backend_config {
__u32 ip;
unsigned char mac[ETH_ALEN];
};
// Backend IP and MAC address map
struct {
__uint(type, BPF_MAP_TYPE_ARRAY);
__uint(max_entries, 2); // Two backends
__type(key, __u32);
__type(value, struct backend_config);
} backends SEC(".maps");
int client_ip = bpf_htonl(0xa000001);
unsigned char client_mac[ETH_ALEN] = {0xDE, 0xAD, 0xBE, 0xEF, 0x0, 0x1};
int load_balancer_ip = bpf_htonl(0xa00000a);
unsigned char load_balancer_mac[ETH_ALEN] = {0xDE, 0xAD, 0xBE, 0xEF, 0x0, 0x10};
static __always_inline __u16
csum_fold_helper(__u64 csum)
{
int i;
for (i = 0; i < 4; i++)
{
if (csum >> 16)
csum = (csum & 0xffff) + (csum >> 16);
}
return ~csum;
}
static __always_inline __u16
iph_csum(struct iphdr *iph)
{
iph->check = 0;
unsigned long long csum = bpf_csum_diff(0, 0, (unsigned int *)iph, sizeof(struct iphdr), 0);
return csum_fold_helper(csum);
}
SEC("xdp")
int xdp_load_balancer(struct xdp_md *ctx) {
void *data_end = (void *)(long)ctx->data_end;
void *data = (void *)(long)ctx->data;
bpf_printk("xdp_load_balancer received packet");
// Ethernet header
struct ethhdr *eth = data;
if ((void *)(eth + 1) > data_end)
return XDP_PASS;
// Check if the packet is IP (IPv4)
if (eth->h_proto != __constant_htons(ETH_P_IP))
return XDP_PASS;
// IP header
struct iphdr *iph = (struct iphdr *)(eth + 1);
if ((void *)(iph + 1) > data_end)
return XDP_PASS;
// Check if the protocol is TCP or UDP
if (iph->protocol != IPPROTO_TCP)
return XDP_PASS;
bpf_printk("Received Source IP: 0x%x", bpf_ntohl(iph->saddr));
bpf_printk("Received Destination IP: 0x%x", bpf_ntohl(iph->daddr));
bpf_printk("Received Source MAC: %x:%x:%x:%x:%x:%x", eth->h_source[0], eth->h_source[1], eth->h_source[2], eth->h_source[3], eth->h_source[4], eth->h_source[5]);
bpf_printk("Received Destination MAC: %x:%x:%x:%x:%x:%x", eth->h_dest[0], eth->h_dest[1], eth->h_dest[2], eth->h_dest[3], eth->h_dest[4], eth->h_dest[5]);
if (iph->saddr == client_ip)
{
bpf_printk("Packet from client");
__u32 key = xxhash32((const char*)iph, sizeof(struct iphdr), 0) % 2;
struct backend_config *backend = bpf_map_lookup_elem(&backends, &key);
if (!backend)
return XDP_PASS;
iph->daddr = backend->ip;
__builtin_memcpy(eth->h_dest, backend->mac, ETH_ALEN);
}
else
{
bpf_printk("Packet from backend");
iph->daddr = client_ip;
__builtin_memcpy(eth->h_dest, client_mac, ETH_ALEN);
}
// Update IP source address to the load balancer's IP
iph->saddr = load_balancer_ip;
// Update Ethernet source MAC address to the current lb's MAC
__builtin_memcpy(eth->h_source, load_balancer_mac, ETH_ALEN);
// Recalculate IP checksum
iph->check = iph_csum(iph);
bpf_printk("Redirecting packet to new IP 0x%x from IP 0x%x",
bpf_ntohl(iph->daddr),
bpf_ntohl(iph->saddr)
);
bpf_printk("New Dest MAC: %x:%x:%x:%x:%x:%x", eth->h_dest[0], eth->h_dest[1], eth->h_dest[2], eth->h_dest[3], eth->h_dest[4], eth->h_dest[5]);
bpf_printk("New Source MAC: %x:%x:%x:%x:%x:%x\n", eth->h_source[0], eth->h_source[1], eth->h_source[2], eth->h_source[3], eth->h_source[4], eth->h_source[5]);
// Return XDP_TX to transmit the modified packet back to the network
return XDP_TX;
}
char _license[] SEC("license") = "GPL";

View File

@@ -0,0 +1,96 @@
// xdp_lb.c
#include <arpa/inet.h>
#include <bpf/bpf.h>
#include <bpf/libbpf.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <net/if.h>
#include "xdp_lb.skel.h" // The generated skeleton
struct backend_config {
__u32 ip;
unsigned char mac[6];
};
static int parse_mac(const char *str, unsigned char *mac) {
if (sscanf(str, "%hhx:%hhx:%hhx:%hhx:%hhx:%hhx",
&mac[0], &mac[1], &mac[2], &mac[3], &mac[4], &mac[5]) != 6) {
fprintf(stderr, "Invalid MAC address format\n");
return -1;
}
return 0;
}
int main(int argc, char **argv) {
if (argc != 6) {
fprintf(stderr, "Usage: %s <ifname> <backend1_ip> <backend1_mac> <backend2_ip> <backend2_mac>\n", argv[0]);
return 1;
}
const char *ifname = argv[1];
struct backend_config backend[2];
// Parse backend 1
if (inet_pton(AF_INET, argv[2], &backend[0].ip) != 1) {
fprintf(stderr, "Invalid backend 1 IP address\n");
return 1;
}
if (parse_mac(argv[3], backend[0].mac) < 0) {
return 1;
}
// Parse backend 2
if (inet_pton(AF_INET, argv[4], &backend[1].ip) != 1) {
fprintf(stderr, "Invalid backend 2 IP address\n");
return 1;
}
if (parse_mac(argv[5], backend[1].mac) < 0) {
return 1;
}
// Load and attach the BPF program
struct xdp_lb_bpf *skel = xdp_lb_bpf__open_and_load();
if (!skel) {
fprintf(stderr, "Failed to open and load BPF skeleton\n");
return 1;
}
int ifindex = if_nametoindex(ifname);
if (ifindex < 0) {
perror("if_nametoindex");
xdp_lb_bpf__destroy(skel);
return 1;
}
if (bpf_program__attach_xdp(skel->progs.xdp_load_balancer, ifindex) < 0) {
fprintf(stderr, "Failed to attach XDP program\n");
xdp_lb_bpf__destroy(skel);
return 1;
}
// Update backend configurations
for (int i = 0; i < 2; i++) {
if (bpf_map_update_elem(bpf_map__fd(skel->maps.backends), &i, &backend[i], 0) < 0) {
perror("bpf_map_update_elem");
xdp_lb_bpf__destroy(skel);
return 1;
}
}
printf("XDP load balancer configured with backends:\n");
printf("Backend 1 - IP: %s, MAC: %s\n", argv[2], argv[3]);
printf("Backend 2 - IP: %s, MAC: %s\n", argv[4], argv[5]);
printf("Press Ctrl+C to exit...\n");
while (1) {
sleep(1); // Keep the program running
}
// Cleanup and detach
bpf_xdp_detach(ifindex, 0, NULL);
xdp_lb_bpf__detach(skel);
xdp_lb_bpf__destroy(skel);
return 0;
}

View File

@@ -0,0 +1,57 @@
#ifndef XXHASH_BPF_H
#define XXHASH_BPF_H
#define PRIME1 0x9E3779B1U
#define PRIME2 0x85EBCA77U
#define PRIME3 0xC2B2AE3DU
#define PRIME4 0x27D4EB2FU
#define PRIME5 0x165667B1U
static __always_inline unsigned int rotl (unsigned int x, int r) {
return ((x << r) | (x >> (32 - r)));
}
// Normal stripe processing routine.
static __always_inline unsigned int round_xxhash(unsigned int acc, const unsigned int input) {
return rotl(acc + (input * PRIME2), 13) * PRIME1;
}
static __always_inline unsigned int avalanche_step (const unsigned int h, const int rshift, const unsigned int prime) {
return (h ^ (h >> rshift)) * prime;
}
// Mixes all bits to finalize the hash.
static __always_inline unsigned int avalanche (const unsigned int h) {
return avalanche_step(avalanche_step(avalanche_step(h, 15, PRIME2), 13, PRIME3), 16, 1);
}
static __always_inline unsigned int endian32 (const char *v) {
return (unsigned int)((unsigned char)(v[0]))|((unsigned int)((unsigned char)(v[1])) << 8)
|((unsigned int)((unsigned char)(v[2])) << 16)|((unsigned int)((unsigned char)(v[3])) << 24);
}
static __always_inline unsigned int fetch32 (const char *p, const unsigned int v) {
return round_xxhash(v, endian32(p));
}
// Processes the last 0-15 bytes of p.
static __always_inline unsigned int finalize (const unsigned int h, const char *p, unsigned int len) {
return
(len >= 4) ? finalize(rotl(h + (endian32(p) * PRIME3), 17) * PRIME4, p + 4, len - 4) :
(len > 0) ? finalize(rotl(h + ((unsigned char)(*p) * PRIME5), 11) * PRIME1, p + 1, len - 1) :
avalanche(h);
}
static __always_inline unsigned int h16bytes_4 (const char *p, unsigned int len, const unsigned int v1, const unsigned int v2, const unsigned int v3, const unsigned int v4) {
return
(len >= 16) ? h16bytes_4(p + 16, len - 16, fetch32(p, v1), fetch32(p+4, v2), fetch32(p+8, v3), fetch32(p+12, v4)) :
rotl(v1, 1) + rotl(v2, 7) + rotl(v3, 12) + rotl(v4, 18);
}
static __always_inline unsigned int h16bytes_3 (const char *p, unsigned int len, const unsigned int seed) {
return h16bytes_4(p, len, seed + PRIME1 + PRIME2, seed + PRIME2, seed, seed - PRIME1);
}
static __always_inline unsigned int xxhash32 (const char *input, unsigned int len, unsigned int seed) {
return finalize((len >= 16 ? h16bytes_3(input, len, seed) : seed + PRIME5) + len, (input) + (len & ~0xF), len & 0xF);
}
#endif

View File

@@ -219,12 +219,11 @@
<h3 id="了解如何开发-ebpf-程序10-15h"><a class="header" href="#了解如何开发-ebpf-程序10-15h">了解如何开发 eBPF 程序10-15h</a></h3>
<p>了解并尝试一下 eBPF 开发框架:</p>
<ul>
<li>bpftrace 教程对于最简单的应用来说bpftrace 可能是最方便的https://eunomia.dev/zh/tutorials/bpftrace-tutorial/ 试试1h</li>
<li>bpftrace 教程对于最简单的应用来说bpftrace 可能是最方便的:<a href="https://eunomia.dev/zh/tutorials/bpftrace-tutorial/">https://eunomia.dev/zh/tutorials/bpftrace-tutorial/</a> 试试1h</li>
<li>BCC 开发各类小工具的例子:<a href="https://github.com/iovisor/bcc/blob/master/docs/tutorial_bcc_python_developer.md">https://github.com/iovisor/bcc/blob/master/docs/tutorial_bcc_python_developer.md</a> 跑一遍3-4h</li>
<li>libbpf 的一些例子:<a href="https://github.com/libbpf/libbpf-bootstrap">https://github.com/libbpf/libbpf-bootstrap</a> 选感兴趣的运行一下并阅读一下源代码2h</li>
<li>基于 libbpf 和 eunomia-bpf 的教程:<a href="https://github.com/eunomia-bpf/bpf-developer-tutorial">https://github.com/eunomia-bpf/bpf-developer-tutorial</a> (阅读 1-10 的部分3-4h</li>
<li>基于 C 语言 libbpf, Go 语言或者 Rust 语言和 eunomia-bpf 的教程:<a href="https://github.com/eunomia-bpf/bpf-developer-tutorial">https://github.com/eunomia-bpf/bpf-developer-tutorial</a> (阅读 1-20 的部分3-8h</li>
</ul>
<p>其他开发框架Go 语言或者 Rust 语言请自行搜索并且尝试0-2h</p>
<p>有任何问题或者想了解的东西,不管是不是和本项目相关,都可以在本项目的 discussions 里面开始讨论。</p>
<p>回答一些问题并且进行一些尝试2-5h</p>
<ol>
@@ -285,15 +284,13 @@ eBPF 程序每次执行时候都需要进行编译,编译则需要用户配置
字节码装载到内核中。同样的,<code>libbpf-bootstrap</code> 也有非常完备的入门教程,用户可以在<a href="https://nakryiko.com/posts/libbpf-bootstrap/">该处</a>
得到详细的入门操作介绍。</p>
<h3 id="eunomia-bpf"><a class="header" href="#eunomia-bpf">eunomia-bpf</a></h3>
<p>开发、构建和分发 eBPF 一直以来都是一个高门槛的工作,使用 BCC、bpftrace 等工具开发效率高、可移植性好,但是分发部署时需要安装 LLVM、Clang 等编译环境,每次运行的时候执行本地或远程编译过程,资源消耗较大;使用原生的 CO-RE libbpf 时又需要编写不少用户态加载代码来帮助 eBPF 程序正确加载和从内核中获取上报的信息,同时对于 eBPF 程序的分发、管理也没有很好地解决方案。</p>
<p><a href="https://github.com/eunomia-bpf/eunomia-bpf">eunomia-bpf</a> 是一个开源的 eBPF 动态加载运行时和开发工具链,是为了简化 eBPF 程序的开发、构建、分发、运行而设计的,基于 libbpf 的 CO-RE 轻量级开发框架。</p>
<p>使用 eunomia-bpf ,可以:</p>
<ul>
<li>在编写 eBPF 程序或工具时只编写内核态代码,自动获取内核态导出信息,并作为模块动态加载;</li>
<li>使用 WASM 进行用户态交互程序的开发,在 WASM 虚拟机内部控制整个 eBPF 程序的加载和执行,以及处理相关数据;</li>
<li>eunomia-bpf 可以将预编译的 eBPF 程序打包为通用的 JSON 或 WASM 模块,跨架构和内核版本进行分发,无需重新编译即可动态加载运行。</li>
</ul>
<p>eunomia-bpf 由一个编译工具链和一个运行时库组成, 对比传统的 BCC、原生 libbpf 等框架,大幅简化了 eBPF 程序的开发流程,在大多数时候只需编写内核态代码,即可轻松构建、打包、发布完整的 eBPF 应用,同时内核态 eBPF 代码保证和主流的 libbpflibbpfgolibbpf-rs 等开发框架的 100% 兼容性。需要编写用户态代码的时候,也可以借助 Webassembly 实现通过多种语言进行用户态开发。和 bpftrace 等脚本工具相比, eunomia-bpf 保留了类似的便捷性, 同时不仅局限于 trace 方面, 可以用于更多的场景, 如网络、安全等等</p>
<p>eunomia-bpf 由一个编译工具链和一个运行时库组成, 对比传统的 BCC、原生 libbpf 等框架,简化了 eBPF 程序的开发流程,在大多数时候只需编写内核态代码,即可轻松构建、打包、发布完整的 eBPF 应用。</p>
<blockquote>
<ul>
<li>eunomia-bpf 项目 Github 地址: <a href="https://github.com/eunomia-bpf/eunomia-bpf">https://github.com/eunomia-bpf/eunomia-bpf</a></li>
@@ -404,7 +401,7 @@ Packing ebpf object and config into package.json...
</code></pre>
<p>然后使用 ecli 运行编译后的程序:</p>
<pre><code class="language-console">$ sudo ./ecli run package.json
Runing eBPF program...
Running eBPF program...
</code></pre>
<p>运行这段程序后,可以通过查看 /sys/kernel/debug/tracing/trace_pipe 文件来查看 eBPF 程序的输出:</p>
<pre><code class="language-console">$ sudo cat /sys/kernel/debug/tracing/trace_pipe | grep "BPF triggered sys_enter_write"
@@ -4702,19 +4699,81 @@ Packing ebpf object and config into package.json...
<li><a href="http://just4coding.com/2022/08/05/tc/">http://just4coding.com/2022/08/05/tc/</a></li>
<li><a href="https://arthurchiao.art/blog/understanding-tc-da-mode-zh/">https://arthurchiao.art/blog/understanding-tc-da-mode-zh/</a></li>
</ul>
<div style="break-before: page; page-break-before: always;"></div><h1 id="ebpf-入门实践教程二十一使用-xdp-实现可编程包处理"><a class="header" href="#ebpf-入门实践教程二十一使用-xdp-实现可编程包处理">eBPF 入门实践教程二十一:使用 xdp 实现可编程包处理</a></h1>
<h2 id="背景-3"><a class="header" href="#背景-3">背景</a></h2>
<p>xdpeXpress Data Path是 Linux 内核中新兴的一种绕过内核的、可编程的包处理方案。相较于 cBPFxdp 的挂载点非常底层,位于网络设备驱动的软中断处理过程,甚至早于 skb_buff 结构的分配。因此,在 xdp 上挂载 eBPF 程序适用于很多简单但次数极多的包处理操作(如防御 Dos 攻击可以达到很高的性能24Mpps/core</p>
<h2 id="xdp-概述"><a class="header" href="#xdp-概述">XDP 概述</a></h2>
<p>xdp 不是第一个支持可编程包处理的系统,在此之前,以 DPDKData Plane Development Kit为代表的内核旁路方案甚至能够取得更高的性能其思路为完全绕过内核由用户态的网络应用接管网络设备从而避免了用户态和内核态的切换开销。然而这样的方式具有很多天然的缺陷</p>
<div style="break-before: page; page-break-before: always;"></div><h1 id="ebpf-入门实践教程二十一-使用-xdp-进行可编程数据包处理"><a class="header" href="#ebpf-入门实践教程二十一-使用-xdp-进行可编程数据包处理">eBPF 入门实践教程二十一: 使用 XDP 进行可编程数据包处理</a></h1>
<p>在本教程中,我们将介绍 XDPeXpress Data Path并通过一个简单的例子帮助你入门。之后我们将探讨更高级的 XDP 应用,例如负载均衡器、防火墙及其他实际应用。如果你对 eBPF 或 XDP 感兴趣,请在 <a href="https://github.com/eunomia-bpf/bpf-developer-tutorial">Github</a> 上为我们点赞!</p>
<h2 id="什么是-xdp"><a class="header" href="#什么是-xdp">什么是 XDP</a></h2>
<p>XDP 是 Linux 内核中的一种高性能可编程数据路径,专为网络接口级的数据包处理而设计。通过将 eBPF 程序直接附加到网络设备驱动程序上XDP 能够在数据包到达内核网络栈之前拦截并处理它们。这使得 XDP 能够进行极低延迟和高效的数据包处理,非常适合如 DDoS 防护、负载均衡和流量过滤等任务。实际上XDP 每核心的吞吐量可以高达 <strong>每秒 2400 万包Mpps</strong></p>
<h3 id="为什么选择-xdp"><a class="header" href="#为什么选择-xdp">为什么选择 XDP</a></h3>
<p>XDP 运行在比传统 Linux 网络组件(如 cBPF更低的层级在网络设备驱动程序的软中断上下文中执行。它能够在数据包被内核标准网络栈处理之前对其进行处理避免了创建 Linux 中表示网络数据包的 <code>skb_buff</code> 结构。这种早期处理为简单但频繁的操作(如丢弃恶意数据包或负载均衡服务器)带来了显著的性能提升。</p>
<p>与其他数据包处理机制相比XDP 在性能和可用性之间取得了平衡,它利用了 Linux 内核的安全性和可靠性,同时通过可编程的 eBPF 提供了灵活性。</p>
<h2 id="xdp-与其他方法的比较"><a class="header" href="#xdp-与其他方法的比较">XDP 与其他方法的比较</a></h2>
<p>在 XDP 出现之前,一些解决方案通过完全绕过内核来加速数据包处理。其中一个显著的例子是 <strong>DPDK</strong>数据平面开发工具包。DPDK 允许用户空间应用程序直接控制网络设备,从而实现非常高的性能。然而,这种方法也存在一些权衡:</p>
<ol>
<li>
<p><strong>缺乏内核集成</strong>DPDK 及其他内核绕过解决方案无法利用现有的内核网络功能,开发者必须在用户空间重新实现许多协议和功能。</p>
</li>
<li>
<p><strong>安全边界</strong>:这些绕过技术破坏了内核的安全模型,使得难以利用内核提供的安全工具。</p>
</li>
<li>
<p><strong>用户空间与内核的转换开销</strong>:当用户空间数据包处理需要与传统内核网络交互时(例如基于套接字的应用程序),数据包必须重新注入到内核中,增加了开销和复杂性。</p>
</li>
<li>
<p><strong>专用 CPU 使用</strong>为了处理高流量DPDK 和类似解决方案通常需要专用的 CPU 核心来处理数据包,这限制了通用系统的可扩展性和效率。</p>
</li>
</ol>
<p>另一个替代 XDP 的方法是使用 Linux 网络栈中的 <strong>内核模块</strong><strong>挂钩</strong>。虽然这种方法可以很好地集成现有的内核功能,但它需要大量的内核修改,且由于在数据包处理管道的后期运行,无法提供与 XDP 相同的性能优势。</p>
<h3 id="xdp--ebpf-的优势"><a class="header" href="#xdp--ebpf-的优势">XDP + eBPF 的优势</a></h3>
<p>XDP 与 eBPF 结合提供了介于内核绕过方案(如 DPDK和内核集成方案之间的中间地带。以下是 XDP + eBPF 脱颖而出的原因:</p>
<ul>
<li>无法与内核中成熟的网络模块集成,而不得不在用户态将其重新实现;</li>
<li>破坏了内核的安全边界,使得内核提供的很多网络工具变得不可用;</li>
<li>在与常规的 socket 交互时,需要从用户态重新将包注入到内核;</li>
<li>需要占用一个或多个单独的 CPU 来进行包处理;</li>
<li>
<p><strong>高性能</strong>通过在网络接口卡NIC驱动程序级别拦截数据包XDP 可以实现接近线速的性能,用于丢弃、重定向或负载均衡数据包,同时保持低资源消耗。</p>
</li>
<li>
<p><strong>内核集成</strong>:与 DPDK 不同XDP 在 Linux 内核中工作,允许与现有的内核网络栈和工具(如 <code>iptables</code><code>nftables</code> 或套接字)无缝交互。无需在用户空间重新实现网络协议。</p>
</li>
<li>
<p><strong>安全性</strong>eBPF 虚拟机确保用户定义的 XDP 程序是被隔离的不会对内核造成不稳定影响。eBPF 的安全模型防止恶意或有缺陷的代码损害系统,提供了一个安全的可编程数据包处理环境。</p>
</li>
<li>
<p><strong>不需要专用 CPU</strong>XDP 允许数据包处理而无需将整个 CPU 核心专用于网络任务。这提高了系统的整体效率,允许更灵活的资源分配。</p>
</li>
</ul>
<p>总的来说XDP + eBPF 提供了一种强大的可编程数据包处理解决方案,结合了高性能与内核集成的灵活性和安全性。它消除了完全绕过内核方案的缺点,同时保留了内核安全性和功能的优势。</p>
<h2 id="xdp-的项目和应用案例"><a class="header" href="#xdp-的项目和应用案例">XDP 的项目和应用案例</a></h2>
<p>XDP 已经在许多高调的项目中得到应用,这些项目展示了它在实际网络场景中的强大功能和灵活性:</p>
<h3 id="1-cilium"><a class="header" href="#1-cilium">1. <strong>Cilium</strong></a></h3>
<ul>
<li><strong>描述</strong>Cilium 是一个为云原生环境(尤其是 Kubernetes设计的开源网络、安全和可观测性工具。它利用 XDP 实现高性能的数据包过滤和负载均衡。</li>
<li><strong>应用案例</strong>Cilium 将数据包过滤和安全策略卸载到 XDP实现高吞吐量和低延迟的容器化环境流量管理同时不牺牲可扩展性。</li>
<li><strong>链接</strong><a href="https://cilium.io/">Cilium</a></li>
</ul>
<h3 id="2-katran"><a class="header" href="#2-katran">2. <strong>Katran</strong></a></h3>
<ul>
<li><strong>描述</strong>Katran 是由 Facebook 开发的第 4 层负载均衡器,优化了高可扩展性和性能。它使用 XDP 处理数据包转发,开销极小。</li>
<li><strong>应用案例</strong>Katran 每秒处理数百万个数据包,高效地将流量分配到后端服务器上,利用 XDP 在大规模数据中心中实现低延迟和高性能的负载均衡。</li>
<li><strong>链接</strong><a href="https://github.com/facebookincubator/katran">Katran GitHub</a></li>
</ul>
<h3 id="3-cloudflare-的-xdp-ddos-保护"><a class="header" href="#3-cloudflare-的-xdp-ddos-保护">3. <strong>Cloudflare 的 XDP DDoS 保护</strong></a></h3>
<ul>
<li><strong>描述</strong>Cloudflare 已经实现了基于 XDP 的实时 DDoS 缓解。通过在 NIC 级别处理数据包Cloudflare 能够在恶意流量进入网络栈之前过滤掉攻击流量,最小化 DDoS 攻击对其系统的影响。</li>
<li><strong>应用案例</strong>Cloudflare 利用 XDP 在管道早期丢弃恶意数据包,保护其基础设施免受大规模 DDoS 攻击,同时保持对合法流量的高可用性。</li>
<li><strong>链接</strong><a href="https://blog.cloudflare.com/l4drop-xdp-ebpf-based-ddos-mitigations/">Cloudflare 博客关于 XDP</a></li>
</ul>
<p>这些项目展示了 XDP 在不同领域的可扩展和高效的数据包处理能力,从安全和负载均衡到云原生网络。</p>
<h3 id="为什么选择-xdp-而不是其他方法"><a class="header" href="#为什么选择-xdp-而不是其他方法">为什么选择 XDP 而不是其他方法?</a></h3>
<p>与传统方法(如 <code>iptables</code><code>nftables</code><code>tc</code>相比XDP 提供了几个明显的优势:</p>
<ul>
<li>
<p><strong>速度与低开销</strong>XDP 直接在 NIC 驱动程序中运行,绕过了内核的大部分开销,使数据包处理更快。</p>
</li>
<li>
<p><strong>可定制性</strong>XDP 允许开发人员通过 eBPF 创建自定义的数据包处理程序,提供比传统工具(如 <code>iptables</code>)更大的灵活性和细粒度控制。</p>
</li>
<li>
<p><strong>资源效率</strong>XDP 不需要像 DPDK 等用户空间解决方案那样将整个 CPU 核心专用于数据包处理,因此它是高性能网络的更高效选择。</p>
</li>
</ul>
<p>除此之外,利用内核模块和内核网络协议栈中的 hook 点也是一种思路,然而前者对内核的改动大,出错的代价高昂;后者在整套包处理流程中位置偏后,其效率不够理想。</p>
<p>总而言之xdp + eBPF 为可编程包处理系统提出了一种更为稳健的思路,在某种程度上权衡了上述方案的种种优点和不足,获取较高性能的同时又不会对内核的包处理流程进行过多的改变,同时借助 eBPF 虚拟机的优势将用户定义的包处理过程进行隔离和限制,提高了安全性。</p>
<h2 id="编写-ebpf-程序-3"><a class="header" href="#编写-ebpf-程序-3">编写 eBPF 程序</a></h2>
<pre><code class="language-C">#include "vmlinux.h"
#include &lt;bpf/bpf_helpers.h&gt;
@@ -4772,12 +4831,12 @@ Packing ebpf object and config into package.json...
<li><a href="http://arthurchiao.art/blog/linux-net-stack-implementation-rx-zh/">http://arthurchiao.art/blog/linux-net-stack-implementation-rx-zh/</a></li>
<li><a href="https://github.com/xdp-project/xdp-tutorial/tree/master/basic01-xdp-pass">https://github.com/xdp-project/xdp-tutorial/tree/master/basic01-xdp-pass</a></li>
</ul>
<div style="break-before: page; page-break-before: always;"></div><h1 id="在-andorid-上使用-ebpf-程序"><a class="header" href="#在-andorid-上使用-ebpf-程序">在 Andorid 上使用 eBPF 程序</a></h1>
<div style="break-before: page; page-break-before: always;"></div><h1 id="在-android-上使用-ebpf-程序"><a class="header" href="#在-android-上使用-ebpf-程序">在 Android 上使用 eBPF 程序</a></h1>
<blockquote>
<p>本文主要记录了笔者在 Android Studio Emulator 中测试高版本 Android Kernel 对基于 libbpf 的 CO-RE 技术支持程度的探索过程、结果和遇到的问题。
测试采用的方式是在 Android Shell 环境下构建 Debian 环境,并基于此尝试构建 eunomia-bpf 工具链、运行其测试用例。</p>
</blockquote>
<h2 id="背景-4"><a class="header" href="#背景-4">背景</a></h2>
<h2 id="背景-3"><a class="header" href="#背景-3">背景</a></h2>
<p>截至目前2023-04Android 还未对 eBPF 程序的动态加载做出较好的支持,无论是以 bcc 为代表的带编译器分发方案,还是基于 btf 和 libbpf 的 CO-RE 方案,都在较大程度上离不开 Linux 环境的支持,无法在 Android 系统上很好地运行<sup class="footnote-reference"><a href="#WeiShu">1</a></sup></p>
<p>虽然如此,在 Android 平台上尝试 eBPF 也已经有了一些成功案例,除谷歌官方提供的修改 <code>Android.bp</code> 以将 eBPF 程序随整个系统一同构建并挂载的方案<sup class="footnote-reference"><a href="#Google">2</a></sup>,也有人提出基于 Android 内核构建 Linux 环境进而运行 eBPF 工具链的思路,并开发了相关工具。</p>
<p>目前已有的资料,大多基于 adeb/eadb 在 Android 内核基础上构建 Linux 沙箱,并对 bcc 和 bpftrace 相关工具链进行测试,而对 CO-RE 方案的测试工作较少。在 Android 上使用 bcc 工具目前有较多参考资料,如:</p>

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff