<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0"
	xmlns:content="http://purl.org/rss/1.0/modules/content/"
	xmlns:wfw="http://wellformedweb.org/CommentAPI/"
	xmlns:dc="http://purl.org/dc/elements/1.1/"
	xmlns:atom="http://www.w3.org/2005/Atom"
	xmlns:sy="http://purl.org/rss/1.0/modules/syndication/"
	xmlns:slash="http://purl.org/rss/1.0/modules/slash/"
	>

<channel>
	<title>绿色记忆 &#187; eBPF</title>
	<atom:link href="https://blog.gmem.cc/tag/ebpf/feed" rel="self" type="application/rss+xml" />
	<link>https://blog.gmem.cc</link>
	<description></description>
	<lastBuildDate>Mon, 06 Apr 2026 12:46:48 +0000</lastBuildDate>
	<language>en-US</language>
		<sy:updatePeriod>hourly</sy:updatePeriod>
		<sy:updateFrequency>1</sy:updateFrequency>
	<generator>http://wordpress.org/?v=3.9.14</generator>
	<item>
		<title>eBPF学习笔记</title>
		<link>https://blog.gmem.cc/ebpf</link>
		<comments>https://blog.gmem.cc/ebpf#comments</comments>
		<pubDate>Fri, 22 Jan 2021 09:29:27 +0000</pubDate>
		<dc:creator><![CDATA[Alex]]></dc:creator>
				<category><![CDATA[C]]></category>
		<category><![CDATA[Linux]]></category>
		<category><![CDATA[eBPF]]></category>

		<guid isPermaLink="false">https://blog.gmem.cc/?p=37035</guid>
		<description><![CDATA[<p>简介 BPF，即Berkeley Packet Filter，是一个古老的网络封包过滤机制。它允许从用户空间注入一段简短的字节码到内核来定制封包处理逻辑。Linux从2.5开始移植了BPF，tcpdump就是基于BPF的应用。 所谓eBPF（extended BPF），则是从3.18引入的，对BPF的改造和功能增强： 使用类似于X86的体系结构，eBPF设计了一个通用的RISC指令集，支持11个64bit寄存器（32bit子寄存器）r0-r10，使用512字节的栈空间 引入了JIT编译，取代了BPF解释器。eBPF程序直接被编译为目标体系结构的机器码 和网络子系统进行了解耦。它的数据模型是通用的，eBPF程序可以挂钩到[crayon-69d54115477e6513131984-i/]或[crayon-69d54115477ed634351530-i/] 使用Maps来存储全局数据，这是一种通用的键值存储。可用作不同eBPF程序、eBPF和用户空间程序的状态共享 助手函数（Helper Functions），这些函数供eBPF程序调用，可以实现封包改写、Checksum计算、封包克隆等能力 尾调用（Tail Calls），可以用于将程序控制权从一个eBPF转移给另外一个。老版本的eBPF对程序长度有4096字节的限制，通过尾调用可以规避 用于Pin对象（Maps、eBPF程序）的伪文件系统 支持将eBPF Offload给智能硬件的基础设施 以上增强，让eBPF不仅仅限于网络封包处理，当前eBPF的应用领域包括： 网络封包处理：XDP、TC、socket progs、kcm、calico、cilium等 内核跟踪和性能监控：KProbes、UProbes、TracePoints 安全领域：Secomp、landlock等。例如阻止部分类型的系统调用 <a class="read-more" href="https://blog.gmem.cc/ebpf">[...]</a></p>
<p>The post <a rel="nofollow" href="https://blog.gmem.cc/ebpf">eBPF学习笔记</a> appeared first on <a rel="nofollow" href="https://blog.gmem.cc">绿色记忆</a>.</p>
]]></description>
				<content:encoded><![CDATA[<div class="wri_content_clear_both"><div class="blog_h1"><span class="graybg">简介</span></div>
<p>BPF，即Berkeley Packet Filter，是一个古老的网络封包过滤机制。它允许从用户空间<span style="background-color: #c0c0c0;">注入一段简短的字节码到内核</span>来定制封包处理逻辑。Linux从2.5开始移植了BPF，tcpdump就是基于BPF的应用。</p>
<p>所谓eBPF（extended BPF），则是从3.18引入的，对BPF的改造和功能增强：</p>
<ol>
<li>使用类似于X86的体系结构，eBPF设计了一个通用的RISC指令集，支持11个64bit寄存器（32bit子寄存器）r0-r10，使用512字节的栈空间</li>
<li><span style="background-color: #c0c0c0;">引入了JIT编译</span>，取代了BPF解释器。eBPF程序直接被编译为目标体系结构的机器码</li>
<li>和网络子系统进行了解耦。它的数据模型是通用的，eBPF程序可以挂钩到<pre class="crayon-plain-tag">Kprobe</pre>或<pre class="crayon-plain-tag">Tracepoint</pre></li>
<li>使用Maps来存储全局数据，这是一种通用的键值存储。可用作不同eBPF程序、eBPF和用户空间程序的<span style="background-color: #c0c0c0;">状态共享</span></li>
<li>助手函数（Helper Functions），这些函数供eBPF程序调用，可以实现封包改写、Checksum计算、封包克隆等能力</li>
<li>尾调用（Tail Calls），可以用于将程序控制权从一个eBPF转移给另外一个。老版本的eBPF对程序长度有4096字节的限制，通过尾调用可以规避</li>
<li>用于Pin对象（Maps、eBPF程序）的<span style="background-color: #c0c0c0;">伪文件系统</span></li>
<li>支持将eBPF Offload给智能硬件的基础设施</li>
</ol>
<p>以上增强，让eBPF不仅仅限于网络封包处理，当前eBPF的应用领域包括：</p>
<ol>
<li>网络封包处理：XDP、TC、socket progs、kcm、calico、cilium等</li>
<li>内核跟踪和性能监控：KProbes、UProbes、TracePoints</li>
<li>安全领域：Secomp、landlock等。例如阻止部分类型的系统调用</li>
</ol>
<p>现在BPF一般都是指eBPF，而老的BPF一般称为cBPF（classic BPF）。</p>
<p>性能是eBPF的另外一个优势，由于<span style="background-color: #c0c0c0;">所有代码都在内核空间运行，避免了复制数据到用户空间、上下文切换等开销</span>。甚至编译过程都在尽可能的优化，例如助手函数会被内联到eBPF程序中，避免函数调用的开销。</p>
<p>用户提供的代码在内核中运行，安全性需要得到保证。eBPF校验器会对字节码进行各方面的检查，确保它不会导致内核崩溃或锁死。</p>
<p>eBPF具有非常好的灵活性、动态性，可以随时的注入、卸载，不需要重启内核或者中断网络连接。</p>
<p>eBPF程序可以在不同体系结构之间移植。</p>
<div class="blog_h1"><span class="graybg">eBPF基础</span></div>
<div class="blog_h2"><span class="graybg">BPF架构</span></div>
<p><a href="https://blog.gmem.cc/wp-content/uploads/2021/05/ebpf-arch.jpg"><img class=" wp-image-37049 aligncenter" src="https://blog.gmem.cc/wp-content/uploads/2021/05/ebpf-arch.jpg" alt="ebpf-arch" width="995" height="543" /></a>如上图所示，eBPF应用程序，从开发到运行的典型流程如下：</p>
<ol>
<li>
<p>利用Clang，将C语言开发的代码编译为eBPF object文件</p>
</li>
<li>
<p>在用户空间将eBPF object文件载入内核。载入前，可能对object文件进行各种修改。这一步骤，可能通过iproute2之类的BPF ELF loader完成，也可能通过自定义的控制程序完成</p>
</li>
<li>
<p>BPF Verifier在VM中进行安全性校验</p>
</li>
<li>
<p>JIT编译器将字节码编译为机器码，返回BPF程序的文件描述符</p>
</li>
<li>使用文件描述符将BPF程序挂钩到某个子系统（例如networking）的挂钩点。子系统有可能将BPF程序offload给硬件（例如智能网卡）</li>
<li>
<p>用户空间通过eBPF Map和内核空间交换数据，获知eBPF程序的执行结果</p>
</li>
</ol>
<div class="blog_h2"><span class="graybg">挂钩点</span></div>
<p>eBPF程序以事件驱动的方式执行，具体来说，就是在<span style="background-color: #c0c0c0;">内核的代码路径上，存在大量挂钩点</span>（Hook Point）。eBPF程序会注册到某些挂钩点，当内核运行到挂钩点后，就执行eBPF程序。</p>
<p>挂钩点主要包括以下几类：</p>
<ol>
<li>网络事件，例如封包到达</li>
<li>Kprobes / Uprobes</li>
<li>系统调用</li>
<li>函数的入口/退出点</li>
</ol>
<div class="blog_h2"><span class="graybg">BPF Verifier</span></div>
<p>在加载之后，BPF校验器负责验证eBPF程序是否安全，它会模拟所有的执行路径，并且：</p>
<ol>
<li>检查程序控制流，发现循环</li>
<li>检测越界的跳转、不可达指令</li>
<li>跟踪Context的访问、栈移除</li>
<li>检查unpriviledged的指针泄漏</li>
<li>检查助手函数调用参数</li>
</ol>
<div class="blog_h2"><span class="graybg">BPF JITs</span></div>
<p>在校验之后，eBPF程序被JIT编译器编译为Native代码。</p>
<div class="blog_h2"><span class="graybg">BPF Maps</span></div>
<p>键值对形式的存储，通过文件描述符来定位，值是不透明的Blob（任意数据）。用于跨越多次调用共享数据，或者与用户空间应用程序共享数据。</p>
<p>一个eBPF程序可以<span style="background-color: #c0c0c0;">直接访问最多64个Map</span>，多个eBPF程序可以共享同一Map。</p>
<div class="blog_h2"><span class="graybg">Pinning</span></div>
<p>BPF Maps和程序都是内核资源，仅能通过文件描述符访问到。文件描述符对应了内核中的匿名inodes。</p>
<p>用户空间程序可以使用大部分基于文件描述符的API，但是<span style="background-color: #c0c0c0;">文件描述符是限制在进程的生命周期内的</span>，这导致Map难以被共享。比较显著的例子是iproute2，当tc或XDP加载eBPF程序之后，自身会立刻退出。这导致无法从用户空间访问Map。</p>
<p>为了解决上面的问题，引入了一个最小化的、<span style="background-color: #c0c0c0;">内核空间中的BPF文件系统。BPF程序和Map会被pin到一个被称为object pinning的进程</span>。bpf系统调用有两个命令BPF_OBJ_PIN、BPF_OBJ_GET分别用于钉住、取回对象。</p>
<p>tc这样的工具就是利用Pinning在ingress/egress端共享Map。</p>
<div class="blog_h2"><span class="graybg">尾调用</span></div>
<p>尾调用允许一个BPF程序调用另外一个，这种调用<span style="background-color: #c0c0c0;">没有函数调用那样的开销</span>。其实现方式是long jump，重用当前stack frame。</p>
<p>注意：只用相同类型的BPF程序才能相互尾调用。</p>
<p>要使用尾调用，需要一个BPF_MAP_TYPE_PROG_ARRAY类型的Map，其内容目前必须由用户空间产生，值是需要被尾调用的BPF程序的文件描述符。通过助手函数bpf_tail_call触发尾调用，内核会将此调用内联到一个特殊的BPF指令。</p>
<div class="blog_h2"><span class="graybg">BPF-BPF调用</span></div>
<p>BPF - BPF调用是一个新添加的特性。在此特性引入之前，典型的BPF C程序需要将所有可重用的代码声明为always_inline的，这样才能确保LLVM生成的object包含所有函数。这会导致函数在每个object文件中都反复（只要它被调用超过一次）出现，增加体积。</p>
<pre class="crayon-plain-tag">#include &lt;linux/bpf.h&gt;

#ifndef __section
# define __section(NAME)                  \
   __attribute__((section(NAME), used))
#endif

#ifndef __inline
# define __inline                         \
   inline __attribute__((always_inline))
#endif

// 总是内联
static __inline int foo(void)
{
    return XDP_DROP;
}

__section("prog")
int xdp_drop(struct xdp_md *ctx)
{
    return foo();
}

char __license[] __section("license") = "GPL";</pre>
<p>总是需要内联的原因是BPF的Loader/Verifier/Interpreter/JITs不支持函数调用。但是从<span style="background-color: #c0c0c0;">内核4.16和LLVM 6.0开始，此限制消除</span>，BPF程序不再总是需要always_inline。上面程序的__inline可以去掉了。</p>
<p>目前x86_64/arm64的JIT编译器支持BPF to BPF调用，这是很重要的性能优化，因为它大大简化了生成的object文件的尺寸，对CPU指令缓存更加友好。</p>
<p>JIT编译器<span style="background-color: #c0c0c0;">为每个函数生成独立的映像</span>（Image），并且<span style="background-color: #c0c0c0;">在JIT的最后一个步骤中修复映像中的函数调用地址</span>。</p>
<p>到5.9为止，你不能同时使用BPF-BPF调用（BPF子程序）和尾调用。从5.10开始，可以混合使用，但是仍然存在一些限制。此外，<span style="background-color: #c0c0c0;">混合使用两者可能导致内核栈溢出</span>，原因是尾调用在跳转之前仅会unwind当前栈帧。</p>
<div class="blog_h2"><span class="graybg">Offloading</span></div>
<p>BPF网络程序，特别是tc和XDP，提供了将BPF代码offload给NIC执行的特性。这个特性需要驱动的支持。</p>
<div class="blog_h2"><span class="graybg">BPF前端工具</span></div>
<p>能够加载BPF程序的前端工具有很多，包括bcc、perf、iproute2等。内核也在tools/lib/bpf目录下提供了用户空间库，被perf用来加载BPF追踪应用程序到内核。这是一个通用的库，你也可以直接调用它。BCC是面向追踪的工具箱。内核在samples/bpf下也提供了一些BPF示例，这些示例解析Object文件，并且直接通过系统调用将其载入内核。</p>
<p>基于不同前端工具，实现BPF程序的语法、语义（例如对于段名的约定）有所不同。</p>
<div class="blog_h2"><span class="graybg">相关sysctl</span></div>
<div class="blog_h3"><span class="graybg">/proc/sys/net/core/bpf_jit_enable</span></div>
<p>启用或禁用BPF JIT编译器：</p>
<p style="padding-left: 30px;">0 仅用，仅仅使用解释器，默认值<br />1 启用JIT编译器<br />2 启用JIT编译器并且生成debugging trace到内核日志</p>
<p>设置为2，可以使用bpf_jit_disasm处理debugging trace</p>
<div class="blog_h3"><span class="graybg">/proc/sys/net/core/bpf_jit_harden</span></div>
<p>启用或禁用JIT加固，加固和性能是对立的，但是可以缓和JIT spraying：</p>
<p style="padding-left: 30px;">0 禁用JIT加固，默认值<br />1 对非特权用户启用<br />2 对所有用户启用</p>
<div class="blog_h3"><span class="graybg">/proc/sys/net/core/bpf_jit_kallsyms</span></div>
<p>启用或禁用JITed的程序的内核符号导出（导出到/proc/kallsyms），这样可以和perf工具一起使用，还能够让内核对BPF程序的地址感知（用于stack unwinding）：</p>
<p style="padding-left: 30px;">0 启用<br />1 仅对特权用户启用</p>
<div class="blog_h3"><span class="graybg">/proc/sys/kernel/unprivileged_bpf_disabled</span></div>
<p>是否启用非特权的bpf系统调用。默认启用，一旦禁用，重启前无法恢复启用状态。不会影响seccomp等不使用bpf2系统调用的cBPF程序：</p>
<p style="padding-left: 30px;">0 启用<br />1 禁用</p>
<div class="blog_h2"><span class="graybg">助手函数</span></div>
<p>eBPF程序可以调用助手函数，完成各种任务，例如：</p>
<ol>
<li>在Map中搜索、更新、删除键值对</li>
<li>生成伪随机数</li>
<li>读写隧道元数据</li>
<li>尾调用 —— 将eBPF程序链在一起</li>
<li>执行套接字相关操作，例如绑定、查询Cookies、重定向封包</li>
<li>打印调试信息</li>
<li>获取系统启动到现在的时间</li>
</ol>
<p>助手函数是定义在内核中的，有一个白名单，决定哪些内核函数可以被eBPF程序调用。</p>
<p>根据eBPF的约定，<span style="background-color: #c0c0c0;">助手函数的参数数量不超过5</span>。</p>
<p>编译后，助手函数的代码是内联到eBPF程序中的，因而不存在函数调用的开销（栈帧处理开销、CPU流水线预取指令失效开销）。</p>
<p>返回int的类型的助手函数，通常操作成功返回0，否则返回负数。如果不是如此，会特别说明。</p>
<p>助手函数不可以随意调用，不同类型的eBPF程序，可以调用不同的助手函数子集。</p>
<div class="blog_h1"><span class="graybg">iproute2</span></div>
<p>iproute2提供的BPF前端，主要用来载入BPF网络程序，这些程序的类型包括XDP、tc、lwt。只要是为iproute2编写的BPF程序，共享统一的加载逻辑。</p>
<div class="blog_h2"><span class="graybg">XDP</span></div>
<div class="blog_h3"><span class="graybg">加载XDP程序</span></div>
<p>编译好的XDP类型（BPF_PROG_TYPE_XDP）的BPF程序 ，可以使用如下命令载入到支持XDP的网络设备：</p>
<pre class="crayon-plain-tag">ip link set dev eth0 xdp obj prog.o</pre>
<p>上述命令假设程序位于名为prog的段中。如果不使用默认段名，则需要指定sec参数：</p>
<pre class="crayon-plain-tag"># 如果程序放在foobar段
ip link set dev em1 xdp obj prog.o sec foobar</pre>
<p>如果程序没有标注段，也就是位于默认的.text段，则也可以用上面的命令加载。</p>
<p>如果已经存在挂钩到网络设备的XDP程序，默认情况下命令会报错，可以用-force参数强制替换：</p>
<pre class="crayon-plain-tag">ip -force link set dev em1 xdp obj prog.o</pre>
<p>大多数支持XDP的驱动，能够原子的替换XDP程序，而不会引起流量中断。出于性能的考虑同时只能有一个XDP程序挂钩， 可以利用前文提到的尾调用来组织多个XDP程序。</p>
<p>如果网络设备挂钩了XDP程序，则ip link命令会显示xdp标记和程序的ID。使用bpftool传入ID可以查看更多细节信息。</p>
<div class="blog_h3"><span class="graybg">卸载XDP程序</span></div>
<pre class="crayon-plain-tag">ip link set dev eth0 xdp off</pre>
<div class="blog_h3"><span class="graybg">XDP操作模式</span></div>
<p>iproute2实现了XDP所支持的三种操作模式：</p>
<ol>
<li>xdpdrv：即native XDP，<span style="background-color: #c0c0c0;">BPF程序在驱动的接收路径的最早时刻被调用</span>。这是正常的XDP模式，上游内核的所有主要10G/40G+网络驱动（包括virtio）都实现了XDP支持，也就是可使用该模式</li>
<li>xdpoffload：由智能网卡的驱动（例如Netronome的nfp驱动）实现，将整个XDP程序offload到硬件中，网卡每接收到封包都会执行XDP程序。该模式比native XDP的性能更高，缺点是，并非所有助手函数、Map类型可用。</li>
<li>xdpgeneric：即generic XDP，作为尚不支持native XDP的驱动的试验台。挂钩点比native XDP晚很多，已经进入网络栈的主接收路径，生成了skb对象，因此性能比native XDP差很多，不会用于生产环境</li>
</ol>
<p>在切换驱动的XDP模式时，驱动通常需要重新配置它的接收/发送Rings，以保证接收到的封包线性的（linearly）存放到单个内存页中。</p>
<p>调用ip link set dev xxx xdp命令时，内核会首先尝试在native XDP模式下载入，如果驱动不支持，则自动使用generic XDP模式。要强制使用native XDP，则可以使用：</p>
<pre class="crayon-plain-tag">#                           强制使用native XDP
ip -force link set dev eth0 xdpdrv obj prog.o</pre>
<p>使用类似的方式可以强制使用xdpgeneric、xdpoffload。</p>
<p><span style="background-color: #c0c0c0;">切换操作模式目前不能原子的进行，但是在单个操作模式下替换XDP程序则可以</span>。</p>
<p>使用<pre class="crayon-plain-tag">verb</pre>选项，可以显示详尽的BPF校验日志：</p>
<pre class="crayon-plain-tag">ip link set dev eth0 xdp obj xdp-example.o verb</pre>
<p>除了从文件加载BPF程序，也可以直接从BPF伪文件系统中得到程序并使用：</p>
<pre class="crayon-plain-tag">ip link set dev eth0 xdp pinned /sys/fs/bpf/prog
#                               m:表示BPF文件系统的挂载点，默认/sys/fs/bpf/
ip link set dev eth0 xdp pinned m:prog</pre>
<div class="blog_h2"><span class="graybg">tc</span></div>
<p>对于为tc设计的BPF程序（BPF_PROG_TYPE_SCHED_CLS、BPF_PROG_TYPE_SCHED_ACT），可以使用tc命令加载并挂钩到网络设备。和XDP不同，tc程序没有对驱动的依赖。</p>
<p><pre class="crayon-plain-tag">clsact</pre>是4.1引入了一个特殊的dummy qdisc，它持有classifier和action，但是不能执行实际的queueing。要挂钩BPF classifier，clsact是必须启用的：</p>
<pre class="crayon-plain-tag">tc qdisc add dev eth0 clsact</pre>
<p>clsact提供了两个特殊的钩子<pre class="crayon-plain-tag">ingress</pre>、<pre class="crayon-plain-tag">egress</pre>，对应了BPF classifier可用的两个挂钩点。这两个钩子位于网络数据路径的中心位置，任何封包都必须经过</p>
<p>下面的命令，将BPF程序挂钩到eth0的ingress路径上：</p>
<pre class="crayon-plain-tag">tc filter add dev eth0 ingress bpf da obj prog.o</pre>
<p>下面的命令将BPF程序挂钩到eth0的egress路径上：</p>
<pre class="crayon-plain-tag">tc filter add dev eth0 egress bpf da obj prog.o </pre>
<p>ingress钩子在内核中由<pre class="crayon-plain-tag">__netif_receive_skb_core() -&gt; sch_handle_ingress()</pre>调用。</p>
<p>egress钩子在内核中由<pre class="crayon-plain-tag">__dev_queue_xmit() -&gt; sch_handle_egress()</pre>调用。</p>
<p><span style="background-color: #c0c0c0;">clsact是以无锁方式处理的</span>，<span style="background-color: #c0c0c0;">支持挂钩到虚拟的、没有队列概念的网络设备，例如veth</span>。</p>
<p><pre class="crayon-plain-tag">da</pre>即direct-action模式，这是推荐的模式，应当总是在命令中指定。da模式表示BPF classifier不需要调用外部的tc action模块，因为BPF程序会将封包修改、转发或者其它动作都完成，这正是BPF性能优势所在。</p>
<p>类似XDP，如果不使用默认的段名，需要用sec选项：</p>
<pre class="crayon-plain-tag">tc filter add dev eth0 egress bpf da obj prog.o sec foobar</pre>
<p>已经挂钩到设备的tc程序的列表，可以用下面的命令查看：</p>
<pre class="crayon-plain-tag">tc filter show dev em1 ingress
filter protocol all pref 49152 bpf

     # 针对的L3协议    优先级      分类器类型  分类器句柄
filter protocol all    pref 49152 bpf        handle 0x1 
  # 从prog.o的ingress段加载了程序
  prog.o:[ingress] 
  # BPF程序运行在da模式
  direct-action 
  # 程序ID是全局范围唯一的BPF程序标识符，可以被bpftool使用
  id 1 
  # 程序指令流的哈希，哈希可以用来关联到Object文件，perf报告栈追踪的时候使用此哈希
  tag c5f7825e5dac396f


tc filter show dev em1 egress
filter protocol all pref 49152 bpf
filter protocol all pref 49152 bpf handle 0x1 prog.o:[egress] direct-action id 2 tag b2fd5adc0f262714</pre>
<p>tc可以挂钩多个BPF程序，这和XDP不同，它提供了多个其它的、可以链接在一起的classifier。尽管如此，单个da模式的BPF程序可以满足所有封包操作需求，它可以直接返回action断言，例如<pre class="crayon-plain-tag">TC_ACT_OK</pre>, <pre class="crayon-plain-tag">TC_ACT_SHOT</pre>。使用单个BPF程序是推荐的用法。</p>
<p>除非打算自动替换挂钩的BPF程序，建议初次挂钩时明确的指定pref和handle，这样，在后续手工替换的时候就不需要查询获取pref、handle：</p>
<pre class="crayon-plain-tag">tc filter add dev eth0 ingress pref 1 handle 1 bpf da obj prog.o sec foobar</pre>
<p>使用下面的命令原子的替换BPF程序：</p>
<pre class="crayon-plain-tag">tc filter replace dev eth0 ingress pref 1 handle 1 bpf da obj prog.o sec foobar</pre>
<p>要移除所有以及挂钩的BPF程序，执行：</p>
<pre class="crayon-plain-tag">tc filter del dev eth0 ingress
tc filter del dev eth0 egress</pre>
<p>要从网络设备上移除整个clsact qdisc，可以：</p>
<pre class="crayon-plain-tag">tc qdisc del dev eth0 clsact</pre>
<p>类似于XDP程序，tc程序也支持offload给职能网卡。你需要首先启用hw-tc-offload：</p>
<pre class="crayon-plain-tag">ethtool -K eth0 hw-tc-offload on</pre>
<p>然后再启用clsact并挂钩BPF程序。XDP和tc的offloading不能同时开启。</p>
<div class="blog_h2"><span class="graybg">netdevsim</span></div>
<p>内核提供了一个dummy驱动netdevsim，它实现了XDP/tc BPF的offloading接口，用于测试目的。</p>
<p>下面的命令可以启用netdevsim设备：</p>
<pre class="crayon-plain-tag">modprobe netdevsim
echo "1 1" &gt; /sys/bus/netdevsim/new_device
devlink dev
# netdevsim/netdevsim1
devlink port
# netdevsim/netdevsim1/0: type eth netdev eth0 flavour physical
ip l
# 4: eth0: &lt;BROADCAST,NOARP,UP,LOWER_UP&gt; mtu 1500 qdisc noqueue state UNKNOWN mode DEFAULT group default qlen 1000
#     link/ether 2a:d5:cd:08:d1:3f brd ff:ff:ff:ff:ff:ff</pre>
<div class="blog_h1"><span class="graybg">XDP</span></div>
<div class="blog_h2"><span class="graybg">简介</span></div>
<p>在网络封包处理方面，出现过一种提升性能的技术 —— 内核旁路（Kernel Bypass ）：完全在用户空间实现网络驱动和整个网络栈，避免上下文切换、内核网络层次、中断处理。具体实现包括Intel的DPDK （Data Plane Development Kit）、Cisco的VPP等。</p>
<p>内核旁路技术的缺点是：</p>
<ol>
<li>作为硬件资源的抽象层，内核是经过良好测试和验证的。在用户空间重新实现驱动，稳定性、可复用性欠佳</li>
<li>实现网络栈也是困难的</li>
<li>作为一个沙盒，网络处理程序难以和内核其它部分集成/交互</li>
<li>无法使用内核提供的安全层</li>
</ol>
<p><span style="background-color: #c0c0c0;">eXpress Data Path，为内核提供了一个基于eBPF的、高性能的、可编程的、运行在驱动层的封包处理框架</span>，它提升性能的思路和内核旁路技术相反 —— 完全在内核空间实现封包处理逻辑，例如过滤、映射、路由等。XDP通过在网络接收路径的最早期挂钩eBPF程序来实现高速封包过滤。最早期意味着：NIC驱动刚刚从receiver rings接收到封包，任何高成本操作，例如分配skb并将封包推入网络栈，尚未进行。</p>
<p>XDP的起源来自于对DDoS攻击的防范。Cloudflare依赖（leverages heavily on）iptables进行封包过滤，在配置相当好的服务器上，可以处理1Mpps的流量。但是当出现DDoS攻击时，流量会高达3Mpps，这会导致Linux系统overflooded by IRQ请求，直到系统变得不稳定。</p>
<p>由于Cloudflare希望继续使用iptables以及其它内核网络栈功能，它不考虑使用DPDK这样的完全控制硬件的方案，而是使用了所谓部分内核旁路（partial kernel bypass），NIC的一部分队列继续附到内核，另外一部分队列则附到一个用户空间应用程序，此程序决定封包是否应该被丢弃。通过在网络栈的最底部就决定是否应该丢弃封包，需要经由内核网络子系统的封包数量大大减少了。</p>
<p><a href="https://blog.cloudflare.com/single-rx-queue-kernel-bypass-with-netmap/">Cloudflare利用了Netmap工具包</a>实现部分内核旁路。但是这个思路可以延伸为，在内核网络栈中增加一个Checkpoint，这个点应该离NIC接收到封包的时刻尽可能的近。这个Checkpoint将把封包交给用户编写的程序，决定是应该丢弃，还是继续正常处理路径。</p>
<p><a href="https://blog.gmem.cc/wp-content/uploads/2021/05/xdp-packet-processing.png"><img class="wp-image-37127 aligncenter" src="https://blog.gmem.cc/wp-content/uploads/2021/05/xdp-packet-processing.png" alt="xdp-packet-processing" width="1068" height="584" /></a></p>
<p>XDP对应的BPF程序类型是：BPF_PROG_TYPE_XDP。XDP程序可以读写封包，调用助手函数解析封包、计算Checksum，这些操作都不会牵涉系统调用的开销（都在内核空间执行）。</p>
<p>尽管XDP的基本用途是，尽早的决定封包是否应该丢弃。但是，由于网络函数无非是读、写、转发、丢弃等原语的组合，XDP可以用来实现任何网络功能。</p>
<p>XDP的主要优势包括：</p>
<ol>
<li>可以使用各种内核基础设施，例如路由表、套接字、网络栈</li>
<li>运行在内核中，使用和内核其它部分一致的安全模型</li>
<li>运行在内核中，不需要跨越用户/内核空间边界，能够灵活的转发封包给其它内核实体，例如命名空间、网络栈</li>
<li>支持动态替换XDP程序，不会引起网络中断</li>
<li>保证封包的线性（linearly）布局，封包位于单个DMAed内存页中，访问起来很方便</li>
<li>保证封包有256字节可用的额外headroom，可以用于（使用助手函数<pre class="crayon-plain-tag">bpf_xdp_adjust_head</pre>、<pre class="crayon-plain-tag">bpf_xdp_adjust_meta</pre>）添加自定义的封装包头</li>
</ol>
<p>从内核4.8+开始，主要发行版中XDP可用，大部分10G+网络驱动支持XDP。</p>
<div class="blog_h2"><span class="graybg">应用场景</span></div>
<div class="blog_h3"><span class="graybg">DDoS缓解</span></div>
<p>XDP的高性能特征，让它非常适合实现DDoS攻击缓解，以及一般性防火墙。</p>
<div class="blog_h3"><span class="graybg">封包转发</span></div>
<p>BPF程序可以对封包进行任意的修改，甚至是通过助手函数任意的增减headroom大小实现封装/解封装。</p>
<p>处理完的封包通过XDP_REDIRECT动作即可转发封包给其它NIC，或者转发给其它CPU（利用BPF的cpumap）</p>
<div class="blog_h3"><span class="graybg">负载均衡</span></div>
<p>使用XDP_TX动作，hairpinned LB可以将修改后的封包从接收它的网卡发送回去。</p>
<div class="blog_h3"><span class="graybg">流量采样和监控</span></div>
<p>XDP支持将部分或截断的封包内容存放到无锁的per-CPU的内存映射ring buffer中。此ring buffer由Linux perf基础设施提供，可以被用户空间访问。</p>
<div class="blog_h2"><span class="graybg">编程接口</span></div>
<div class="blog_h3"><span class="graybg">xdp_buff</span></div>
<p>在XDP中，代表当前封包的结构是：</p>
<pre class="crayon-plain-tag">struct xdp_buff {
    // 内存页中，封包数据的开始点指针
    void *data;
    // 内存页中，封包数据的结束点指针
    void *data_end;
    // 最初和和data指向同一位置。后续可以被bpf_xdp_adjust_meta()调整，向data_hard_start方向移动
    // 可以用于为元数据提供空间。这种元数据对于正常的内核网络栈是不可见的，但是能够被tc BPF程序读取，
    // 因为元数据会从XDP传送到skb中
    // data_meta可以仅仅适用于在尾调用之间传递信息，类似于可被tc访问的skb-&gt;cb[]
    void *data_meta;
    // XDP支持headroom，这个字段给出页中，此封包可以使用的，最小的地址
    // 如果封包被封装，则需要调用bpf_xdp_adjust_head()，将data向data_hard_start方向移动
    // 解封装时，也可以使用bpf_xdp_adjust_head()移动指针
    void *data_hard_start;
    // 提供一些额外的per receive queue元数据，这些元数据在ring setup time生成
    struct xdp_rxq_info *rxq;
};

// 接收队列信息
struct xdp_rxq_info {
    struct net_device *dev;
    u32 queue_index;
    u32 reg_state;
} ____cacheline_aligned; // 缓存线（默认一般是64KB），CPU以缓存线为单位读取内存到CPU高速缓存</pre>
<p>它通过BPF context传递给XDP程序。</p>
<div class="blog_h3"><span class="graybg">xdp_action</span></div>
<pre class="crayon-plain-tag">enum xdp_action {
    // 提示BPF出现错误，和DROP的区别仅仅是会发送一个trace_xdp_exception追踪点
    XDP_ABORTED = 0,
    // 应当在驱动层丢弃封包，不必再浪费额外资源。对于DDos缓和、一般性防火墙很有用
    XDP_DROP,
    // 允许封包通过，进入网络栈进行常规处理
    // 处理此封包的CPU后续将分配skb，将封包信息填充进去，然后传递给GRO引擎
    XDP_PASS,
    // 将封包从接收到的网络接口发送回去，可用于实现hairpinned LB
    XDP_TX,
    // 重定向封包给另外一个NIC
    XDP_REDIRECT,
};</pre>
<p>这个枚举是XDP程序需要返回的断言，告知驱动应该如何处理封包。</p>
<div class="blog_h1"><span class="graybg"><a id="tc"></a>tc</span></div>
<p>关于tc的基础知识，参考<a href="/tc">基于tc的网络QoS管理</a>。</p>
<div class="blog_h2"><span class="graybg">tc程序简介</span></div>
<p>BPF可以和内核的tc层一起工作。tc程序和XDP程序有以下不同：</p>
<ol>
<li>tc程序的BPF输入上下文是skb_buff，而非xdp_buff。在XDP之后，内核会解析封包，存入skb_buff。解析的开销导致tc程序的性能远低于XDP，但是，tc程序可以访问skb的mark, pkt_type, protocol, priority, queue_mapping, napi_id, cb[]数组, hash, tc_classid, tc_index等字段，以及VLAN元数据、XDP传送来的自定义元数据。BPF上下文<pre class="crayon-plain-tag">struct __sk_buff</pre>定义在<pre class="crayon-plain-tag">linux/bpf.h</pre></li>
<li>tc程序可以挂钩到ingress/egress网络路径上，XDP则仅仅能挂钩到ingress路径</li>
<li>tc程序对驱动层没有依赖，可以挂钩到任何类型的网络设备。除非启用tc BPF程序的offloading</li>
</ol>
<p>尽管tc程序的挂钩点没有XDP那么早，但是仍然是<span style="background-color: #c0c0c0;">在内核网络路径的早期。它在GRO运行之后，任何协议处理之前执行</span>。iptables PREROUTING、nftables ingress hook等工具也在相同的位置挂钩。</p>
<div class="blog_h2"><span class="graybg">tc程序工作方式</span></div>
<p>工作在tc层的BPF程序，是从一个名为<pre class="crayon-plain-tag">cls_bpf</pre>的过滤器运行的。tc程序<span style="background-color: #c0c0c0;">不但可以读取skb的元数据、封包内容，还能够对封包进行任意修改，甚至使用action verdict终止tc处理过程</span>。</p>
<p>过滤器<span style="background-color: #c0c0c0;">cls_bpf可以挂钩1-N个BPF程序</span>，当有多个BPF程序情况下，前面的程序返回verdict <pre class="crayon-plain-tag">TC_ACT_UNSPEC</pre>会导致继续执行后面的BPF程序。使用多个BPF程序的缺点是，需要反复解析封包，导致性能降低。</p>
<p>cls_bpf有一个direct-action（da）模式，这样BPF程序能够直接返回action verdict，决定封包命运，结束tc处理流水线。</p>
<p>tc BPF程序也支持在运行时动态更新，而不会中断任何网络流量。</p>
<p>cls_bpf可以挂钩到的ingress/egress钩子，均被一个伪（不在qdisc树形结构中）排队规则<pre class="crayon-plain-tag">sch_clsact</pre>管理。对于ingress qdisc来说，sche_clsact可以作为一个drop-in的替代品。对于在<pre class="crayon-plain-tag">__dev_queue_xmit()</pre>中执行的egress钩子，需要强调，sche_clsact不在内核的root qdisc锁下运行。因此，<span style="background-color: #c0c0c0;">不管是ingress/egress，使用sche_clsact时tc BPF程序都是以无锁方式执行的，这和典型的qdisc完全不同</span>。此外需要注意，<span style="background-color: #c0c0c0;">sch_clsact执行期间不会发生抢占</span>。</p>
<p>典型情况下，egress方向会有附到网络设备的qdisc，例如sch_htb、sch_fq，它们其中有些是classful qdisc。classful qdisc会通过<pre class="crayon-plain-tag">tcf_classify()</pre>调用分类器，cls_bpf也可以被挂到这种qdisc上。这时，BPF程序在root qdisc下运行，可能面临锁争用问题。</p>
<p>为了达到最大性能（减少锁争用），可以考虑这样的用法：使用sch_clsact + cls_bpf，在root qdisc锁之外，完成任务繁重的封包分类工作，并且设置skb-&gt;mark或skb-&gt;priority。然后，由运行在root qdisc锁下的sch_htb快速的根据skb字段完成分类、塑形操作。</p>
<p>sch_clsact + cls_bpf组合使用时，如果cls_bpf是da模式、只包含单个BPF程序、且位于ingress网络路径，则支持offload给智能网卡。</p>
<div class="blog_h2"><span class="graybg">编程接口</span></div>
<div class="blog_h3"><span class="graybg">__sk_buff</span></div>
<p>在tc BPF程序中，代表当前封包的结构是<pre class="crayon-plain-tag">__sk_buff</pre>，这种结构叫UAPI（user space API of the kernel），可以访问内核<pre class="crayon-plain-tag">sk_buff</pre>结构的某些字段。</p>
<pre class="crayon-plain-tag">struct __sk_buff {
	__u32 len;
	__u32 pkt_type;
	__u32 mark;
	__u32 queue_mapping;
	__u32 protocol;
	__u32 vlan_present;
	__u32 vlan_tci;
	__u32 vlan_proto;
	__u32 priority;
	__u32 ingress_ifindex;
	__u32 ifindex;
	__u32 tc_index;
	__u32 cb[5];
	__u32 hash;
	__u32 tc_classid;
	__u32 data;
	__u32 data_end;
	__u32 napi_id;

	/* Accessed by BPF_PROG_TYPE_sk_skb types from here to ... */
	__u32 family;
	__u32 remote_ip4;	/* Stored in network byte order */
	__u32 local_ip4;	/* Stored in network byte order */
	__u32 remote_ip6[4];	/* Stored in network byte order */
	__u32 local_ip6[4];	/* Stored in network byte order */
	__u32 remote_port;	/* Stored in network byte order */
	__u32 local_port;	/* stored in host byte order */
	/* ... here. */

	__u32 data_meta;
	__bpf_md_ptr(struct bpf_flow_keys *, flow_keys);
	__u64 tstamp;
	__u32 wire_len;
	__u32 gso_segs;
	__bpf_md_ptr(struct bpf_sock *, sk);
};</pre>
<div class="blog_h3"><span class="graybg">verdicts</span></div>
<p>tc ingress/egress钩子能够返回的verdict定义在：</p>
<pre class="crayon-plain-tag">// 未指定，如果有多个BPF程序，会继续运行下一个。如果没有更多BPF程序
// 则提示内核在没有任何side-effect的情况下继续处理skb
#define TC_ACT_UNSPEC         (-1)
// 从tc BPF程序角度，TC_ACT_OK、TC_ACT_RECLASSIFY等价
#define TC_ACT_OK               0
// 提示内核丢弃封包，在ingress方向，网络栈上层无法看到封包；在egress方向，封包不会被发出
#define TC_ACT_SHOT             2
// 从tc BPF程序角度，TC_ACT_STOLEN、TC_ACT_QUEUED、TC_ACT_TRAP等价
// 类似于TC_ACT_SHOT，区别：
//   TC_ACT_SHOT导致内核通过kfree_skb()释放封包并返回NET_XMIT_DROP作为即时的反馈
//   TC_ACT_STOLEN导致内核通过consume_skb()释放封包，并且返回NET_XMIT_SUCCESS，
//      效果是上层以为封包是成功发送的
#define TC_ACT_STOLEN           4
// 利用助手函数bpf_redirect()，重定向封包到相同/不同设备的ingress/egress路径
#define TC_ACT_REDIRECT         7 </pre>
<div class="blog_h2"><span class="graybg">应用场景</span></div>
<div class="blog_h3"><span class="graybg">容器网络策略</span></div>
<p>对于容器来说，容器网络命名空间和初始网络命名空间通过一对veth连接。我们可以在宿主机端实现网络策略：</p>
<ol>
<li>主机侧的egress，对应容器的ingress</li>
<li>主机侧的ingress，对应容器的egress</li>
</ol>
<p>将tc BPF程序挂钩到宿主机veth的egress/ingress钩子即可。</p>
<p>对于veth这样的虚拟设备，XDP是不适合的。因为内核在虚拟设备这里，单纯在一个skb上操作，<span style="background-color: #c0c0c0;">XDP由于一些限制，无法和克隆的skb一起工作</span>。克隆skb在内核TCP/IP栈被大量使用，用来存放重传的数据段，这里XDP钩子会被直接跳过。此外，XDP需要线性化（放到单个页）整个skb，这也导致严重的性能影响。</p>
<div class="blog_h3"><span class="graybg">转发和负载均衡</span></div>
<p>容器工作负载的东西向流量是主要的目标场景。</p>
<p>不像XDP仅作用在ingress上，tc BPF可以在某些场景下，应用到容器egress方向。在对容器透明的前提下，可以利用BPF在egress进行NAT和LB处理，利用bpf_redirection助手函数，<span style="background-color: #c0c0c0;">BPF可以将封包转到任何接口的ingress/egress路径，不需要网桥之类的设备辅助</span>。</p>
<div class="blog_h3"><span class="graybg">流采样和监控</span></div>
<p>类似XDP，流采样和监控可以通过高性能、无锁的per-CPU内存映射的perf ring buffer实现，依此tc BPF程序可以调用助手函数<pre class="crayon-plain-tag">bpf_skb_event_output()</pre>推送定制数据、完整/截断的封包内容到用户空间。</p>
<p>由于tc BPF程序可以同时挂到ingress/egress，因此可以为任何节点实现双向的监控。</p>
<p>BPF程序可以预先做一些聚合，而不是把所有东西推送到用户空间。</p>
<div class="blog_h3"><span class="graybg">预处理封包调度</span></div>
<p>如上文所提到，sch_clsact的egress钩子，即<pre class="crayon-plain-tag">sch_handle_egress()</pre>，在获取内核qdisc root锁之前运行。这种无锁的特征让tc BPF程序适合执行较重（耗时）的分类任务，并将分类结果设置到skb的某些字段，在后续交由常规的、有锁的qdisc进行塑形和重排。</p>
<div class="blog_h1"><span class="graybg">开发环境</span></div>
<div class="blog_h2"><span class="graybg">内核和工具</span></div>
<p>本节介绍如何创建完整的BPF开发环境。尽管手工构建iproute2和Linux内核是非必须的（主流发行版已经内置），但是测试最新特性、或者需要贡献BPF补丁到内核、iproute2时则需要。</p>
<p>安装构建需要的软件：</p>
<pre class="crayon-plain-tag">sudo apt-get install -y make gcc libssl-dev bc libelf-dev libcap-dev \
  clang gcc-multilib llvm libncurses5-dev git pkg-config libmnl-dev bison flex \
  graphviz</pre>
<div class="blog_h3"><span class="graybg">构建内核</span></div>
<p>BPF相关的新特性在内核的net-next分支上开发，最后的BPF fixes则在net分支上。</p>
<p>注意打开以下内核配置项：</p>
<pre class="crayon-plain-tag">CONFIG_CGROUP_BPF=y
CONFIG_BPF=y
CONFIG_BPF_SYSCALL=y
CONFIG_NET_SCH_INGRESS=m
CONFIG_NET_CLS_BPF=m
CONFIG_NET_CLS_ACT=y
# 如果目标体系结构支持JIT，自动y
CONFIG_BPF_JIT=y
CONFIG_LWTUNNEL_BPF=y
CONFIG_HAVE_EBPF_JIT=y
CONFIG_BPF_EVENTS=y
CONFIG_TEST_BPF=m</pre>
<p>从新编译的内核启动后，运行BPF自我测试套件，应该全部通过：</p>
<pre class="crayon-plain-tag">cd tools/testing/selftests/bpf/
make
sudo ./test_verifier</pre>
<div class="blog_h3"><span class="graybg">构建iproute2</span></div>
<p>iproute2具有独立的Git仓库：</p>
<pre class="crayon-plain-tag">git clone https://git.kernel.org/pub/scm/network/iproute2/iproute2.git</pre>
<p>master分支对应内核的net分支，net-next分支对应内核的net-next分支。</p>
<p>执行下面的命令编译：</p>
<pre class="crayon-plain-tag">cd iproute2/
./configure --prefix=/usr
# 确保输出：
# ELF support: yes
# 这样iproute2才能处理LLVM BPF后端产生的ELF文件
sudo make install</pre>
<div class="blog_h3"><span class="graybg">构建bpftool</span></div>
<p>bpftools是BPF程序、Map的调试、introspection工具。它位于内核源码树的tools/bpf/bpftool/</p>
<pre class="crayon-plain-tag">cd tools/bpf/bpftool/
make
sudo make install</pre>
<div class="blog_h3"><span class="graybg">构建libbpf</span></div>
<div class="blog_h2"><span class="graybg">工具链</span></div>
<p>LLVM 3.7+是当前唯一提供BPF后端的编译套件，GCC目前不支持。主流发行版默认启用了LLVM的BPF后端支持，因此直接安装clang和llvm包足够将C编译为BPF Object文件。</p>
<p>通过下面的命令确认你的LLVM支持BPF目标：</p>
<pre class="crayon-plain-tag">llc --version
LLVM (http://llvm.org/):
  LLVM version 10.0.0
  
  Optimized build.
  Default target: x86_64-pc-linux-gnu
  Host CPU: skylake

  Registered Targets:
    ...
    # 默认情况下，bpf目标使用编译它的CPU的端序
    bpf        - BPF (host endian)
    # 这两个目标用于交叉编译
    bpfeb      - BPF (big endian)
    bpfel      - BPF (little endian)</pre>
<div class="blog_h3"><span class="graybg">编译命令</span></div>
<p>对于下面这个最简单的XDP Drop程序：</p>
<pre class="crayon-plain-tag">#include &lt;linux/bpf.h&gt;

#ifndef __section
# define __section(NAME)                  \
   __attribute__((section(NAME), used))
#endif

__section("prog")
int xdp_drop(struct xdp_md *ctx)
{
    return XDP_DROP;
}

char __license[] __section("license") = "GPL";</pre>
<p>可以这样编译：</p>
<pre class="crayon-plain-tag">clang -O2 -Wall -target bpf -c xdp-example.c -o xdp-example.o
# 加载，需要4.12或更高版本
# ip link set dev em1 xdp obj xdp-example.o</pre>
<div class="blog_h1"><span class="graybg">BPF CO-RE</span></div>
<p>所谓CO-RE是指一次编译，到处运行（Compile Once – Run Everywhere）。</p>
<p>编写可移植性（能够跨越不同内核版本运行）的BPF程序是一项挑战，BPF CO-RE能够辅助这一过程。</p>
<div class="blog_h2"><span class="graybg">问题</span></div>
<p>BPF程序本质上是一段直接插入内核的代码，它可以访问（被允许访问的）所有内核内部状态。BPF程序无法控制所运行内核的内存布局：</p>
<ol>
<li>不同内核版本，结构体字段的顺序可能不同，某些字段可能在新版本被内嵌到子结构中，字段的类型可能出现不兼容的变更</li>
<li>根据内核构建时的配置不同，某些字段可能被注释掉</li>
</ol>
<p>并不是所有BPF程序都需要访问内核数据结构，例如opensnoop，它依赖 kprobes/tracepoints 来追踪程序打开了那些文件，仅仅需要捕获一些系统调用参数，而系统调用提供稳定的ABI，不会随着内核版本而变化。</p>
<p>不幸的是，opensnoop这样的程序是少数派，不访问内核数据结构也限制了程序的能力。</p>
<p><span style="background-color: #c0c0c0;">内核中的BPF机制提供了一个有限集合的稳定接口</span>，BPF程序可以依赖这套结构，不用担心跨内核的兼容性问题。尽管底层结构会发生变化，但是BPF提供的抽象保持稳定。这种接口的一个例子是<pre class="crayon-plain-tag">__sk_buff</pre>，它是内核数据结构<pre class="crayon-plain-tag">sk_buff</pre>的稳定接口，可以访问sk_buff的一部分重要字段。对__sk_buff字段的访问，会被透明的转写（可以需要多次pointer chasing）为对sk_buff对应字段的访问。类似__sk_buff的、针对特定类型BPF程序的接口抽象还有不少，如果你编写这些类型的BPF程序，可能（如果接口可以满足需求）不用担心可移植性问题。</p>
<p>一旦有了读取内核内部原始状态的需求，例如读取<pre class="crayon-plain-tag">struct task_struct</pre>的某个字段，可移植性问题就出现了。如果某些内核为此结构添加了额外字段，如何保证读取的不是垃圾数据？如果出现了字段重命名，例如4.7将task_struct的fs字段重命名为fsbase，又该怎么办？这些可移植性问题都让你不能简单的使用开发机上的内核头文件来构建BPF程序。</p>
<div class="blog_h2"><span class="graybg">BCC方案</span></div>
<p>解决可移植性问题的一个方案是BCC，使用BCC时，BPF内核程序的C源码作为字符串嵌入在你的用户空间程序（控制程序）中，当控制程序被部署到目标环境后，BCC会调用内嵌的Clang/LLVM，拉取本地内核头文件，执行即席的BPF程序构建。如果字段可能在某些环境下compiled-out，你只需要在源码中使用相应的#ifdef/#else。BCC方案有一些关键的缺点：</p>
<ol>
<li>Clang/LLVM组合非常大，你需要将其部署到所有运行BPF程序的机器</li>
<li>Clang/LLVM在编译时很消耗资源，如果在程序启动时编译BPF代码，可能会生产环境的工作负载产生不利影响</li>
<li>目标机器上可能不包含匹配的内核头文件</li>
<li>开发和测试的迭代变得痛苦，可能在运行时得到很多编译错误</li>
<li>太多的magic，难以定位错误，你需要记住命名约定、自动为tracepoint生成的结构、依赖代码重写来读取内核数据/获取kprobe参数</li>
<li>读写BPF Map时需要编写半面向对象的C代码，和内核中发生的不完全匹配</li>
<li>仍然需要在用户空间编写大量的样板代码</li>
</ol>
<div class="blog_h2"><span class="graybg">CO-RE原理</span></div>
<p>BPF CO-RE将软件栈所有层次 —— 内核、用户空间BPF loader库（libbpf）、编译器（Clang）——的必要功能/数据片段整合到一起，来降低编写可移植性BPF程序的难度。CO-RE需要下列组件的谨慎集成和协作：</p>
<ol>
<li>BTF类型信息：允许捕获关于内核、BPF程序的类型/代码的关键信息</li>
<li>Clang为BPF程序C代码提供了express the intent和记录relocation信息的手段</li>
<li>BPF loader（libbpf）根据内核的BTF和BPF程序，调整编译后的BPF代码，使其适合在目标内核上运行</li>
<li>对于BPF CO-RE不可知的内核，提供了一些高级的BPF特性，满足高级场景</li>
</ol>
<div class="blog_h3"><span class="graybg">BTF</span></div>
<p>即BPF Type Format，类似于DWARF调试信息，但是没有那么generic和verbose。它是一种空间高效的、紧凑的、有足够表达能力的格式，足以描述C程序的所有类型信息。由于它的简单性和BPF去重算法，对比DWARF，BTF能够缩小100x的尺寸。现在，在运行时总是保留BTF信息是常见做法，它对应内核选项<pre class="crayon-plain-tag">CONFIG_DEBUG_INFO_BTF=y</pre>，在Ubuntu 20.10开始默认开启。</p>
<p>BTF能够用来增强BPF verifier的能力，能够允许BPF代码直接访问内核内存，不需要<pre class="crayon-plain-tag">bpf_probe_read()</pre>。</p>
<p>对于CO-RE来说，更重要的是，内核通过<pre class="crayon-plain-tag">/sys/kernel/btf/vmlinux</pre>暴露了权威的、自描述的BTF信息。执行下面的命令，你可以得到一个可编译的C头文件：</p>
<pre class="crayon-plain-tag">bpftool btf dump file /sys/kernel/btf/vmlinux format c</pre>
<p>此文件通常命名为<pre class="crayon-plain-tag">vmlinux.h</pre>，其中包含了所有的内核类型信息，甚至包含那些不会通过kernel-devel包暴露的信息。</p>
<div class="blog_h3"><span class="graybg">编译器支持</span></div>
<p>为了启用CO-RE，并且让BPF loader（libbpf）来为正在运行的（目标）内核调整BPF程序，Clang被扩展，增加了一些built-ins。</p>
<p>这些built-ins会发出（emit）BTF relocations，BTF relocations是BPF程序需要读取什么信息的高层描述。假设程序需要访问task_struct-&gt;pid，Clang会将其记录：需要访问pid_t类型的、名为pid、位于task_struct结构中的字段。这样，即使字段顺序调整，甚至pid字段被放入一个内嵌的匿名结构体/联合体中，BPF程序仍然能够正确访问到pid字段。</p>
<p>能够捕获（进而重定位）的信息不单单是字段偏移量，还包括字段是否存在、字段的size。甚至对于位域（bitfield）字段，也能够捕获足够多的信息，让对它的访问能够被重定位。</p>
<div class="blog_h3"><span class="graybg">BPF loader</span></div>
<p>BPF loader在加载程序时，会利用前述的（构建机的）内核BTF信息、Clang重定位信息，并读取当前内核的BTF信息，对BPF程序（ELF object文件）进行裁减（custom tailored） —— 解析和匹配所有类型、字段，更新字段偏移量，以及其它可重定位数据 —— 确保程序在当前内核上能够正确运行。</p>
<div class="blog_h3"><span class="graybg">内核</span></div>
<p>要支持CO-RE，内核不需要更多的改变（除了开启CONFIG_DEBUG_INFO_BTF）。被BPF loader（libbpf）处理过的BPF程序，对于内核来说，和在本机编译的BPF程序是完全等价的。</p>
<div class="blog_h2"><span class="graybg">CO-RE现状</span></div>
<p>截至2021年，BPF CO-RE是很成熟的技术，在大量生产环境下运行。</p>
<p>由于引入了BPF CO-RE，超过25个BCC工具被转换为libbpf +BPF CO-RE方式编写。由于越来越多的Linux发行版（Ubuntu 20.10、RHEL 8.2+）默认开启BTF，BPF CO-RE相关工具的适用面变得越来越广，可以替代笨重的、基于python的BCC工具。</p>
<p>BPF CO-RE在不同BPF应用领域被广泛接受，包括追踪、性能监控、安全/审计，甚至网络BPF程序。</p>
<p>要使用BPF CO-RE，可以考虑从脚手架项目libbpf-bootstrap开始。</p>
<div class="blog_h2"><span class="graybg">使用CO-RE</span></div>
<div class="blog_h3"><span class="graybg">解除内核依赖</span></div>
<p>为了避免依赖于系统头文件，可以生成包含所有内核类型的vmlinux.h：</p>
<pre class="crayon-plain-tag">bpftool btf dump file /sys/kernel/btf/vmlinux format c &gt; vmlinux.h</pre>
<p>这样你的代码中就不需要包含各种内核头文件、也不必须安装kernel-devel包了：</p>
<pre class="crayon-plain-tag">#include &lt;linux/sched.h&gt;
#include &lt;linux/fs.h&gt;</pre>
<p>由于BTF（以及DWARF）不会记录宏信息，因此某些常用的宏可能没有包含在vmlinux.h中，好在其中大部分可以通过bpf_helpers.h访问。</p>
<div class="blog_h3"><span class="graybg">读取内核结构</span></div>
<p>使用BCC时，你可以直接访问：</p>
<pre class="crayon-plain-tag">pid_t pid = task-&gt;pid;</pre>
<p>BCC会自动将其重写为对<pre class="crayon-plain-tag">bpf_probe_read()</pre>的调用。</p>
<p>使用CO-RE的时候，由于没有BCC这种代码重写机制，为了打到同样效果，你可能需要：</p>
<ol>
<li>libbpf + BPF_PROG_TYPE_TRACING：如果编写的是这类程序，你可以直接写：<br />
<pre class="crayon-plain-tag">pid_t pid = task-&gt;pid;</pre></p>
<p>而不需要bpf_probe_read()调用。要实现可移植性，则需要将上述代码包围到 <pre class="crayon-plain-tag">__builtin_preserve_access_index</pre>中：</p>
<pre class="crayon-plain-tag">pid_t pid = __builtin_preserve_access_index(({ task-&gt;pid; }));</pre>
</li>
<li>对于其它类型BPF程序，你不能直接访问结构字段，而需要：<br />
<pre class="crayon-plain-tag">pid_t pid; bpf_probe_read(&amp;pid, sizeof(pid), &amp;task-&gt;pid);</pre>
<p>要实现可移植性，则需要：</p>
<pre class="crayon-plain-tag">pid_t pid; bpf_core_read(&amp;pid, sizeof(pid), &amp;task-&gt;pid);
// 或者
bpf_probe_read(&amp;pid, sizeof(pid), __builtin_preserve_access_index(&amp;task-&gt;pid));</pre>
</li>
</ol>
<p>进行pointer chasing时，使用bpf_probe_read()/bpf_core_read()会变得痛苦：
<pre class="crayon-plain-tag">u64 inode = task-&gt;mm-&gt;exe_file-&gt;f_inode-&gt;i_ino;</pre>
<p>你需要逐步的分配指针临时变量，逐步读取字段，非常麻烦。 幸运的是，CO-RE提供了助手宏：</p>
<pre class="crayon-plain-tag">u64 inode = BPF_CORE_READ(task, mm, exe_file, f_inode, i_ino);

// 或者
u64 inode;
BPF_CORE_READ_INTO(&amp;inode, task, mm, exe_file, f_inode, i_ino);</pre>
<p>类似的，和<pre class="crayon-plain-tag">bpf_probe_read_str()</pre>对应的CO-RE函数是<pre class="crayon-plain-tag">bpf_core_read_str()</pre>，以及助手宏<pre class="crayon-plain-tag">BPF_CORE_READ_STR_INTO()</pre>。</p>
<p>要检查字段是否在目标内核存在，可以使用<pre class="crayon-plain-tag">bpf_core_field_exists()</pre>宏：</p>
<pre class="crayon-plain-tag">pid_t pid = bpf_core_field_exists(task-&gt;pid) ? BPF_CORE_READ(task, pid) : -1;</pre>
<p>某些内部的、非UAPI的内核枚举值，可能跨内核版本时发生变动，甚至依赖于特定的内核配置（例如cgroup_subsys_id），这导致硬编码任何值都是不可靠的。使用Enum relocation宏<pre class="crayon-plain-tag">bpf_core_enum_value_exists()</pre>和<pre class="crayon-plain-tag">bpf_core_enum_value()</pre>，可以检查特定枚举值是否存在，并捕获它的值。Enum relocation重定向的一个重要用途是检测BPF助手函数是否存在，如果不存在则使用旧版本的替代物。</p>
<p>要捕获（如果不确定某个字段是否在别的内核版本中发生了类型变更）字段的size，可以使用 <pre class="crayon-plain-tag">bpf_core_field_size()</pre>：</p>
<pre class="crayon-plain-tag">u32 comm_sz = bpf_core_field_size(task-&gt;comm);</pre>
<p>位域字段的读取，可以使用：</p>
<pre class="crayon-plain-tag">struct tcp_sock *s = ...;

// 读取s-&gt;is_cwnd_limited对应的位域字段
bool is_cwnd_limited = BPF_CORE_READ_BITFIELD(s, is_cwnd_limited);

// 或者
u64 is_cwnd_limited;
BPF_CORE_READ_BITFIELD_PROBED(s, is_cwnd_limited, &amp;is_cwnd_limited);</pre>
<div class="blog_h3"><span class="graybg">内核版本和配置差异</span></div>
<p>某些情况下，内核之间不是简单的结构性差异：</p>
<ol>
<li>同一含义的字段可能被重命名</li>
<li>字段的含义可能改变，例如从4.6开始，task_struct.utime/stime从原先的以jiffies为单位改为纳秒为单位</li>
</ol>
<p>内核配置的差异，可能会出现某些内核下无法读取字段的情况。</p>
<p>CO-RE提供处理这些问题的辅助机制是libbpf提供的extern Kconfig variables和struct flavors。</p>
<p>BPF程序可以定义具有知名名称的变量，例如LINUX_KERNEL_VERSION；或者一个匹配内核Kconfig键的变量，例如CONFIG_HZ。libbpf能够自动设置这些外部变量为匹配当前内核的值，BPF verifier也会跟踪这些变量，并进行高级的流分析和dead code消除。</p>
<pre class="crayon-plain-tag">//                              声明外部Kconfig变量
extern u32 LINUX_KERNEL_VERSION __kconfig;
extern u32 CONFIG_HZ __kconfig;

u64 utime_ns;

if (LINUX_KERNEL_VERSION &gt;= KERNEL_VERSION(4, 11, 0))
    utime_ns = BPF_CORE_READ(task, utime);
else
    /* convert jiffies to nanoseconds */
    utime_ns = BPF_CORE_READ(task, utime) * (1000000000UL / CONFIG_HZ);</pre>
<p>struct flavors则用于解决内核存在不兼容类型的情况。实际上就是为不同版本的内核定义不同的结构：</p>
<pre class="crayon-plain-tag">// 新版本内核使用此结构
struct thread_struct {
    ...
    u64 fsbase;
    ...
};

// 4.6或者更低版本使用此结构
// 三下划线及其后面的部分，被认为是结构的一个flavor，flavor部分会被libbpf忽略，
// 这意味着在进行relocation时thread_struct___v46仍然对应着运行中的内核的thread_struct结构

struct thread_struct___v46 { /* ___v46 is a "flavor" part */
    ...
    u64 fs;
    ...
};

extern int LINUX_KERNEL_VERSION __kconfig;
...

struct thread_struct *thr = ...;
u64 fsbase;
if (LINUX_KERNEL_VERSION &gt; KERNEL_VERSION(4, 6, 0))
    // 强制转型为flavor，从而抽取需要的字段
    fsbase = BPF_CORE_READ((struct thread_struct___v46 *)thr, fs);
else
    fsbase = BPF_CORE_READ(thr, fsbase);</pre>
<p>如果没有struct flavors，你就不能编写可移植的BPF程序。你只能通过#ifdef条件编译为多个BPF object文件，然后在控制程序中，判断当前内核版本，然后选择加载匹配的BPF object。</p>
<div class="blog_h3"><span class="graybg">根据用户配置修改行为</span></div>
<p>即使知道目标内核版本、配置，BPF程序可能仍然不知道如何从内核中读取需要的数据。这种情况下，可以通过用户空间的控制程序进行精确的判断，然后通过BPF Map传递一个配置信息给BPF程序，BPF程序根据此配置信息改变自己的行为。这种做法的缺点是：</p>
<ol>
<li>BPF程序每次都需要读取Map的成本，对于高性能BPF程序不可忽略</li>
<li>配置信息即使在BPF程序启动后是不可变的（没有代码去改它），但是对于BPF verifier来说，仍然是一个黑盒。BPF verifier不能根据配置信息来裁减掉dead code，或者进行其它高级的代码分析。这样，为新版本内核（假设这个版本引入了新的助手函数）编写的分支，在旧版本内核上无法被裁减，从而可能破坏程序（无法通过校验，以为助手函数不存在）</li>
</ol>
<p>解决上述缺点的方法是使用只读全局变量。变量的值由控制程序加载BPF object文件后设置（修改ELF文件）。这不会带来Map查询的成本，BPF verifer会将其此变量作为常量看待，从而裁减dead code。</p>
<pre class="crayon-plain-tag">/* global read-only variables, set up by control app */
const bool use_fancy_helper;
const u32 fallback_value;

...

u32 value;
if (use_fancy_helper)
    value = bpf_fancy_helper(ctx);
else
    value = bpf_default_helper(ctx) * fallback_value;</pre>
<p>通过BPF skeleton，可以很容易的从用户空间修改ELF文件。</p>
<div class="blog_h3"><span class="graybg">编译BPF程序</span></div>
<p>利用上文提到的BPF CO-RE提供的多种能力，编写好代码后，你需要用Clang 10+版本，编译得到BPF object文件。</p>
<div class="blog_h3"><span class="graybg">生成BPF skeleton</span></div>
<p>从编译好的BPF object文件，可以利用<pre class="crayon-plain-tag">bpftool gen skeleton</pre>自动生成BPF skeleton。</p>
<div class="blog_h3"><span class="graybg">编写控制程序</span></div>
<p>将BPF skeleton（头文件）包含到你的用户空间控制程序中，获得打开、加载、挂钩BPF程序，以及修改BPF对象等能力。</p>
<div class="blog_h1"><span class="graybg">eBPF编程</span></div>
<div class="blog_h2"><span class="graybg">C编程要点</span></div>
<p>使用C语言编写eBPF程序，需要注意：</p>
<ol>
<li>可以访问助手函数、上下文对象</li>
<li>程序的入口点通过段来指定，而非main函数</li>
<li>在对象文件中包含多个入口点是允许的</li>
<li>所有库函数调用被内联，因而运行时不存在函数调用的开销</li>
<li>没有全局变量（5.5-）</li>
<li>没有循环</li>
<li>没有常量</li>
<li>LLVM的内置函数一般是可用的、并且被内联</li>
<li>栈空间大小限制为512字节</li>
</ol>
<div class="blog_h3"><span class="graybg">内联一切</span></div>
<p>除非使用支持BPF-BPF调用的4.16+内核（和LLVM6.0+），所有函数都需要被内联，没有函数调用（老版本LLVM上）或共享库调用。</p>
<p>BPF程序不能使用共享库，公共代码可以放在头文件中，被主程序include。尽管不能使用共享库，但是通过include头文件来使用静态内联函数、宏定义是很常见的。</p>
<p>为了确保这一点，需要为所有作为库使用的函数标注__inline：</p>
<pre class="crayon-plain-tag">#include &lt;linux/bpf.h&gt;

#ifndef __section
# define __section(NAME)                  \
   __attribute__((section(NAME), used))
#endif

#ifndef __inline
# define __inline                         \
   // 使用always_inline，因为仅仅使用inline编译器仍然可能在
   // 代码过大的情况下不内联，从而导致在ELF文件中生成一个relocation entry
   // iproute2这样的ELF loader不能解析BPF程序
   // 仅仅BPF Map是ELF loader能够处理的relication entry
   inline __attribute__((always_inline))
#endif

static __inline int foo(void)
{
    return XDP_DROP;
}

__section("prog")
int xdp_drop(struct xdp_md *ctx)
{
    return foo();
}

char __license[] __section("license") = "GPL";</pre>
<p>从4.16开始支持在BPF程序中使用BPF-BPF函数调用，libbpf v0.2+也对此特性提供的完整的支持，确保代码的relocations/adjustment正确进行。你可以去掉<pre class="crayon-plain-tag">__always_inline</pre>，甚至用<pre class="crayon-plain-tag">__noinline</pre>强制不得进行内联。</p>
<p>非内联的global函数从5.5+开始被支持，但是和static函数比起来具有不同的语义以及校验约束。</p>
<div class="blog_h3"><span class="graybg">每个程序一个段</span></div>
<p>BPF的C程序依赖段（section）注解。典型的C程序被结构化为三个或更多的段，BPF ELF loader<span style="background-color: #c0c0c0;">通过段的名字来抽取、准备相关的信息</span>，以便载入程序和Map。</p>
<p>例如，iproute2使用maps和license作为默认的段名称，来获取创建Map所需元数据，以及BPF程序的License信息。加载时，License信息也被推入内核，这样某些仅仅在GPL协议下暴露的函数（例如bpf_ktime_get_ns和bpf_probe_read）允许被调用，确保BPF程序的License兼容性。</p>
<p>其它的段名，都专用于BPF程序代码。下面的代码使用了ingress/egress两个段，可以被tc加载并挂钩到网络设备的ingress/egress钩子：</p>
<pre class="crayon-plain-tag">#include &lt;linux/bpf.h&gt;
#include &lt;linux/pkt_cls.h&gt;
#include &lt;stdint.h&gt;
#include &lt;iproute2/bpf_elf.h&gt;

#ifndef __section
# define __section(NAME)                  \
   __attribute__((section(NAME), used))
#endif

#ifndef __inline
# define __inline                         \
   inline __attribute__((always_inline))
#endif

// 共享的Map是全局变量，因此访问它的时候需要同步
#ifndef lock_xadd
# define lock_xadd(ptr, val)              \
   ((void)__sync_fetch_and_add(ptr, val))
#endif

// 这个宏用于将BPF助手函数映射到C代码
// 函数map_lookup_elem被映射到定义uapi/linux/bpf.h在中的枚举值 BPF_FUNC_map_lookup_elem
// 在载入内核后，Verifier会检查传入的参数是否为期望的类型，并且将对助手函数的调用指向真实函数的调用
#ifndef BPF_FUNC
# define BPF_FUNC(NAME, ...)              \
   (*NAME)(__VA_ARGS__) = (void *)BPF_FUNC_##NAME
#endif

static void *BPF_FUNC(map_lookup_elem, void *map, const void *key);
// static void (*map_lookup_elem)(void *map, const void *key) = (void *)BPF_FUNC_map_lookup_elem;




// 这个是共享的Map，类型struct bpf_elf_map是iproute2定义的
// iproute2提供了公共的BPF ELF loader，因此struct bpf_elf_map对于XDP和tc程序来说是一样的
//                         必须放在maps段，这样loader才能发现
//                         可以定义多个Map，都必须放在maps段
struct bpf_elf_map acc_map __section("maps") = {
    .type           = BPF_MAP_TYPE_ARRAY,
    .size_key       = sizeof(uint32_t),
    .size_value     = sizeof(uint32_t),
    // 该Map被Pin到PIN_GLOBAL_NS，这意味着Map将被tc钉为BPF伪文件系统中的位于
    // /sys/fs/bpf/tc/globals/目录下的节点。对于此acc_map，节点路径为
    // /sys/fs/bpf/tc/globals/acc_map
    // global是跨越多个Object文件的全局命名空间。如果不同BPF程序中均有名为acc_map
    // 的Map映射到PIN_GLOBAL_NS，这这些程序会共享统一Map。仅仅第一个载入的BPF程序会触发
    // Map的创建，后续载入的程序直接使用

    // 如果取值PIN_NONE则不会映射为BPF文件系统中的节点，当tc退出后，无法从用户空间访问Map
    .pinning        = PIN_GLOBAL_NS,
    .max_elem       = 2,
};

// 这个是共享的内联函数
static __inline int account_data(struct __sk_buff *skb, uint32_t dir)
{
    uint32_t *bytes;
    // 将Map传递给助手函数
    bytes = map_lookup_elem(&amp;acc_map, &amp;dir);
    if (bytes)
            lock_xadd(bytes, skb-&gt;len);

    return TC_ACT_OK;
}

// 两个段，都会调用account_data往Map中写入数据
__section("ingress")
int tc_ingress(struct __sk_buff *skb)
{
    return account_data(skb, 0);
}

__section("egress")
int tc_egress(struct __sk_buff *skb)
{
    return account_data(skb, 1);
}

char __license[] __section("license") = "GPL";</pre>
<p>使用下面的命令编译：</p>
<pre class="crayon-plain-tag">clang -O2 -Wall -target bpf -c tc-example.c -o tc-example.o</pre>
<p>利用tc加载该程序：</p>
<pre class="crayon-plain-tag">tc qdisc add dev eth0 clsact
tc filter add dev eth0 ingress bpf da obj tc-example.o sec ingress
tc filter add dev eth0 egress bpf da obj tc-example.o sec egress

tc filter show dev eth0 ingress
# filter protocol all pref 49152 bpf
# filter protocol all pref 49152 bpf handle 0x1 tc-example.o:[ingress] direct-action id 1 tag c5f7825e5dac396f

tc filter show dev em1 egress
# filter protocol all pref 49152 bpf
# filter protocol all pref 49152 bpf handle 0x1 tc-example.o:[egress] direct-action id 2 tag b2fd5adc0f262714

mount | grep bpf
# sysfs on /sys/fs/bpf type sysfs (rw,nosuid,nodev,noexec,relatime,seclabel)
# bpf on /sys/fs/bpf type bpf (rw,relatime,mode=0700)

tree /sys/fs/bpf/
# /sys/fs/bpf/
# +-- ip -&gt; /sys/fs/bpf/tc/
# +-- tc
# |   +-- globals
# |       +-- acc_map
# +-- xdp -&gt; /sys/fs/bpf/tc/</pre>
<p>一旦有封包通过eth0接口，则acc_map中的计数器值就会增加。</p>
<div class="blog_h3"><span class="graybg">没有全局变量</span></div>
<p>除非内核版本在5.5+以上，BPF程序中没有普通C程序中的全局变量。5.5+的全局变量底层仍然是基于BPF Map实现的。</p>
<p>作为变通方案，可以使用BPF_MAP_TYPE_PERCPU_ARRAY类型的Map，这种Map为每个CPU核心存储一个任意大小的值。由于<span style="background-color: #c0c0c0;">BPF程序在运行过程中绝不会被抢占</span>，在此Map中初始化一个临时的缓冲区（例如为了突破栈的大小限制）用作全局变量是安全的。在<span style="background-color: #c0c0c0;">发生尾调用的情况下也不会发生抢占，Map的内容不会消失</span>。</p>
<p>对于任何需要跨越多次BPF程序运行保存的状态，都使用普通BPF Map即可。</p>
<div class="blog_h3"><span class="graybg">没有常量字符串或数组</span></div>
<p>由于一切内联、不支持全局变量，定义<pre class="crayon-plain-tag">const</pre>的字符串或其它数组都是不被支持的，生成在ELF文件中的relocation entry会被BPF ELF loaders拒绝。</p>
<p>打印调试消息可以利用助手函数trace_printk：</p>
<pre class="crayon-plain-tag">static void BPF_FUNC(trace_printk, const char *fmt, int fmt_size, ...);

#ifndef printk
# define printk(fmt, ...)                                      \
    ({                                                         \
        char ____fmt[] = fmt;                                  \
        trace_printk(____fmt, sizeof(____fmt), ##__VA_ARGS__); \
    })
#endif


// 调用
printk("skb len:%u\n", skb-&gt;len);

// 在用于空间使用下面的命令查看打印的消息
// tc exec bpf dbg</pre>
<p>不建议在生产环境使用trace_printk()助手函数，因为类似于<pre class="crayon-plain-tag">"skb len:%u\n"</pre>这样的字符串必须在每次调用时载入到BPF Stack，此外助手函数最多支持5个参数。</p>
<p>对于网络应用，可以使用<pre class="crayon-plain-tag">skb_event_output()</pre>或者<pre class="crayon-plain-tag">xdp_event_output()</pre>代替，它们允许从BPF程序传递自定义的结构体，外加一个可选的packet sample到perf event ring buffer。</p>
<div class="blog_h3"><span class="graybg">使用LLVM内置函数</span></div>
<p>除了助手函数之外，BPF程序不能发起任何函数调用，因此公共库代码需要实现为内联函数。此外LLVM提供的一些builtins可以被使用，并且总是保证被内联：</p>
<pre class="crayon-plain-tag">#ifndef memset
# define memset(dest, chr, n)   __builtin_memset((dest), (chr), (n))
#endif

#ifndef memcpy
# define memcpy(dest, src, n)   __builtin_memcpy((dest), (src), (n))
#endif

#ifndef memmove
# define memmove(dest, src, n)  __builtin_memmove((dest), (src), (n))
#endif</pre>
<p>内置函数memcmp()存在一些边缘场景，会导致不发生inline，因此在LLVM解决此问题之前不推荐使用。</p>
<div class="blog_h3"><span class="graybg">尚不支持循环</span></div>
<p>BPF Verifier会检查程序代码，确保其不包含循环，目的是确保程序总是能停止。</p>
<p>只有非常特殊形式的循环被允许：</p>
<pre class="crayon-plain-tag">// 使用该指令，内核5.3+不再需要
#pragma unroll
    // 循环的upper buounds是常量
    for (i = 0; i &lt; IPV6_MAX_HEADERS; i++) {
        // ...
    }</pre>
<p>另外一种比较刁钻的实现循环的方式是，自我尾调用， 使用一个BPF_MAP_TYPE_PERCPU_ARRAY作为变量存储。这种方式的循环次数是动态的，但是最多迭代34次（初始程序，加上最多33次尾调用）。</p>
<div class="blog_h3"><span class="graybg">使用尾调用</span></div>
<p>尾调用提供了一种灵活的、在运行时原子的修改程序行为的方式，它的做法是从一个BPF程序跳转到另外一个，同时保留当前栈帧。尾调用没有return的概念，当前程序直接被替换掉。</p>
<p>发起尾调用时必须使用BPF_MAP_TYPE_PROG_ARRAY类型的Map，传递目标BPF程序所在索引。</p>
<p>使用尾调用可以非常灵活的对程序进行“分区”。例如挂钩到XDP或tc的根BPF程序可以发起对索引为0的BPF程序的尾调用，后者执行流量采样，然后跳转到BPF程序1，在此应用防火墙策略。封包在此被丢弃，或进一步尾调用2来处理，BPF程序2修改封包并将其从网络接口发出。</p>
<pre class="crayon-plain-tag">#ifndef __stringify
# define __stringify(X)   #X
#endif

#ifndef __section
# define __section(NAME)                  \
   __attribute__((section(NAME), used))
#endif

#ifndef __section_tail
# define __section_tail(ID, KEY)          \
   __section(__stringify(ID) "/" __stringify(KEY))
#endif

#ifndef BPF_FUNC
# define BPF_FUNC(NAME, ...)              \
   (*NAME)(__VA_ARGS__) = (void *)BPF_FUNC_##NAME
#endif

#define BPF_JMP_MAP_ID   1

static void BPF_FUNC(tail_call, struct __sk_buff *skb, void *map,
                     uint32_t index);

// 创建一个eBPF程序数组并且钉到BPF文件系统的全局命名空间下的/jmp_map节点
struct bpf_elf_map jmp_map __section("maps") = {
    .type           = BPF_MAP_TYPE_PROG_ARRAY,
    .id             = BPF_JMP_MAP_ID,
    .size_key       = sizeof(uint32_t),
    .size_value     = sizeof(uint32_t),
    .pinning        = PIN_GLOBAL_NS,
    .max_elem       = 1,
};

// iproute2的BPF ELF loader能够识别标记为__section_tail()的块，将其存放到某个程序数组中
// 第一个参数ID用于决定存放到哪个数组，第二个参数用于决定存放到数组的哪个索引
// 不仅仅是tc，任何iproute2支持的BPF程序类型（XDP，lwt等）都可以使用这种标记
__section_tail(BPF_JMP_MAP_ID, 0)
int looper(struct __sk_buff *skb)
{
    printk("skb cb: %u\n", skb-&gt;cb[0]++);
    tail_call(skb, &amp;jmp_map, 0);
    return TC_ACT_OK;
}

// 主程序
__section("prog")
int entry(struct __sk_buff *skb)
{
    skb-&gt;cb[0] = 0;
    // 发起尾调用
    tail_call(skb, &amp;jmp_map, 0);
    return TC_ACT_OK;
}

char __license[] __section("license") = "GPL";</pre>
<p>钉在BPF伪文件系统的BPF程序数组，可被用户空间程序查询或修改，tc也提供了更新BPF程序的命令：</p>
<pre class="crayon-plain-tag">#                   更换globals/jmp_map的 0索引元素
#                                             用new.o的 foo段代替
tc exec bpf graft m:globals/jmp_map key 0 obj new.o sec foo</pre>
<div class="blog_h3"><span class="graybg">受限的栈空间</span></div>
<p>BPF程序的栈空间仅有512字节，因此编码时需要小心。要使用一个较大的缓冲区，可以从BPF_MAP_TYPE_PERCPU_ARRAY类型的Map分配。</p>
<div class="blog_h3"><span class="graybg">去除字节对齐补白</span></div>
<p>现代编译器默认情况下会进行字节边界对齐 —— 结构体成员被对齐到是它们长度的整数倍的内存边界，空出的部分自动补白：</p>
<pre class="crayon-plain-tag">struct called_info {
    u64 start;  // 8-byte
    u64 end;    // 8-byte
    u32 sector; // 4-byte
}; // size of 20-byte ?

printf("size of %d-byte\n", sizeof(struct called_info)); // size of 24-byte

// Actual compiled composition of struct called_info
// 0x0(0)                   0x8(8)
//  ↓________________________↓
//  |        start (8)       |
//  |________________________|
//  |         end  (8)       |
//  |________________________|
//  |  sector(4) |  PADDING  | &lt;= address aligned to 8
//  |____________|___________|     with 4-byte PADDING.</pre>
<p>由于字节对齐的原因，结构体的大小通常比期望的大。</p>
<p>BPF Verifier会检查栈的边界，确保程序不会越界（512字节）访问，或者访问未初始化的栈区域。使用带补白的结构作为Map的值，可能导致在<pre class="crayon-plain-tag">bpf_prog_load()</pre>时报invalid indirect read from stack错。</p>
<p>你需要使用pack指令移除补白：</p>
<pre class="crayon-plain-tag">#pragma pack(4)
struct called_info {
    u64 start;  // 8-byte
    u64 end;    // 8-byte
    u32 sector; // 4-byte
}; // size of 20-byte ?

printf("size of %d-byte\n", sizeof(struct called_info)); // size of 20-byte

// Actual compiled composition of packed struct called_info
// 0x0(0)                   0x8(8)
//  ↓________________________↓
//  |        start (8)       |
//  |________________________|
//  |         end  (8)       |
//  |________________________|
//  |  sector(4) |             &lt;= address aligned to 4
//  |____________|                 with no PADDING.</pre>
<p>移除字节对齐补白后，会导致CPU内存访问效率的降低，在某些体系结构下，不对齐的访问（unaligned access）可能被Verifier拒绝。</p>
<p>所以，最优的方式是人工添加仅用于实现字节对齐的pad字段：</p>
<pre class="crayon-plain-tag">struct called_info {
    u64 start;  // 8-byte
    u64 end;    // 8-byte
    u32 sector; // 4-byte
    u32 pad;    // 4-byte
}; // size of 24-byte ?

printf("size of %d-byte\n", sizeof(struct called_info)); // size of 24-byte

// Actual compiled composition of struct called_info with explicit padding
// 0x0(0)                   0x8(8)
//  ↓________________________↓
//  |        start (8)       |
//  |________________________|
//  |         end  (8)       |
//  |________________________|
//  |  sector(4) |  pad (4)  | &lt;= address aligned to 8
//  |____________|___________|     with explicit PADDING.</pre>
<div class="blog_h3"><span class="graybg">无效引用问题</span></div>
<p>诸如bpf_skb_store_bytes之类的助手函数，会导致封包的size发生变化。由于Verifier无法在运行时跟踪这种变化，因此一旦调用了这类助手函数，对数据的引用（指针）立刻会被Verifer无效化（invalidated）：</p>
<pre class="crayon-plain-tag">struct iphdr *ip4 = (struct iphdr *) skb-&gt;data + ETH_HLEN;

skb_store_bytes(skb, l3_off + offsetof(struct iphdr, saddr), &amp;new_saddr, 4, 0);

// Verifier会拒绝下面的代码，因为此处ip4这个指针已经无效了，不能解引用
if (ip4-&gt;protocol == IPPROTO_TCP) {
    // do something
}</pre>
<p>解决办法是重新获取引用：</p>
<pre class="crayon-plain-tag">struct iphdr *ip4 = (struct iphdr *) skb-&gt;data + ETH_HLEN;

skb_store_bytes(skb, l3_off + offsetof(struct iphdr, saddr), &amp;new_saddr, 4, 0);

// 重新获取引用
ip4 = (struct iphdr *) skb-&gt;data + ETH_HLEN;

if (ip4-&gt;protocol == IPPROTO_TCP) {
    // do something
}</pre>
<div class="blog_h2"><span class="graybg">bpftool</span></div>
<p>bpftool是内核提供（tools/bpf/bpftool/）的，主要的BPF内省（introspection）和调试工具。它支持：</p>
<ol>
<li>Dump出当前加载到系统中的所有BPF程序，以及Maps</li>
<li>列出指定程序使用的Maps</li>
<li>Dump中Map的所有键值对</li>
<li>对Map键值对进行增删改查</li>
<li>将Map或程序钉到BPF伪文件系统</li>
</ol>
<p>指定bpftool操作的目标时，可以使用ID，或者目标在BPF伪文件系统中的路径。</p>
<div class="blog_h3"><span class="graybg">查看程序和Map</span></div>
<p>列出所有程序：</p>
<pre class="crayon-plain-tag">bpftool prog</pre>
<p>输出为JSON：</p>
<pre class="crayon-plain-tag"># 对于所有子命令，都支持输出JSON
bpftool prog --json --pretty</pre>
<p>列出所有Map：</p>
<pre class="crayon-plain-tag">bpftool map</pre>
<p>查看特定程序：</p>
<pre class="crayon-plain-tag">bpftool prog show id 406
#    程序类型为sched_cls，即BPF_PROG_TYPE_SCHED_CLS
406: sched_cls  tag e0362f5bd9163a0a
#    加载此程序的用户和时间
     loaded_at Apr 09/16:24  uid 0
#    指令序列长度为11144字节
#                   JIT编译后的映像为7721字节
#                               程序本身（不包含Map）占用空间122888字节
#                                                使用的Maps列表
     xlated 11144B  jited 7721B  memlock 12288B  map_ids 18,20,8,5,6,14</pre>
<div class="blog_h3"><span class="graybg">Dump程序指令</span></div>
<p>使用下面的命令可以dump出BPF程序的指令：</p>
<pre class="crayon-plain-tag"># bpftool prog dump xlated id 406
 0: (b7) r7 = 0
 1: (63) *(u32 *)(r1 +60) = r7
 2: (63) *(u32 *)(r1 +56) = r7
 3: (63) *(u32 *)(r1 +52) = r7
[...]
47: (bf) r4 = r10
48: (07) r4 += -40
49: (79) r6 = *(u64 *)(r10 -104)
50: (bf) r1 = r6
51: (18) r2 = map[id:18]                    # &lt;-- 使用ID为18的Map
53: (b7) r5 = 32
54: (85) call bpf_skb_event_output#5656112  # &lt;-- 调用助手函数
55: (69) r1 = *(u16 *)(r6 +192)
[...]</pre>
<p>使用下面的命令可以dump出程序JIT后的汇编指令：</p>
<pre class="crayon-plain-tag"># bpftool prog dump jited id 406
 0:        push   %rbp
 1:        mov    %rsp,%rbp
 4:        sub    $0x228,%rsp
 b:        sub    $0x28,%rbp
 f:        mov    %rbx,0x0(%rbp)
13:        mov    %r13,0x8(%rbp)
17:        mov    %r14,0x10(%rbp)
1b:        mov    %r15,0x18(%rbp)
1f:        xor    %eax,%eax
21:        mov    %rax,0x20(%rbp)
25:        mov    0x80(%rdi),%r9d</pre>
<div class="blog_h3"><span class="graybg">Dump Map</span></div>
<p>Dump整个Map：</p>
<pre class="crayon-plain-tag">bpftool map dump id 5</pre>
<div class="blog_h2"><span class="graybg">libbpf</span></div>
<p>libbpf是一个C/C++库，作为内核的一部分进行维护，位于tools/lib/bpf目录下。内核自带的eBPF代码样例均依赖于此库。libbpf提供了一个eBPF loader，用于处理LLVM生成的ELF文件，将其载入内核。libbpf中的一部分特性源自BCC，它也包含了一些额外的功能，例如全局变量、BPF Skeletons。</p>
<div class="blog_h2"><span class="graybg">BPF系统调用</span></div>
<p>为了支持eBPF相关操作，例如载入eBPF程序、挂钩到特定事件、创建和访问eBPF Map，Linux中引入了一个新的系统调用<pre class="crayon-plain-tag">bpf</pre>。</p>
<p>该系统调用的签名如下：</p>
<pre class="crayon-plain-tag">//      命令      用于内核和用户空间的数据交互  attr的字节数
int bpf(int cmd, union bpf_attr *attr,      unsigned int size);
// cmd有很多，要么和eBPF程序交互、要么和eBPF Map交互，或者同时和两者交互</pre>
<div class="blog_h3"><span class="graybg">BPF_PROG_LOAD</span></div>
<p>该命令用于载入eBPF程序。载入的时候需要指明程序的类型。程序类型决定了以下事项：</p>
<ol>
<li>程序在何处挂钩</li>
<li>校验器允许程序调用哪些助手函数</li>
<li>是否允许直接访问网络封包数据</li>
<li>传递给程序的<span style="background-color: #c0c0c0;">第一个参数的对象的类型</span></li>
</ol>
<p>可以看到，程序类型规定了eBPF程序的API接口。某些时候，定义一个新的程序类型，仅仅是为了限制可调用函数的列表，例如BPF_PROG_TYPE_CGROUP_SKB、BPF_PROG_TYPE_SOCKET_FILTER</p>
<div class="blog_h3"><span class="graybg">BPF_MAP_CREATE</span></div>
<p>用于创建eBPF Maps，参考下文。</p>
<div class="blog_h2"><span class="graybg">BPF程序类型</span></div>
<div class="blog_h3"><span class="graybg">BPF_PROG_TYPE_SOCKET_FILTER</span></div>
<p>网络封包过滤器</p>
<div class="blog_h3"><span class="graybg">BPF_PROG_TYPE_KPROBE</span></div>
<p>挂钩到一个KProbe，BPF程序在某个内核函数被调用时触发</p>
<div class="blog_h3"><span class="graybg">BPF_PROG_TYPE_SCHED_CLS</span></div>
<p>网络流量控制（TC）的分类器（classifier）</p>
<div class="blog_h3"><span class="graybg">BPF_PROG_TYPE_SCHED_ACT</span></div>
<p>网络流量控制（TC）动作（action）</p>
<div class="blog_h3"><span class="graybg">BPF_PROG_TYPE_TRACEPOINT</span></div>
<p>挂钩到一个Tracepoint，当执行到内核特定的代码路径时触发BPF程序</p>
<div class="blog_h3"><span class="graybg">BPF_PROG_TYPE_XDP</span></div>
<p>在设备驱动接收路径上运行的网络封包过滤器</p>
<div class="blog_h3"><span class="graybg">BPF_PROG_TYPE_PERF_EVENT</span></div>
<p>决定是否应当触发一个Perf Event Handler</p>
<div class="blog_h3"><span class="graybg">BPF_PROG_TYPE_CGROUP_SKB</span></div>
<p>为控制组提供的网络封包过滤器</p>
<div class="blog_h3"><span class="graybg">BPF_PROG_TYPE_CGROUP_SOCK</span></div>
<p>同上，但是允许修改套接字选项</p>
<div class="blog_h3"><span class="graybg">BPF_PROG_TYPE_LWT_*</span></div>
<p>用于轻量级隧道（lightweight tunnels ）的网络封包过滤器</p>
<div class="blog_h3"><span class="graybg">BPF_PROG_TYPE_SOCK_OPS</span></div>
<p>用于设置套接字选项</p>
<div class="blog_h3"><span class="graybg">BPF_PROG_TYPE_SK_SKB</span></div>
<p>用于在套接字之间转发封包</p>
<div class="blog_h3"><span class="graybg">BPF_PROG_CGROUP_DEVICE</span></div>
<p>决定是否允许一个设备操作</p>
<div class="blog_h2"><span class="graybg">BPF Map</span></div>
<p>部分Map专供特定的助手函数使用，以实现特殊任务。</p>
<div class="blog_h3"><span class="graybg">定义Map</span></div>
<p>要在BPF程序中定义一个Map，需要用到如下结构：</p>
<pre class="crayon-plain-tag">struct bpf_map_def {
        unsigned int type; // Map类型
        unsigned int key_size; // 键的长度
        unsigned int value_size; // 值的长度
        unsigned int max_entries; // 最大键值对数量
        unsigned int map_flags; // 标记
        unsigned int inner_map_idx;
        unsigned int numa_node;
};</pre>
<p>下面是一个例子：</p>
<pre class="crayon-plain-tag">struct bpf_map_def SEC("maps") my_map = {
        .type = BPF_MAP_TYPE_ARRAY,
        .key_size = sizeof(int),
        .value_size = sizeof(u64),
        .max_entries = MAX_CPU,
};</pre>
<p>在加载阶段，<pre class="crayon-plain-tag">bpf_load.c</pre>会扫描BPF object的ELF头以发现Map定义，并调用tools/lib/bpf/bpf.c中的<pre class="crayon-plain-tag">bpf_create_map_node()</pre> 或 <pre class="crayon-plain-tag">bpf_create_map_in_map_node()</pre>来创建Map。这两个函数实际上是调用<pre class="crayon-plain-tag">bpf</pre>系统调用的<pre class="crayon-plain-tag">BPF_MAP_CREATE</pre>命令。</p>
<p>除非你在编写lwt或tc等类型的BPF程序，你都应该使用上面的方式来定义Map。tc之类的程序使用iproute2作为loader，可以使用下面的结构来定义Map：</p>
<pre class="crayon-plain-tag">#define PIN_GLOBAL_NS           2

struct bpf_elf_map {
        __u32 type;
        __u32 size_key;
        __u32 size_value;
        __u32 max_elem;
        __u32 flags;
        __u32 id;
        __u32 pinning;
};    

struct bpf_elf_map SEC("maps") tun_iface = {
        .type = BPF_MAP_TYPE_ARRAY,
        .size_key = sizeof(int),
        .size_value = sizeof(int),
        .pinning = PIN_GLOBAL_NS,
        .max_elem = 1,

};</pre>
<div class="blog_h3"><span class="graybg">钉住Map</span></div>
<p>所谓Pinning是指通过文件系统路径来暴露Map。对于tc之类的iproute2加载的程序，可以使用这些宏：</p>
<pre class="crayon-plain-tag">#define PIN_NONE        0
#define PIN_OBJECT_NS        1
// 钉到/sys/fs/bpf/tc/globals/ 
#define PIN_GLOBAL_NS        2</pre>
<p>其它程序，你可以手工调用libbpf钉住Map： <pre class="crayon-plain-tag">bpf_obj_pin(fd, path)</pre>。其它程序可以调用<pre class="crayon-plain-tag">mapfd = bpf_obj_get(pinned_file_path);</pre>获得Map的文件描述符。</p>
<div class="blog_h3"><span class="graybg">操控Map</span></div>
<p>检查头文件<pre class="crayon-plain-tag">linux/bpf_types.h</pre>你会发现，不同的Map的操作，是由<pre class="crayon-plain-tag">bpf_map_ops</pre>结构所引用的不同函数指针实现的。</p>
<pre class="crayon-plain-tag">BPF_MAP_TYPE(BPF_MAP_TYPE_ARRAY, array_map_ops)
BPF_MAP_TYPE(BPF_MAP_TYPE_PERCPU_ARRAY, percpu_array_map_ops)</pre>
<p>不过对于BPF开发者来说，所有Map都可以在eBPF或用户空间程序中，通过<pre class="crayon-plain-tag">bpf_map_lookup_elem()</pre>和<pre class="crayon-plain-tag">bpf_map_update_elem()</pre>函数访问。</p>
<div class="blog_h2"><span class="graybg">Map类型</span></div>
<p>所有的Map类型定义在枚举<pre class="crayon-plain-tag">bpf_map_type</pre>中。</p>
<div class="blog_h3"><span class="graybg">BPF_MAP_TYPE_HASH</span></div>
<p>哈希表。</p>
<div class="blog_h3"><span class="graybg">BPF_MAP_TYPE_ARRAY</span></div>
<p>Array Map，为快速查找优化。</p>
<p>键是数组索引值（4字节，64bit），不支持删除键值。所有其它Array Map都具有此特征。</p>
<div class="blog_h3"><span class="graybg">BPF_MAP_TYPE_PROG_ARRAY</span></div>
<p>存放对应eBPF程序的文件描述符的数组。用于实现<pre class="crayon-plain-tag">bpf_tail_call()</pre>需要的跳转表（jump tables）。</p>
<div class="blog_h3"><span class="graybg">BPF_MAP_TYPE_PERCPU_ARRAY</span></div>
<p>per-CPU Array Map，也就是每个CPU对应一个值。<span style="background-color: #c0c0c0;">键仍然是数字，值则是和CPU个数相同的数组</span>：</p>
<pre class="crayon-plain-tag">long values[nr_cpus];
ret = bpf_map_lookup_elem(map_fd, &amp;next_key, values);
if (ret) {
    perror("Error looking up stat");
    continue;
}
for (i = 0; i &lt; nr_cpus; i++) {
    sum += values[i];
} </pre>
<p>此Map可用于代替栈上变量，分配大的缓冲区，以解决栈空间仅512字节的问题。亦可用作全局变量，以解决较旧版本内核中BPF程序没有原生全局变量支持的问题</p>
<p>由于值是per-CPU的，而执行中的BPF程序不会被抢占。因此只要正确编码（仅访问当前CPU的值），就不会产生竞态条件。对于<span style="background-color: #c0c0c0;">会被频繁执行的代码路径，一般会考虑per-CPU的Map</span>。</p>
<div class="blog_h3"><span class="graybg">BPF_MAP_TYPE_PERF_EVENT_ARRAY</span></div>
<p>即perfbuf，per-CPU的缓冲区。在内核空间，供<pre class="crayon-plain-tag">bpf_perf_event_output()</pre>函数使用，调用该函数，可以输出指定类型的结构到缓冲区。</p>
<p>在用户空间，可以进行epoll，当有数据输出时会得到通知。</p>
<div class="blog_h3"><span class="graybg">BPF_MAP_TYPE_RINGBUF</span></div>
<p>即ringbuf，perfbuf的继任，所有CPU共享的一个环形缓冲区。</p>
<div class="blog_h3"><span class="graybg">BPF_MAP_TYPE_CGROUP_ARRAY</span></div>
<p>在用户空间，存放cgroup的文件描述符。</p>
<p>在内核空间，调用<pre class="crayon-plain-tag">bpf_skb_under_cgroup()</pre>来检查skb是否和Map中指定索引的cgroup关联。</p>
<div class="blog_h3"><span class="graybg">BPF_MAP_TYPE_PERCPU_HASH</span></div>
<p>per-CPU的哈希表</p>
<div class="blog_h3"><span class="graybg">BPF_MAP_TYPE_LRU_HASH</span></div>
<p>使用LRU算法的哈希表</p>
<div class="blog_h3"><span class="graybg">BPF_MAP_TYPE_LRU_PERCPU_HASH</span></div>
<p>per-CPU的使用LRU算法的哈希表</p>
<div class="blog_h3"><span class="graybg">BPF_MAP_TYPE_LPM_TRIE</span></div>
<p>最长前缀匹配的字典树（<a href="/trie">trie</a>），可用于IP地址范围匹配。</p>
<div class="blog_h3"><span class="graybg">BPF_MAP_TYPE_STACK_TRACE</span></div>
<p>存储栈追踪信息。</p>
<div class="blog_h3"><span class="graybg">BPF_MAP_TYPE_ARRAY_OF_MAPS</span></div>
<p>Map的数组。</p>
<div class="blog_h3"><span class="graybg">BPF_MAP_TYPE_HASH_OF_MAPS</span></div>
<p>Map的Map。</p>
<div class="blog_h3"><span class="graybg">BPF_MAP_TYPE_DEVICE_MAP</span></div>
<p>存储和查找网络设备引用。</p>
<div class="blog_h3"><span class="graybg">BPF_MAP_TYPE_SOCKMAP</span></div>
<p>存储和查找套接字，允许基于助手函数，实现套接字重定向。</p>
<div class="blog_h2"><span class="graybg">助手函数</span></div>
<p>助手函数由libbpf库提供，定义在<pre class="crayon-plain-tag">bpf_helpers.h</pre>中。</p>
<div class="blog_h3"><span class="graybg">bpf_map_lookup_elem</span></div>
<p>签名： <pre class="crayon-plain-tag">void *bpf_map_lookup_elem(struct bpf_map *map, const void *key)</pre></p>
<p>查找eBPF中和一个Key关联的条目。如果找不到条目，返回NULL</p>
<div class="blog_h3"><span class="graybg">bpf_map_update_elem</span></div>
<p>签名：<pre class="crayon-plain-tag">int bpf_map_update_elem(struct bpf_map *map, const void *key, const void *value, u64 flags)</pre></p>
<p>添加或修改一个Key关联的值。flags可以是以下之一：</p>
<ol>
<li>BPF_NOEXIST 键值必须不存在，即执行添加操作。不能和BPF_MAP_TYPE_ARRAY、BPF_MAP_TYPE_PERCPU_ARRAY联用</li>
<li>BPF_EXIST，键值必须存在，即执行更新操作</li>
<li>BPF_ANY，更新或修改</li>
</ol>
<div class="blog_h3"><span class="graybg">bpf_map_delete_elem</span></div>
<p>签名：<pre class="crayon-plain-tag">int bpf_map_delete_elem(struct bpf_map *map, const void *key)</pre></p>
<p>从eBPF Map中删除条目</p>
<div class="blog_h3"><span class="graybg">bpf_probe_read</span></div>
<p>签名：<pre class="crayon-plain-tag">int bpf_probe_read(void *dst, u32 size, const void *src)</pre></p>
<p>对于Tracing用途的eBPF程序，可以安全的从src读取size字节存储到dst</p>
<div class="blog_h3"><span class="graybg">bpf_ktime_get_ns</span></div>
<p>签名：<pre class="crayon-plain-tag">u64 bpf_ktime_get_ns(void)</pre></p>
<p>读取系统从启动到现在的纳秒数，返回ktime</p>
<div class="blog_h3"><span class="graybg">bpf_trace_printk</span></div>
<p>签名：<pre class="crayon-plain-tag">int bpf_trace_printk(const char *fmt, u32 fmt_size, ...)</pre></p>
<p>类似于printk()的调试工具，从DebugFS打印由fmt所定义的格式化字符串到<pre class="crayon-plain-tag">/sys/kernel/debug/tracing/trace</pre>。最多支持3个额外的u64参数。</p>
<p>每当此函数被调用，它都会打印一行到trace，格式取决于配置<pre class="crayon-plain-tag">/sys/kernel/debug/tracing/trace_options</pre>。默认格式如下：</p>
<pre class="crayon-plain-tag"># 当前任务的名字
#      当前任务的PID
#            当前CPU序号
#                  每个字符表示一个选项
#                       时间戳
#                                      BPF使用的指令寄存器的Fake值
telnet-470   [001] .N.. 419421.045894: 0x00000001: &lt;formatted msg&gt;</pre>
<p>可以使用的格式化占位符：<pre class="crayon-plain-tag">%d, %i, %u, %x, %ld, %li, %lu, %lx, %lld, %lli, %llu, %llx, %p, %s</pre>。不支持长度、补白等修饰符。</p>
<p>该函数比较缓慢，应该仅用于调试目的。</p>
<div class="blog_h3"><span class="graybg">bpf_get_prandom_u32</span></div>
<p>签名：<pre class="crayon-plain-tag">u32 bpf_get_prandom_u32(void)</pre></p>
<p>获得一个伪随机数。</p>
<div class="blog_h3"><span class="graybg">bpf_get_smp_processor_id</span></div>
<p>签名：<pre class="crayon-plain-tag">u32 bpf_get_smp_processor_id(void)</pre></p>
<p>得到SMP处理器ID，需要注意，所有eBPF都在禁止抢占的情况下运行，这意味着在eBPF程序的执行过程中，此ID不会改变。</p>
<div class="blog_h3"><span class="graybg">bpf_skb_store_bytes</span></div>
<p>签名：<pre class="crayon-plain-tag">int bpf_skb_store_bytes(struct sk_buff *skb, u32 offset, const void *from, u32 len, u64 flags)</pre></p>
<p>存储缓冲区from的len字节到，skb所关联的封包的offset位置。flags是以下位域的组合：</p>
<ol>
<li>BPF_F_RECOMPUTE_CSUM：自动重新计算修改后的封包的Checksum</li>
<li>BPF_F_INVALIDATE_HASH：重置<pre class="crayon-plain-tag">skb-&gt;hash</pre> <pre class="crayon-plain-tag">skb-&gt;swhash</pre> <pre class="crayon-plain-tag">skb-&gt;l4hash</pre>为0</li>
</ol>
<p>调用此助手函数会导致封包缓冲区改变，因此在加载期间校验器对指针的校验将失效，必须重新校验。</p>
<div class="blog_h3"><span class="graybg">bpf_skb_load_bytes</span></div>
<p>签名：<pre class="crayon-plain-tag">int bpf_skb_load_bytes(const struct sk_buff *skb, u32 offset, void *to, u32 len)</pre></p>
<p>从skb中的offset位置读取len长的数据，存放到to缓冲区。</p>
<p>从4.7开始，该函数的功能基本被直接封包访问（direct packet access）代替 —— <pre class="crayon-plain-tag">skb-&gt;data</pre>和<pre class="crayon-plain-tag">skb-&gt;data_end</pre>给出了封包数据的位置。如果希望一次性读取大量数据到eBPF，仍然可以使用该函数。</p>
<div class="blog_h3"><span class="graybg">bpf_l3_csum_replace</span></div>
<p>签名：<pre class="crayon-plain-tag">int bpf_l3_csum_replace(struct sk_buff *skb, u32 offset, u64 from, u64 to, u64 size)</pre></p>
<p>重新计算L3（IP）的Checksum。计算是增量进行的，因此助手函数必须知道被修改的头字段的前值（from）、修改后的值（to），以及被修改字段的字节数（size，2或4）。你亦可将from和size设置为0，并将字段修改前后的差存放到to。offset用于指示封包的IP Checksum的位置</p>
<p>调用此助手函数会导致封包缓冲区改变，因此在加载期间校验器对指针的校验将失效，必须重新校验。</p>
<div class="blog_h3"><span class="graybg">bpf_l4_csum_replace</span></div>
<p>签名：<pre class="crayon-plain-tag">int bpf_l4_csum_replace(struct sk_buff *skb, u32 offset, u64 from, u64 to, u64 flags)</pre></p>
<p>重新计算L4（TCP/UDP/ICMP）的Checksum。计算是增量进行的，因此助手函数必须知道被修改的头字段的前值（from）、修改后的值（to），以及被修改字段的字节数（存放在flags的低4bit，2或4）。你亦可将from和flags低4bit设置为0，并将字段修改前后的差存放到to。offset用于指示封包的IP Checksum的位置。</p>
<p>flags的高位用于存放以下标记：</p>
<ol>
<li>BPF_F_MARK_MANGLED_0，如果Checksum是null，则不去修改它，除非设置了BPF_F_MARK_ENFORCE</li>
<li>CSUM_MANGLED_0，对于导致Checksum为null的更新操作，设置此标记</li>
<li>BPF_F_PSEUDO_HDR，提示使用pseudo-header来计算Checksum</li>
</ol>
<p>调用此助手函数会导致封包缓冲区改变，因此在加载期间校验器对指针的校验将失效，必须重新校验。</p>
<div class="blog_h3"><span class="graybg">bpf_tail_call</span></div>
<p>签名：<pre class="crayon-plain-tag">int bpf_tail_call(void *ctx, struct bpf_map *prog_array_map, u32 index)</pre></p>
<p>这是一个特殊的助手函数，用于触发尾调用 —— 跳转到另外一个eBPF程序。新程序将使用一样的栈帧，但是被调用者不能访问调用者在栈上存储的值，以及寄存器。</p>
<p>使用场景包括：</p>
<ol>
<li>突破eBPF程序长度限制</li>
<li>在不同条件下进行跳转（到子程序）</li>
</ol>
<p>出于安全原因，可以连续执行的尾调用次数是受限制的。限制定义在内核宏MAX_TAIL_CALL_CNT中，默认32，无法被用户空间访问</p>
<p>当调用发生后，程序尝试跳转到prog_array_map（BPF_MAP_TYPE_PROG_ARRAY类型的Map）的index索引处的eBPF程序，并且将当前ctx传递给它。</p>
<p>如果调用成功，则当前程序被替换掉，不存在函数调用返回。如果调用失败，则不产生任何作用，当前程序继续运行后续指令。失败的原因包括：</p>
<ol>
<li>指定的index不存在eBPF程序</li>
<li>当前尾调用链的长度超过限制</li>
</ol>
<div class="blog_h3"><span class="graybg">bpf_clone_redirect</span></div>
<p>签名：<pre class="crayon-plain-tag">int bpf_clone_redirect(struct sk_buff *skb, u32 ifindex, u64 flags)</pre></p>
<p>克隆skb关联的封包，并且重定向到由ifindx所指向的网络设备。入站/出站路径都可以用于重定向。标记BPF_F_INGRESS用于确定是重定向到入站（ingress）还是出站（egress）路径，如果该标记存在则入站。</p>
<p>调用此助手函数会导致封包缓冲区改变，因此在加载期间校验器对指针的校验将失效，必须重新校验。</p>
<div class="blog_h3"><span class="graybg">bpf_redirect</span></div>
<p>签名：<pre class="crayon-plain-tag">int bpf_redirect(u32 ifindex, u64 flags)</pre></p>
<p>重定向封包到ifindex所指向的网络设备。类似于bpf_clone_redirect，但是不会进行封包克隆，因而性能较好。缺点是，redirect操作实际上是在eBPF程序返回后的某个代码路径上发生的。</p>
<p>除了XDP之外，入站/出站路径都可以用于重定向。标记BPF_F_INGRESS用于指定是ingress还是egress。当前XDP仅仅支持重定向到egress接口，不支持设置flag</p>
<p>对于XDP，成功返回XDP_REDIRECT，出错返回XDP_ABORTED。对于其它eBPF程序，成功返回TC_ACT_REDIRECT，出错返回TC_ACT_SHOT</p>
<div class="blog_h3"><span class="graybg">bpf_redirect_map</span></div>
<p>签名：<pre class="crayon-plain-tag">int bpf_redirect_map(struct bpf_map *map, u32 key, u64 flags)</pre></p>
<p>将封包重定向到map的key键指向的endpoint。根据map的类型，它的值可能指向：</p>
<ol>
<li>网络设备，用于转发封包到其它ports</li>
<li>CPU，用于重定向XDP帧给其它CPU，仅仅支持Native（驱动层支持的） XDP</li>
</ol>
<p>flags必须置零。</p>
<p>当重定向给网络设备时，该函数比bpf_redirect性能更好。这是由一系列底层实现细节之一决定的，其中之一是该函数会以bulk方式将封包发送给设备。</p>
<p>如果成功返回XDP_REDIRECT，否则返回XDP_ABORTED。</p>
<div class="blog_h3"><span class="graybg">bpf_sk_redirect_map</span></div>
<p>签名：<pre class="crayon-plain-tag">int bpf_sk_redirect_map(struct bpf_map *map, u32 key, u64 flags)</pre></p>
<p>将封包重定向给map（类型BPF_MAP_TYPE_SOCKMAP）的key所指向的套接字。ingress/egress接口都可以用于重定向。标记BPF_F_INGRESS用于确定是不是ingress。</p>
<p>如果成功返回SK_PASS，否则返回SK_DROP。</p>
<div class="blog_h3"><span class="graybg">bpf_sock_map_update</span></div>
<p>签名：<pre class="crayon-plain-tag">int bpf_sock_map_update(struct bpf_sock_ops *skops, struct bpf_map *map, void *key, u64 flags)</pre></p>
<p>添加/更新map的条目，skopts作为key的新值。flags是以下其中之一：</p>
<ol>
<li>BPF_NOEXIST，仅添加</li>
<li>BPF_EXIST，仅更新</li>
<li>BPF_ANY，添加或更新</li>
</ol>
<div class="blog_h3"><span class="graybg">bpf_skb_vlan_push</span></div>
<p>签名：<pre class="crayon-plain-tag">int bpf_skb_vlan_push(struct sk_buff *skb, __be16 vlan_proto, u16 vlan_tci)</pre> </p>
<p>将vlan_proto协议的vlan_tci（VLAN Tag控制信息）Push给skb关联的封包，并且更新Checksum。需要注意ETH_P_8021Q和ETH_P_8021AD的vlan_proto是不一样的，这里使用前者。</p>
<p>调用此助手函数会导致封包缓冲区改变，因此在加载期间校验器对指针的校验将失效，必须重新校验。</p>
<div class="blog_h3"><span class="graybg">bpf_skb_vlan_pop</span></div>
<p>签名：<pre class="crayon-plain-tag">int bpf_skb_vlan_pop(struct sk_buff *skb)</pre></p>
<p>弹出skb关联的封包的VLAN头。</p>
<p>调用此助手函数会导致封包缓冲区改变，因此在加载期间校验器对指针的校验将失效，必须重新校验。</p>
<div class="blog_h3"><span class="graybg">bpf_skb_get_tunnel_key</span></div>
<p>签名：<pre class="crayon-plain-tag">int bpf_skb_get_tunnel_key(struct sk_buff *skb, struct bpf_tunnel_key *key, u32 size, u64 flags)</pre></p>
<p>获取隧道（外层报文）的元数据，skb关联的封包的隧道元数据被填充到key，长度size。标记BPF_F_TUNINFO_IPV6提示隧道是基于IPv6而非IPv4。</p>
<p><pre class="crayon-plain-tag">bpf_tunnel_key</pre>是一个容器结构，它将各种隧道协议的主要参数都存入其中，这样eBPF程序可以方便的根据封装（外层）报文的头来作出各种决定。</p>
<p>对端的IP地址被存放在<pre class="crayon-plain-tag">key-&gt;remote_ipv4</pre> 或 <pre class="crayon-plain-tag">key-&gt;remote_ipv6</pre></p>
<p>通过<pre class="crayon-plain-tag">key-&gt;tunnel_id</pre>可以访问隧道的ID，通常映射到VNI（虚拟网络标识符），调用<pre class="crayon-plain-tag">bpf_skb_set_tunnel_key()</pre>函数需要用到</p>
<p>下面这个示例用在隧道一端的TC Ingress接口，可以过滤掉对端隧道IP不是10.0.0.1的封包：</p>
<pre class="crayon-plain-tag">int ret;
struct bpf_tunnel_key key = {};

ret = bpf_skb_get_tunnel_key(skb, &amp;key, sizeof(key), 0);
if (ret &lt; 0)
        return TC_ACT_SHOT;     // drop packet

if (key.remote_ipv4 != 0x0a000001)
        return TC_ACT_SHOT;     // drop packet

return TC_ACT_OK;               // accept packet</pre>
<p>支持VxLAN、Geneve、GRE、IPIP等类型的隧道。</p>
<div class="blog_h3"><span class="graybg">bpf_skb_set_tunnel_key</span></div>
<p>签名：<pre class="crayon-plain-tag">int bpf_skb_set_tunnel_key(struct sk_buff *skb, struct bpf_tunnel_key *key, u32 size, u64 flags)</pre></p>
<p>为skb关联的封包生成隧道元数据。隧道元数据被设置为长度为size的bpf_tunnel_key结构。flags是如下位域的组合：</p>
<ol>
<li>BPF_F_TUNINFO_IPV6 指示隧道基于IPv6而非IPv4</li>
<li>BPF_F_ZERO_CSUM_TX 对于IPv4封包，添加一个标记到隧道元数据，提示应该跳过Checksum计算，将其置零</li>
<li>BPF_F_DONT_FRAGMENT，添加一个标记到隧道元数据，提示封包不得被分片（fragmented）</li>
<li>BPF_F_SEQ_NUMBER，添加一个标记到隧道元数据，提示发送封包之前，需要添加sequence number</li>
</ol>
<p>示例：</p>
<pre class="crayon-plain-tag">struct bpf_tunnel_key key;
// populate key ...
bpf_skb_set_tunnel_key(skb, &amp;key, sizeof(key), 0);
bpf_clone_redirect(skb, vxlan_dev_ifindex, 0);</pre>
<div class="blog_h3"><span class="graybg">bpf_skb_get_tunnel_opt</span></div>
<p>签名：<pre class="crayon-plain-tag">int bpf_skb_get_tunnel_opt(struct sk_buff *skb, u8 *opt, u32 size)</pre></p>
<p>从skb关联的封包中获取隧道选项元数据，并且将原始的隧道选项信息存储到大小为size的opt中。</p>
<div class="blog_h3"><span class="graybg">bpf_skb_set_tunnel_opt</span></div>
<p>签名：<pre class="crayon-plain-tag">int bpf_skb_set_tunnel_opt(struct sk_buff *skb, u8 *opt, u32 size)</pre></p>
<p>将隧道选项元数据设置给skb关联的封包。</p>
<div class="blog_h3"><span class="graybg">bpf_skb_change_proto</span></div>
<p>签名：<pre class="crayon-plain-tag">int bpf_skb_change_proto(struct sk_buff *skb, __be16 proto, u64 flags)</pre></p>
<p>将skb的协议改为proto。目前仅仅支持将IPv4改为IPv6。助手函数会做好底层工作，例如修改套接字缓冲的大小。eBPF程序需要调用<pre class="crayon-plain-tag">skb_store_bytes</pre>填充必要的新的报文头字段，并调用<pre class="crayon-plain-tag">bpf_l3_csum_replace</pre>、<pre class="crayon-plain-tag">bpf_l4_csum_replace</pre>重新计算Checksum。</p>
<p>该助手函数的主要意义是执行一个NAT64操作。</p>
<p>在内部实现上，封包的GSO（generic segmentation offload）类型标记为dodgy，因而报文头被检查，TCP分段被GSO/GRO引擎重新分段。</p>
<p>flags必须清零，这个参数暂时没有使用。</p>
<p>调用此助手函数会导致封包缓冲区改变，因此在加载期间校验器对指针的校验将失效，必须重新校验。</p>
<div class="blog_h3"><span class="graybg">bpf_csum_diff</span></div>
<p>签名：<pre class="crayon-plain-tag">s64 bpf_csum_diff(__be32 *from, u32 from_size, __be32 *to, u32 to_size, __wsum seed)</pre></p>
<p>计算两个缓冲区from到to的checksum difference。</p>
<div class="blog_h3"><span class="graybg">bpf_skb_change_type</span></div>
<p>签名：<pre class="crayon-plain-tag">int bpf_skb_change_type(struct sk_buff *skb, u32 type)</pre></p>
<p>修改封包类型，即设置<pre class="crayon-plain-tag">skb-&gt;pkt_type</pre>为type。主要用途是将skb改为PACKET_HOST。type的取值：</p>
<ol style="list-style-type: undefined;">
<li>PACKET_HOST 单播给本机的封包</li>
<li>PACKET_BROADCAST 广播封包</li>
<li>PACKET_MULTICAST 组播封包</li>
<li>PACKET_OTHERHOST单播给其它机器的封包</li>
</ol>
<div class="blog_h3"><span class="graybg">bpf_skb_change_head</span></div>
<p>签名：<pre class="crayon-plain-tag">int bpf_skb_change_head(struct sk_buff *skb, u32 len, u64 flags)</pre></p>
<p>增长封包的headroom，增长len长度，调整MAC头的偏移量。如果需要，该函数会自动扩展和重新分配内存。</p>
<p>该函数可以用于在L3的skb上，推入一个MAC头，然后将其重定向到L2设备。</p>
<p>flags为保留字段，全部置空。</p>
<p>调用此助手函数会导致封包缓冲区改变，因此在加载期间校验器对指针的校验将失效，必须重新校验。</p>
<div class="blog_h3"><span class="graybg">bpf_skb_under_cgroup</span></div>
<p>签名：<pre class="crayon-plain-tag">int bpf_skb_under_cgroup(struct sk_buff *skb, struct bpf_map *map, u32 index)</pre></p>
<p>检查skb是否是由BPF_MAP_TYPE_CGROUP_ARRAY类型的Map的index位置所指向的CGroup2的descendant。</p>
<p>返回值：</p>
<p style="padding-left: 30px;">0 ：不是目标Cgroup2的descendant<br />1：是目标Cgroup2的descendant<br />负数：出错</p>
<div class="blog_h3"><span class="graybg">bpf_set_hash_invalid</span></div>
<p>签名：<pre class="crayon-plain-tag">void bpf_set_hash_invalid(struct sk_buff *skb)</pre></p>
<p>无效化<pre class="crayon-plain-tag">skb-&gt;hash</pre>。在通过直接封包访问修改报文头之后调用此函数，以提示哈希值以及过期，内核下一次访问哈希或者调用bpf_get_hash_recalc时会触发哈希值的重新计算。</p>
<div class="blog_h3"><span class="graybg">bpf_get_hash_recalc</span></div>
<p>签名：<pre class="crayon-plain-tag">u32 bpf_get_hash_recalc(struct sk_buff *skb)</pre></p>
<p>获取封包哈希值<pre class="crayon-plain-tag">skb-&gt;hash</pre>，如果该字段没有设置（特别是因为封包修改导致哈希被清空）则计算并设置哈希。后续可以直接访问skb-&gt;哈希获取哈希值。</p>
<p>调用bpf_set_hash_invalid()、bpf_skb_change_proto()、bpf_skb_store_bytes()+BPF_F_INVALIDATE_HASH标记，都会导致哈希值清空，并导致下一次bpf_get_hash_recalc()调用重新生成哈希值。</p>
<div class="blog_h3"><span class="graybg">bpf_set_hash</span></div>
<p>签名：<pre class="crayon-plain-tag">u32 bpf_set_hash(struct sk_buff *skb, u32 hash)</pre></p>
<p>设置完整哈希值到skb-&gt;hash</p>
<div class="blog_h3"><span class="graybg">bpf_skb_change_tail</span></div>
<p>签名：<pre class="crayon-plain-tag">int bpf_skb_change_tail(struct sk_buff *skb, u32 len, u64 flags)</pre></p>
<p>Resize(trim/grow) skb关联的封包到len长。flags必须置零。</p>
<p>改变封包长度后，eBPF程序可能需要调用bpf_skb_store_bytes、bpf_l3_csum_replace、bpf_l3_csum_replace等函数填充数据、重新计算Checksum。</p>
<p>一般用于回复ICMP控制报文。</p>
<p>调用此助手函数会导致封包缓冲区改变，因此在加载期间校验器对指针的校验将失效，必须重新校验。</p>
<div class="blog_h3"><span class="graybg">bpf_skb_pull_data</span></div>
<p>签名：<pre class="crayon-plain-tag">int bpf_skb_pull_data(struct sk_buff *skb, u32 len)</pre></p>
<p>所谓non-linear的skb，是指被fragmented的skb，即有一部分数据没有存放在skb所在内存，而是存放在其它内存页（可能有多个），并通过skb_shared_info记录这些数据位置。</p>
<p>当skb是non-linear的、并且不是所有len长是linear section的一部分的前提下，拉取skb的non-linear数据。确保skb的len字节是可读写的。如果len设置为0，则拉取拉取skb的整个长度的数据。</p>
<p>进行封包直接访问时，通过<pre class="crayon-plain-tag">skb-&gt;data_end</pre>来测试某个偏移量是否在封包范围内，可能因为两个原因失败：</p>
<ol>
<li>偏移量是无效的</li>
<li>偏移量对应的数据是在skb的non-linear部分中</li>
</ol>
<p>该助手函数可以用来一次性拉取non-linear数据，然后再进行偏移量测试和数据访问。</p>
<p>此函数确保skb是uncloned，这是直接封包访问的前提。</p>
<p>调用此助手函数会导致封包缓冲区改变，因此在加载期间校验器对指针的校验将失效，必须重新校验。</p>
<div class="blog_h3"><span class="graybg">bpf_get_socket_cookie</span></div>
<p>签名：<pre class="crayon-plain-tag">u64 bpf_get_socket_cookie(struct sk_buff *skb)</pre></p>
<p>如果skb关联到一个已知的套接字，则得到套接字的cookie（由内核生成），如果尚未设置cookie，则生成之。一旦cookie生成，在套接字的生命周期范围内都不会改变。</p>
<p>该助手用于监控套接字网络流量统计信息，它在网络命名空间范围内为套接字提供唯一标识。</p>
<div class="blog_h3"><span class="graybg">bpf_get_socket_uid</span></div>
<p>签名：<pre class="crayon-plain-tag">u32 bpf_get_socket_uid(struct sk_buff *skb)</pre></p>
<p>获得套接字的owner UID。如果套接字是NULL，或者不是full socket（time-wait状态，或者是一个request socket），则返回overflowuid，overflowuid不一定是socket的实际UID。</p>
<div class="blog_h3"><span class="graybg">bpf_csum_update</span></div>
<p>签名：<pre class="crayon-plain-tag">s64 bpf_csum_update(struct sk_buff *skb, __wsum csum)</pre></p>
<p>如果驱动已经为整个封包提供了Checksum，那么此函数将csum加到<pre class="crayon-plain-tag">skb-&gt;csum</pre>字段上，其它情况下返回错误。</p>
<p>该助手函数应当和<pre class="crayon-plain-tag">bpf_csum_diff()</pre>联合使用，典型场景是，通过封包直接访问修改了封包内容之后，进行Checksum更新。</p>
<div class="blog_h3"><span class="graybg">bpf_get_route_realm</span></div>
<p>签名：<pre class="crayon-plain-tag">u32 bpf_get_route_realm(struct sk_buff *skb)</pre></p>
<p>得到路由的Realm，也就是skb的destination的tclassid字段。这个字段是用户提供的一个tag，类似于net_cls的classid。不同的是，这里的tag关联到路由条目（destination entry）。</p>
<p>可以在clsact TC egress钩子中调用此函数，或者在经典的classful egress qdiscs上使用。不能在TC ingress路径上使用。</p>
<p>要求内核配置选项CONFIG_IP_ROUTE_CLASSID。</p>
<p>返回skb关联的封包的路由的realm。</p>
<div class="blog_h3"><span class="graybg">bpf_setsockopt</span></div>
<p>签名：<pre class="crayon-plain-tag">int bpf_setsockopt(struct bpf_sock_ops *bpf_socket, int level, int optname, char *optval, int optlen)</pre></p>
<p>针对bpf_socket关联的套接字发起一个setsockopt()操作，此套接字必须是full socket。optname为选项名，optval/optlen指定了选项值，level指定了选项的位置。</p>
<p>该函数实际上实现了setsockopt()的子集，支持以下level：</p>
<ol>
<li>SOL_SOCKET，支持选项SO_RCVBUF, SO_SNDBUF, SO_MAX_PACING_RATE, SO_PRIORITY, SO_RCVLOWAT, SO_MARK</li>
<li>IPPROTO_TCP，支持选项TCP_CONGESTION, TCP_BPF_IW, TCP_BPF_SNDCWND_CLAMP</li>
<li>IPPROTO_IP，支持选项IP_TOS</li>
<li>IPPROTO_IPV6，支持选项IPV6_TCLASS</li>
</ol>
<div class="blog_h3"><span class="graybg">bpf_skb_adjust_room</span></div>
<p>签名：<pre class="crayon-plain-tag">int bpf_skb_adjust_room(struct sk_buff *skb, u32 len_diff, u32 mode, u64 flags)</pre></p>
<p>增加/缩小skb关联的封包的数据的room，增量为len_diff。mode可以是：</p>
<ol>
<li>BPF_ADJ_ROOM_NET，在网络层调整room，即在L3头上增加/移除room space</li>
</ol>
<p>flags必须置零。</p>
<p>调用此助手函数会导致封包缓冲区改变，因此在加载期间校验器对指针的校验将失效，必须重新校验。</p>
<div class="blog_h3"><span class="graybg">bpf_xdp_adjust_head</span></div>
<p>签名：<pre class="crayon-plain-tag">int bpf_xdp_adjust_head(struct xdp_buff *xdp_md, int delta)</pre></p>
<p>移动<pre class="crayon-plain-tag">xdp_md-&gt;data</pre> delta字节，delta可以是负数。</p>
<p>该函数准备用于push/pop headers的封包。</p>
<p>调用此助手函数会导致封包缓冲区改变，因此在加载期间校验器对指针的校验将失效，必须重新校验。</p>
<div class="blog_h3"><span class="graybg">bpf_xdp_adjust_meta</span></div>
<p>签名：<pre class="crayon-plain-tag">int bpf_xdp_adjust_meta(struct xdp_buff *xdp_md, int delta)</pre></p>
<p>调整 xdp_md-&gt;data_meta所指向的地址delta字节。该操作改变了存储在xdp_md-&gt;data中的地址信息。</p>
<div class="blog_h3"><span class="graybg">bpf_get_current_task</span></div>
<p>签名：<pre class="crayon-plain-tag">u64 bpf_get_current_task(void)</pre></p>
<p>获取当前Task结构的指针。</p>
<div class="blog_h3"><span class="graybg">bpf_get_stackid</span></div>
<p>签名：<pre class="crayon-plain-tag">int bpf_get_stackid(struct pt_reg *ctx, struct bpf_map *map, u64 flags)</pre></p>
<p>获取一个用户/内核栈，得到其ID。需要传入ctx，即当前追踪程序在其中执行的上下文对象，以及一个BPF_MAP_TYPE_STACK_TRACE类型的Map。通过flags指示需要跳过多少栈帧（0-255），masked with BPF_F_SKIP_FIELD_MASK。flags的其它位如下：</p>
<ol>
<li>BPF_F_USER_STACK 收集用户空间的栈，而非内核栈</li>
<li>BPF_F_FAST_STACK_CMP 基于哈希来对比栈</li>
<li>BPF_F_REUSE_STACKID 如果两个不同的栈哈希到同一个stackid，丢弃旧的</li>
</ol>
<div class="blog_h3"><span class="graybg">bpf_get_current_pid_tgid</span></div>
<p>签名：<pre class="crayon-plain-tag">u64 bpf_get_current_pid_tgid(void)</pre></p>
<p>返回一个包含了当前tgid和pid的64bit整数。值为<pre class="crayon-plain-tag">current_task-&gt;tgid &lt;&lt; 32 | current_task-&gt;pid</pre></p>
<div class="blog_h3"><span class="graybg">bpf_get_current_uid_gid</span></div>
<p>签名：<pre class="crayon-plain-tag">u64 bpf_get_current_uid_gid(void)</pre></p>
<p>返回一个包含了当前GID和UID的整数。值为<pre class="crayon-plain-tag">current_gid &lt;&lt; 32 | current_uid</pre></p>
<div class="blog_h3"><span class="graybg">bpf_get_current_comm</span></div>
<p>签名：<pre class="crayon-plain-tag">int bpf_get_current_comm(char *buf, u32 size_of_buf)</pre></p>
<p>将当前任务的comm属性拷贝到长度为size_of_buf的buf中。comm属性包含可执行文件的路径</p>
<p>调用成功时助手函数确保buf是NULL-terminated。如果失败，则填满0</p>
<div class="blog_h3"><span class="graybg">bpf_get_cgroup_classid</span></div>
<p>签名：<pre class="crayon-plain-tag">u32 bpf_get_cgroup_classid(struct sk_buff *skb)</pre></p>
<p>得到当前任务的classid，即skb所属的<a href="/cgroup-illustrated#net-cls">net_cls控制组</a>的classid。该助手函数可用于TC的egress路径，不能用于ingress路径。</p>
<p>Linux支持两个版本的Cgroups，v1和v2，用户可以混合使用。但是，net_cls是v1特有的Cgroup。这意味着此助手函数和run on cgroups（v2 only）的eBPF程序不兼容，套接字一次仅仅能携带一个版本Cgroup的数据。</p>
<p>内核必须配置CONFIG_CGROUP_NET_CLASSID=y/m才能使用此助手函数。</p>
<p>返回classid，或者0，即默认的没有被配置的classid。</p>
<div class="blog_h3"><span class="graybg">bpf_probe_write_user</span></div>
<p>签名：<pre class="crayon-plain-tag">int bpf_probe_write_user(void *dst, const void *src, u32 len)</pre></p>
<p>尝试在以一个安全方式来写入src的len字节到dst中。仅仅对于运行在用户上下文的线程可用，dst必须是有效的用户空间地址。</p>
<p>由于TOC-TOU攻击的原因，此助手函数不得用于实现任何类型的安全机制。</p>
<p>此函数用于试验目的，存在的导致系统、进程崩溃的风险。当调用了此函数的eBPF程序被挂钩后，内核日志会打印一条警告信息，包含PID和进程名信息。</p>
<div class="blog_h3"><span class="graybg">bpf_probe_read_str</span></div>
<p>签名：<pre class="crayon-plain-tag">int bpf_probe_read_str(void *dst, int size, const void *unsafe_ptr)</pre></p>
<p>从unsafe_ptr拷贝一个NULL结尾的字符串到dst，size包含结尾的NULL字符。如果字符串长度小于size，不会补NUL；如果字符串长度大于size，则截断（保证填充字符串结尾NULL）</p>
<div class="blog_h3"><span class="graybg">bpf_current_task_under_cgroup</span></div>
<p>签名：<pre class="crayon-plain-tag">int bpf_current_task_under_cgroup(struct bpf_map *map, u32 index)</pre></p>
<p>检查当前正在运行的探针是否在map的index所指向的Cgroup2之下。</p>
<div class="blog_h3"><span class="graybg">bpf_get_numa_node_id</span></div>
<p>签名： <pre class="crayon-plain-tag">int bpf_get_numa_node_id(void)</pre></p>
<p>得到当前NUMA节点的ID。该函数的主要目的是用于选取本地NUMA节点的套接字。</p>
<div class="blog_h3"><span class="graybg">bpf_perf_event_output</span></div>
<p>签名：<pre class="crayon-plain-tag">int bpf_perf_event_output(struct pt_reg *ctx, struct bpf_map *map, u64 flags, void *data, u64 size)</pre></p>
<p>将长度为size的blob写入到Map所存放的特殊BPF perf event。map的类型是BPF_MAP_TYPE_PERF_EVENT_ARRAY</p>
<p>perf event必须具有属性：</p>
<p style="padding-left: 30px;">sample_type = PERF_SAMPLE_RAW<br />type = PERF_TYPE_SOFTWARE<br />config = PERF_COUNT_SW_BPF_OUTPUT</p>
<p>flags用于指定写入到数组的索引。masked by BPF_F_INDEX_MASK，如果指定BPF_F_CURRENT_CPU则取当前CPU的值。</p>
<p>当前程序的ctx也需要传递给助手函数。</p>
<p>在用户空间，希望读取值的程序需要针对perf event调用perf_event_open() ，然后将文件描述符存储到Map中。这个操作必须在eBPF程序第一次写入数据到Map之前完成。参考内核中的例子samples/bpf/trace_output_user.c </p>
<p>要和用户空间进行数据交互，该函数优于bpf_trace_printk()，性能更好。适合从eBPF程序stream数据给用户空间读取。</p>
<div class="blog_h3"><span class="graybg">bpf_perf_event_read</span></div>
<p>签名： <pre class="crayon-plain-tag">u64 bpf_perf_event_read(struct bpf_map *map, u64 flags)</pre></p>
<p>读取一个perf event counter的值，该助手函数操作BPF_MAP_TYPE_PERF_EVENT_ARRAY类型的Map。这个Map本质上是一个数组，它的size和CPU数量一致，其值和对应CPU相关。取哪个CPU的值，masked by BPF_F_INDEX_MASK，如果指定BPF_F_CURRENT_CPU则取当前CPU的值。</p>
<p>在4.13之前，仅仅支持hardware perf event。</p>
<p>成功时返回计数器值，否则返回负数。</p>
<p>考虑使用<pre class="crayon-plain-tag">bpf_perf_event_read_value</pre>代替此函数。</p>
<div class="blog_h2"><span class="graybg">BPF Skeleton</span></div>
<p>一个BPF Application由1-N个BPF program、BPF Maps、全局变量组成。所有BPF program（各自对应一个ELF section）、用户空间（的控制）程序可以共享Map/全局变量。</p>
<div class="blog_h3"><span class="graybg">管理生命周期</span></div>
<p>BPF Application通常会经历以下生命周期阶段：</p>
<ol>
<li>Open：控制程序打开BPF object文件，解析了programs/map/global vars，但是尚未在内核中创建这些对象。打开BPF object文件后，控制程序可能进行一些调整，例如设置程序类型、设置全局变量的值，等等</li>
<li>Load：在此阶段，BPF Maps被创建，各种relocations被解析，BPF programs被载入内核并校验。这个阶段结束时，BPF程序的所有组件都被校验并且存在于内核，但是BPF program还不会被内核执行，可以保证在没有竞态条件的前提下，对BPF Map进行初始化</li>
<li>Attach：BPF programs被挂钩到相应的BPF挂钩点（例如tracepoint、kprobes、cgroup hooks、网络封包处理流水线…），BPF program开始工作，读写BPF Maps和全局变量</li>
<li>Teardown：BPF程序被detach并unload，BPF Map被销毁</li>
</ol>
<p>通过bpftool生成的BPF Skeleton，包含了触发上述阶段的函数：</p>
<ol>
<li><pre class="crayon-plain-tag">&lt;name&gt;__open()</pre>：创建并打开BPF Application</li>
<li><pre class="crayon-plain-tag">&lt;name&gt;__load()</pre>：实例化、加载、校验BPF Application组件</li>
<li><pre class="crayon-plain-tag">&lt;name&gt;__attach()</pre>：将BPF programs挂钩到内核的hook point。你也可以选择直接使用libbpf API进行细粒度控制</li>
<li><pre class="crayon-plain-tag">&lt;name&gt;__destroy()</pre>：销毁BPF programs并释放所有资源</li>
</ol>
<div class="blog_h3"><span class="graybg">访问全局变量</span></div>
<p>在内核空间，访问全局变量使用普通的C语法，你甚至可以取地址并将其传递给助手函数。</p>
<p>在控制程序中，你需要通过BPF skeleton来访问这些变量：</p>
<ol>
<li><pre class="crayon-plain-tag">skel-&gt;rodata</pre>，访问只读变量（常量）</li>
<li><pre class="crayon-plain-tag">skel-&gt;bss</pre>，访问可变的、以0初始化的变量</li>
<li><pre class="crayon-plain-tag">skel-&gt;data</pre>，访问可变的、非0初始化的变量</li>
</ol>
<p>在用户空间对这些变量的修改，会立刻反映到内核空间。</p>
<div class="blog_h2"><span class="graybg">BPF Ring Buffer</span></div>
<p>BPF ring buffer是一个新的BPF数据结构，它解决了BPF perf buffer（现有的给用户空间发送数据的事实标准工具）的内存效率、事件re-ordering问题。</p>
<p>ringbuf提供了兼容perfbuf的API，便于迁移。同时也提供了新的reserve/commit API，具有更好的易用性。</p>
<div class="blog_h3"><span class="graybg">对比perfbuf</span></div>
<p>当BPF程序将收集的信息发送到用户空间，供后续处理时，通常会利用perfbuf。perfbuf是per-CPU的环形缓冲区，允许内核和用户空间进行高效的数据交换。但是由于它的per-CPU设计，会导致两个缺点：内存低效、事件乱序。</p>
<p>因为这些缺点，内核5.8+开始，BPF提供了新的数据结构，BPF ring buffer。它是一个多生产者、单消费者（MPSC）的队列，可以安全的被多CPU环境共享。</p>
<p>ringbuf支持perfbuf的特性：</p>
<ol>
<li>可变长度数据记录</li>
<li>基于内存映射区域，高效的从用户空间读取数据的能力，不需要内存拷贝或者执行系统调用</li>
<li>支持epoll通知，或者忙循环（最小化延迟）</li>
</ol>
<div class="blog_h3"><span class="graybg">内存低效问题</span></div>
<p>perfbuf为每个CPU都分配了独立的缓冲区，开发者可能需要进行权衡：</p>
<ol>
<li>如果分配足够大的缓冲，则会浪费内存，特别是核心数很多的情况下</li>
<li>如果分配较小的缓冲，那么出现事件spike时，会丢失数据</li>
</ol>
<p>而ringbuf使用单个缓冲区，因而能够容易应对spike，同时不需要太大的内存消耗。</p>
<div class="blog_h3"><span class="graybg">事件乱序问题</span></div>
<p>在追踪相关事件（例如进程启动/退出、网络连接生命周期事件）的时候，事件顺序非常重要。</p>
<p>使用perfbuf时，如果两个相关事件在很短事件内（若干ms）被不同CPU处理，则可能发出到用户空间的顺序是错乱的。</p>
<div class="blog_h3"><span class="graybg">关于性能</span></div>
<p>在所有应用场景下，BPF ringbuf都有着和perfbuf可比较的性能。</p>
<p>唯一需要考虑使用perfbuf的场景是在NMI (non-maskable interrupt)上下文下运行的BPF程序，例如处理cpu-cycles之类的perf事件。由于ringbuf内部使用一个轻量的自旋锁，在NMI上下文下可能发生锁争用并导致reserve失败。</p>
<div class="blog_h3"><span class="graybg">用法对比</span></div>
<p>为了简化代码的迁移，ringbuf提供了一套类似于perfbuf的API，本节做一个对比。</p>
<p>下面的数据结构代表BPF程序需要收集的一个事件：</p>
<pre class="crayon-plain-tag">#define TASK_COMM_LEN 16
#define MAX_FILENAME_LEN 512

struct event {
	int pid;
	char comm[TASK_COMM_LEN];
	char filename[MAX_FILENAME_LEN];
};</pre>
<p>perfbuf和ringbuf对应不同的Map类型：</p>
<pre class="crayon-plain-tag">/*** perfbuf ***/
struct {
    __uint(type, BPF_MAP_TYPE_PERF_EVENT_ARRAY);
    __uint(key_size, sizeof(int));
    __uint(value_size, sizeof(int));
} pb SEC(".maps");

/*** ringbuf ***/
struct {
    __uint(type, BPF_MAP_TYPE_RINGBUF);
    // 和perfbuf不同，ringbuf的尺寸可以在内核端定义
    // 但是，在用户空间也可以通过bpf_map__set_max_entries()指定或覆盖ringbuf的尺寸
    // 单位是字节，必须是内核页（一般都是4KB）大小的整数倍，并且是2的幂
    __uint(max_entries, 256 * 1024 /* 256 KB */);
} rb SEC(".maps");</pre>
<p>由于event超过512字节，因此不能直接在栈上分配，需要存储到BPF_MAP_TYPE_PERCPU_ARRAY：</p>
<pre class="crayon-plain-tag">struct {
	__uint(type, BPF_MAP_TYPE_PERCPU_ARRAY);
	__uint(max_entries, 1);  // 只需要1个元素即可
	__type(key, int);
	__type(value, struct event);
} heap SEC(".maps");</pre>
<p>内核空间代码：</p>
<pre class="crayon-plain-tag">// 挂钩到sched:sched_process_exec，每当成功的exec()系统调用后触发
SEC("tp/sched/sched_process_exec")
int handle_exec(struct trace_event_raw_sched_process_exec *ctx)
{
	unsigned fname_off = ctx-&gt;__data_loc_filename &amp; 0xFFFF;
	struct event *e;
	int zero = 0;
	// 获得事件的指针
	e = bpf_map_lookup_elem(&amp;heap, &amp;zero);
	if (!e) /* can't happen */
		return 0;

	// 填充字段
	e-&gt;pid = bpf_get_current_pid_tgid() &gt;&gt; 32;
	bpf_get_current_comm(&amp;e-&gt;comm, sizeof(e-&gt;comm));
	bpf_probe_read_str(&amp;e-&gt;filename, sizeof(e-&gt;filename), (void *)ctx + fname_off);

	/*** 填充数据的API不同 ***/
	/*** perfbuf ***/
	// 输出追踪样本，要求在perfbuf中保留e大小的缓冲区，然后通知用户空间有数据可用
	bpf_perf_event_output(ctx, &amp;pb, BPF_F_CURRENT_CPU, e, sizeof(*e));
        /*** ringbuf ***/
	// 不需要传递ctx对象
	bpf_ringbuf_output(&amp;rb, e, sizeof(*e), 0);

	return 0;
}</pre>
<p>用户空间代码：</p>
<pre class="crayon-plain-tag">struct perf_buffer *pb = NULL;
struct perf_buffer_opts pb_opts = {};
struct perfbuf_output_bpf *skel;

/*** 回调函数的签名不同 ***/
/*** perfbuf ***/
void handle_event(void *ctx, int cpu, void *data, unsigned int data_sz)
{
    const struct event *e = data;
    struct tm *tm;
    char ts[32];
    time_t t;
    time(&amp;t);
    tm = localtime(&amp;t);
    strftime(ts, sizeof(ts), "%H:%M:%S", tm);

    printf("%-8s %-5s %-7d %-16s %s\n", ts, "EXEC", e-&gt;pid, e-&gt;comm, e-&gt;filename);
}
/*** ringbuf ***/
int handle_event(void *ctx, void *data, size_t data_sz)
{
    // ...
}

/*** 创建用户空间缓冲区的API不同 ***/
/*** perfbuf ***/
pb_opts.sample_cb = handle_event; // 回调函数
//                    指向内核空间对应物，也就是那个Map
//                                                为每个CPU分配8个页，也就是32KB的缓冲区
pb = perf_buffer__new(bpf_map__fd(skel-&gt;maps.pb), 8 /* 32KB per CPU */, &amp;pb_opts);
if (libbpf_get_error(pb)) {
    err = -1;
    fprintf(stderr, "Failed to create perf buffer\n");
    goto cleanup;
}
/*** ringbuf ***/
rb = ring_buffer__new(bpf_map__fd(skel-&gt;maps.rb), handle_event, NULL, NULL);
if (!rb) {
    err = -1;
    fprintf(stderr, "Failed to create ring buffer\n");
    goto cleanup;
}


// 开始epoll轮询
while (!exiting) {
    /*** 轮询接口不同 ***/
    /*** perfbuf ***/
    err = perf_buffer__poll(pb, 100 /* timeout, ms */);
    /*** ringbuf ***/
    err = ring_buffer__poll(rb, 100 /* timeout, ms */);
    
    // ...
}</pre>
<div class="blog_h3"><span class="graybg"> reserve/commit</span></div>
<p>上面的perfbuf兼容API，具有与perfbuf类似的缺点：</p>
<ol>
<li>额外的内存拷贝：你需要额外的内存空间来构建追踪样本对象，然后才能将它拷贝到缓冲区中</li>
<li>very late data reservation：构建（可能需要采集多种内存数据）追踪样本对象的工作可能是无意义的，如果缓冲区中剩余空间（由于用户空间程序处理缓慢或者事件burst）不足，构建的对象无处存放。使用xxx_output()接口不能提前感知缓冲区剩余空间，从而避免不必要的样本对象构造</li>
</ol>
<p>ringbuf提供了一套新的API，应对上述缺点。</p>
<ol>
<li><pre class="crayon-plain-tag">bpf_ringbuf_reserve()</pre>：尝试在缓冲区中预定空间，这个操作可以在构建样本对象之前就进行，尽早获取空间不足的状况。如果预定成功，则返回指针，并且可以保证后续可以将数据commit到其中；如果预定失败，则返回NULL，我们可以跳过后续的、无意义的操作</li>
<li><pre class="crayon-plain-tag">bpf_ringbuf_commit()</pre>：将样本对象发送到缓冲区</li>
</ol>
<p>此外，reserve的空间，在commit之前，对于用户空间是不可见的。因此你可以直接用它来构造样本对象，不用担心半初始化的对象被看到，同时达到节约内存的目的。</p>
<p>唯一的限制是：reserve的空间大小，必须能够被BPF verifier感知（常量）。如果样本尺寸是动态的，则只能使用<pre class="crayon-plain-tag">bpf_ringbuf_output()</pre>并且承受内存拷贝的代价。</p>
<p>用法示例：</p>
<pre class="crayon-plain-tag">// 不再需要到per-CPU array中预先构造对象：
//   e = bpf_map_lookup_elem(&amp;heap, &amp;zero);
e = bpf_ringbuf_reserve(&amp;rb, sizeof(*e), 0);

// 不再需要将per-CPU array中的对象拷贝到缓冲区
//   bpf_ringbuf_output(&amp;rb, e, sizeof(*e), 0);
bpf_ringbuf_submit(e, 0);</pre>
<div class="blog_h3"><span class="graybg">通知用户空间</span></div>
<p>不管是perfbuf还是ringbuf，如果内核每写入一个样本，就通知（也就是唤醒在poll/epoll上等待的）用户空间程序，其开销是相当大的。</p>
<p>perfbuf的解决办法是，允许用户空间程序构造perfbuf时，指定每发生N个样本才唤醒一次，这可能会让你丢失最多N-1个样本。</p>
<p>ringbuf则允许为<pre class="crayon-plain-tag">bpf_ringbuf_output()</pre>或<pre class="crayon-plain-tag">bpf_ringbuf_commit()</pre>指定额外的标记：</p>
<ol>
<li>BPF_RB_FORCE_WAKEUP 本次写入样本，强制唤醒用户空间程序</li>
<li>BPF_RB_NO_WAKEUP本次写入样本，不会唤醒用户空间程序</li>
</ol>
<p>如果不指定上述标记，ringbuf会根据用户空间程序lagging的情况决定是否唤醒。不指定标记通常是安全的默认值。</p>
<div class="blog_h2"><span class="graybg">调试BPF程序</span></div>
<p>BPF没有常规的调试工具，支持设置断点、探查变量、单步跟踪的那种。</p>
<div class="blog_h3"><span class="graybg">bpf_printk</span></div>
<p>使用<pre class="crayon-plain-tag">bpf_printk(fmt, args...)</pre>可以打印一些信息到<pre class="crayon-plain-tag">/sys/kernel/debug/tracing/trace_pipe</pre>，来帮助你理解发生了什么，这个函数最多支持3个args。</p>
<p>这个函数的成本很高，不能在生产环境使用。</p>
<div class="blog_h3"><span class="graybg">bpf_trace_printk</span></div>
<p>助手函数<pre class="crayon-plain-tag">long bpf_trace_printk(const char *fmt, __u32 fmt_size, ...)</pre>是bpf_printk的wrapper。</p>
<div class="blog_h2"><span class="graybg">libbpf-bootstrap</span></div>
<p>这是libbpf提供的脚手架，可以让你快速开始编写自己的eBPF程序。该脚手架目前提供一些demo程序：</p>
<ol>
<li>minimal：最小化的、能编译、加载、运行的BPF hello world程序</li>
<li>bootstrap：现实可用的、可移植的、全功能的BPF程序，依赖BPF CO-RE。需要当前使用的内核开启<pre class="crayon-plain-tag">CONFIG_DEBUG_INFO_BTF=y</pre>，该demo还示例了5.5+支持的BPF全局变量、5.8+支持的BPF ring buffer</li>
</ol>
<p>libbpf-bootstrap自带了libbpf（作为git子模块）和bpftool（仅x84），避免依赖你的Linux发行版所携带的特定版本。libbpf依赖的库包括libelf、zlib，需要确保已经安装。</p>
<p>执行下面的命令获得脚手架：</p>
<pre class="crayon-plain-tag">git clone https://github.com/libbpf/libbpf-bootstrap.git
git submodule init</pre>
<p>目录结构如下：</p>
<pre class="crayon-plain-tag">.
├── examples
│   ├── c  # C代码示例
│   │   ├── bootstrap.bpf.c  # 在内核空间执行的BPF程序
│   │   ├── bootstrap.c      # 在用户空间执行的逻辑，负责加载BPF字节码，在程序生命周期内和BPF程序交互
│   │   ├── bootstrap.h      # 用户/内核空间程序共享的头
│   │   ├── CMakeLists.txt
│   │   ├── fentry.bpf.c
│   │   ├── fentry.c
│   │   ├── kprobe.bpf.c
│   │   ├── kprobe.c
│   │   ├── Makefile
│   │   ├── minimal.bpf.c
│   │   ├── minimal.c
│   │   ├── uprobe.bpf.c
│   │   ├── uprobe.c
│   │   └── xmake.lua
│   └── rust # Rust代码示例
│       ├── Cargo.lock
│       ├── Cargo.toml
│       └── xdp
│           ├── build.rs
│           ├── Cargo.lock
│           ├── Cargo.toml
│           └── src
│               ├── bpf
│               │   ├── vmlinux.h -&gt; ../../../../../vmlinux/vmlinux.h
│               │   └── xdppass.bpf.c
│               └── main.rs
├── libbpf # 自带的libbpf
├── LICENSE
├── README.md
├── tools  # 自带bpftool，用于为你的BPF代码构建BPF skeleton、生成vmlinux.h头
│   ├── bpftool
│   ├── cmake
│   │   ├── FindBpfObject.cmake
│   │   └── FindLibBpf.cmake
│   └── gen_vmlinux_h.sh  # 用于生成自定义的vmlinux.h
└── vmlinux
    ├── vmlinux_508.h
    └── vmlinux.h -&gt; vmlinux_508.h  # 预生成的vmlinux.h</pre>
<div class="blog_h3"><span class="graybg">minimal</span></div>
<p>最小化的样例，不依赖BPF CO-RE，可以在很老版本的内核上运行。该程序会安装一个每秒触发一次的tracepoint handler，它使用<pre class="crayon-plain-tag">bpf_printk()</pre>和外部通信，你可以通过/sys/kernel/debug/tracing/trace_pipe查看输出。</p>
<p>构建此样例：</p>
<pre class="crayon-plain-tag">cd examples/c
make minimal</pre>
<p>内核空间代码：</p>
<pre class="crayon-plain-tag">// 包含基本的BPF相关的类型和常量。为了使用内核空间BPF API，例如助手函数的flags，需要引入此头
#include &lt;linux/bpf.h&gt;
// 由libbpf提供，包含了大部分常用宏、常量、助手函数，几乎每个BPF程序都会使用
#include &lt;bpf/bpf_helpers.h&gt;
// 这个变量定义你的代码的License，内核强制要求此字段存在，某些功能对于非GPL兼容的License不可用
//             必须定义在license段
char LICENSE[] SEC("license") = "Dual BSD/GPL";
// 全局变量，要求内核版本5.5+，全局变量甚至可以从用户空间读写
// 可以用于配置BPF程序、存放轻量的统计数据、在内核和用户空间传递数据
int my_pid = 0;

// 这里定义了BPF内核程序
// 这个注解，说明了需要创建的BPF程序类型，以及如何挂钩到内核
//   tracepoint BPF程序
//      在进入write系统调用时触发
SEC("tp/syscalls/sys_enter_write")
int handle_tp(void *ctx)
{
	// 调用助手函数，获得PID（内核的术语叫TGID）。为了节约空间，很多助手函数使用高低字节存储不同的数据
	int pid = bpf_get_current_pid_tgid() &gt;&gt; 32;

	// 判断是否是关注进程发出的系统调用
	if (pid != my_pid)
		return 0;
	// 如果是，打印信息到/sys/kernel/debug/tracing/trace_pipe
	// 注意，由于性能问题，这个函数不能用于生产环境
	bpf_printk("BPF triggered from PID %d.\n", pid);

	return 0;
}

// 你还可以定义更多的BPF程序，只需要为它们声明适当的SEC即可。所有这些程序共享全局变量</pre>
<p>用户空间代码：</p>
<pre class="crayon-plain-tag">#include &lt;stdio.h&gt;
#include &lt;unistd.h&gt;
#include &lt;sys/resource.h&gt;
#include &lt;bpf/libbpf.h&gt;
// 由bpftool自动生成的，映射了minimal.bpf.c的高层结构的BPF skeleton
// 编译后的BPF object被内嵌到此头文件（也就是用户空间代码）中，简化了开发和部署
// 文件路径 .output/&lt;app&gt;.skel.h
#include "minimal.skel.h"

// 此回调打印libbpf日志到控制台
static int libbpf_print_fn(enum libbpf_print_level level, const char *format, va_list args)
{
	return vfprintf(stderr, format, args);
}

static void bump_memlock_rlimit(void)
{
	struct rlimit rlim_new = {
		.rlim_cur	= RLIM_INFINITY,
		.rlim_max	= RLIM_INFINITY,
	};

	if (setrlimit(RLIMIT_MEMLOCK, &amp;rlim_new)) {
		fprintf(stderr, "Failed to increase RLIMIT_MEMLOCK limit!\n");
		exit(1);
	}
}

int main(int argc, char **argv)
{
	struct minimal_bpf *skel;
	int err;

	// 为所有libbpf日志设置回调函数
	libbpf_set_print(libbpf_print_fn);

	// 增大内核内部的per-user内存限制，允许BPF子系统为程序、Map分配足够的资源
	bump_memlock_rlimit();

	// 打开BPF skeleton
	skel = minimal_bpf__open();
	if (!skel) {
		fprintf(stderr, "Failed to open BPF skeleton\n");
		return 1;
	}

	// 访问BSS段中的全局变量
	skel-&gt;bss-&gt;my_pid = getpid();

	// 加载和校验BPF程序
	err = minimal_bpf__load(skel);
	if (err) {
		fprintf(stderr, "Failed to load and verify BPF skeleton\n");
		goto cleanup;
	}

	// 挂钩BPF程序，在此即注册tracepoint handler
	// libbpf能够根据SEC注解，自动的为大部分BPF程序类型（tracepoints, kprobes等）选择适当的挂钩点
	// 如果不能满足需求，可以调用libbpf提供的函数手工挂钩的API
	err = minimal_bpf__attach(skel);
	if (err) {
		fprintf(stderr, "Failed to attach BPF skeleton\n");
		goto cleanup;
	}

	printf("Successfully started! Please run `sudo cat /sys/kernel/debug/tracing/trace_pipe` "
	       "to see output of the BPF programs.\n");

	// 触发write系统调用，进而触发BPF程序
	for (;;) {
		/* trigger our BPF program */
		fprintf(stderr, ".");
		sleep(1);
	}

cleanup:
	// 清除所有内核/用户空间的资源
	// 在大部分情况下，即使程序崩溃，没有清理，内核也会作自动清理
	minimal_bpf__destroy(skel);
	return -err;
}</pre>
<p>构建此程序的Makefile：</p>
<pre class="crayon-plain-tag"># SPDX-License-Identifier: (LGPL-2.1 OR BSD-2-Clause)
OUTPUT := .output
CLANG ?= clang
LLVM_STRIP ?= llvm-strip
BPFTOOL ?= $(abspath ../../tools/bpftool)
LIBBPF_SRC := $(abspath ../../libbpf/src)
LIBBPF_OBJ := $(abspath $(OUTPUT)/libbpf.a)
# vmlinux.h由bpftool从当前运行的内核中抽取出，包含了所有内核使用的类型定义
VMLINUX := ../../vmlinux/vmlinux.h
# 使用自己的libbpf API头文件、Linux UAPI头文件，避免依赖当前系统的（可能缺失或过期的）头文件
INCLUDES := -I$(OUTPUT) -I../../libbpf/include/uapi -I$(dir $(VMLINUX))
# 使用-g保留调试信息
CFLAGS := -g -Wall
# 体系结构信息会传递给后续的BPF构建步骤，使用bpf_tracing.h中低级别tracing助手函数需要此信息
ARCH := $(shell uname -m | sed 's/x86_64/x86/')

# 这个示例项目包含多个demo，每个对应一个APP
APPS = minimal bootstrap uprobe kprobe fentry

# 获取Clang在当前系统上默认的includes目录，当以-target bpf编译时，这些目录被显式添加到
# include列表。如果不这样做，某些体系结构/发行版下，体系结构特定的目录可能missing，诸如
# asm/types.h, asm/byteorder.h, asm/socket.h, asm/sockios.h, sys/cdefs.h之类
# 的头文件可能missing
CLANG_BPF_SYS_INCLUDES = $(shell $(CLANG) -v -E - &lt;/dev/null 2&gt;&amp;1 \
	| sed -n '/&lt;...&gt; 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

.PHONY: all
all: $(APPS)

.PHONY: clean
clean:
	$(call msg,CLEAN)
	$(Q)rm -rf $(OUTPUT) $(APPS)

$(OUTPUT) $(OUTPUT)/libbpf:
	$(call msg,MKDIR,$@)
	$(Q)mkdir -p $@

# 1. 构建 libbpf 为静态库，存放在.output目录下
#    如果希望和系统的libbpf共享库链接，去掉这个目标
$(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

# 2. 构建BPF object
$(OUTPUT)/%.bpf.o: %.bpf.c $(LIBBPF_OBJ) $(wildcard %.h) $(VMLINUX) | $(OUTPUT)
	$(call msg,BPF,$@)
	# 必须使用-g -O2     目标必须是bpf 为bpf_tracing.h定义必要的宏
	# 
	$(Q)$(CLANG) -g -O2 -target bpf -D__TARGET_ARCH_$(ARCH) $(INCLUDES) $(CLANG_BPF_SYS_INCLUDES) -c $(filter %.c,$^) -o $@
	$(Q)$(LLVM_STRIP) -g $@ # 去除DWARF信息，从来不会用到。由于BPF程序最终以文本形式嵌入到BPF skeleton，因此要尽量精简

# 3. 生成BPF skeletons, 依赖2
$(OUTPUT)/%.skel.h: $(OUTPUT)/%.bpf.o | $(OUTPUT)
	$(call msg,GEN-SKEL,$@)
	$(Q)$(BPFTOOL) gen skeleton $&lt; &gt; $@

# 4. 构建用户空间程序object，依赖3
$(patsubst %,$(OUTPUT)/%.o,$(APPS)): %.o: %.skel.h

$(OUTPUT)/%.o: %.c $(wildcard %.h) | $(OUTPUT)
	$(call msg,CC,$@)
	$(Q)$(CC) $(CFLAGS) $(INCLUDES) -c $(filter %.c,$^) -o $@

# 5. 构建用户空间程序
$(APPS): %: $(OUTPUT)/%.o $(LIBBPF_OBJ) | $(OUTPUT)
	$(call msg,BINARY,$@)
	$(Q)$(CC) $(CFLAGS) $^ -lelf -lz -o $@

# delete failed targets
.DELETE_ON_ERROR:

# keep intermediate (.skel.h, .bpf.o, etc) targets
.SECONDARY:</pre>
<div class="blog_h3"><span class="graybg">bootstrap</span></div>
<p>minimal是最小化的BPF程序实例，在现代Linux环境下开发BPF程序，bootstrap可以作为一个不错的开始点。它实现的功能包括：</p>
<ol>
<li>命令行参数解析</li>
<li>信号处理</li>
<li>多个BPF程序之间的交互 —— 通过Map共享状态</li>
<li>使用BPF ring buffer来发送数据到用户空间</li>
<li>用于行为参数化的全局常量，以及如何通过修改段数据初始化常量值</li>
<li>使用BPF CO-RE和vmlinux.h来读取内核<pre class="crayon-plain-tag">struct task_struct</pre>暴露的进程的额外信息</li>
</ol>
<p>该程序依赖BPF CO-RE，需要内核配置<pre class="crayon-plain-tag">CONFIG_DEBUG_INFO_BTF=y</pre>。</p>
<p>公共头文件：</p>
<pre class="crayon-plain-tag">#ifndef __BOOTSTRAP_H
#define __BOOTSTRAP_H

#define TASK_COMM_LEN 16
#define MAX_FILENAME_LEN 127

struct event {
	int pid;
	int ppid;
	unsigned exit_code;
	unsigned long long duration_ns;
	char comm[TASK_COMM_LEN];
	char filename[MAX_FILENAME_LEN];
	bool exit_event;
};

#endif /* __BOOTSTRAP_H */</pre>
<p>内核空间代码：</p>
<pre class="crayon-plain-tag">// 该头文件包含内核的所有数据类型，通过gen_vmlinux_h.sh自动生成

// 其中的类型应用了__attribute__((preserve_access_index))注解，可以让Clang
// 生成BPF CO-RE relocations，这让libbpf能够自动的将BPF代码是配到宿主机内核的内存布局，即使
// 该布局和构建BPF程序时候的主机不一致
// BPF CO-RE relocations是创建pre-compiled、可移植的BPF程序的关键，它不需要在目标及其上部署
// Clang/LLVM工具链。一个被选的技术是BCC的运行时编译，这种技术有多个缺点

// 需要注意，vmlinux.h不能和其它系统级的头文件联合适用，会导致类型重定义和冲突
// 因此开发BPF程序时，仅可使用vmlinux.h、libbpf提供的头、你自己定义的头
#include "vmlinux.h"
#include &lt;bpf/bpf_helpers.h&gt;
// 下面两个头文件，用于编写基于BPF CO-RE的追踪应用程序
#include &lt;bpf/bpf_tracing.h&gt;
#include &lt;bpf/bpf_core_read.h&gt;
// 用于/内核空间共享头
#include "bootstrap.h"

char LICENSE[] SEC("license") = "Dual BSD/GPL";

// 定义一个哈希表类型的Map
struct {
	// 这几个__开头的是宏bpf_helpers.h中定义的宏，用来定义结构体字段
	// 定义一个BPF_MAP_TYPE_HASH类型的，最大条目8192的，key类型为pid_t，值类型为u64的Map
	__uint(type, BPF_MAP_TYPE_HASH);
	__uint(max_entries, 8192);
	__type(key, pid_t);
	__type(value, u64);
} exec_start SEC(".maps");
//           为了让libbpf知道它需要创建BPF Map，必须添加此注解

// 定义一个BPF ring buffer类型的Map
// 用于向用户空间发送数据，本样例使用bpf_ringbuf_reserve()/bpf_ringbuf_submit()来实现最高
// 的易用性和性能
struct {
	__uint(type, BPF_MAP_TYPE_RINGBUF);
	__uint(max_entries, 256 * 1024);
} rb SEC(".maps");

// 全局常量，对于用户/内核空间均不可变。在BPF程序校验期间，此常量值已知，BPF Verifier可能依据值来裁减
// 某些dead code
// volatile是必须的，可以防止Clang将变量优化掉（直接没了）
const volatile unsigned long long min_duration_ns = 0;

// 本样例由两个BPF程序组成，这个监控exec系统调用
SEC("tp/sched/sched_process_exec")
int handle_exec(struct trace_event_raw_sched_process_exec *ctx)
{
	struct task_struct *task;
	unsigned fname_off;
	struct event *e;
	pid_t pid;
	u64 ts;

	// 得到当前PID
	pid = bpf_get_current_pid_tgid() &gt;&gt; 32;
	// 获取当前内核时间
	ts = bpf_ktime_get_ns();
	// 记录当前进程的创建时间
	bpf_map_update_elem(&amp;exec_start, &amp;pid, &amp;ts, BPF_ANY);

	/* don't emit exec events when minimum duration is specified */
	if (min_duration_ns)
		return 0;

	// 在ring buffer中保留event长度的空间，返回得到的空间指针
	e = bpf_ringbuf_reserve(&amp;rb, sizeof(*e), 0);
	if (!e)
		return 0;

	// 得到当前Task对象
	task = (struct task_struct *)bpf_get_current_task();
	// 并填充event
	e-&gt;exit_event = false;
	e-&gt;pid = pid;
	// BPF CO-RE读取
	// 相当于e-&gt;ppid = task-&gt;real_parent-&gt;tgid;
	// 但是对于BPF程序来说，BPF verifier需要额外的检查，因为存在读取任意内核内存的可能
	// BPF_CORE_READ()通过简洁的方式完成此过程，并且记录必要的BPF CO-RE relocations，从而
	// 允许libbpf根据宿主机的内存布局，来调整字段的偏移量
	e-&gt;ppid = BPF_CORE_READ(task, real_parent, tgid);
	// 读取当前任务的可执行文件名（不包含路径）
	bpf_get_current_comm(&amp;e-&gt;comm, sizeof(e-&gt;comm));

	// 获取新进程文件名在ctx的偏移量            移除u32高16位
	fname_off = ctx-&gt;__data_loc_filename &amp; 0xFFFF;
	// 从ctx读取文件名，存入e
	bpf_probe_read_str(&amp;e-&gt;filename, sizeof(e-&gt;filename), (void *)ctx + fname_off);

	// 提交给用户空间供处理
	bpf_ringbuf_submit(e, 0);
	return 0;
}

// 本样例由两个BPF程序组成，这个监控进程退出，即exit系统调用
SEC("tp/sched/sched_process_exit")
int handle_exit(struct trace_event_raw_sched_process_template* ctx)
{
	struct task_struct *task;
	struct event *e;
	pid_t pid, tid;
	u64 id, ts, *start_ts, duration_ns = 0;
	
	// 获取正在退出的进程的PID
	id = bpf_get_current_pid_tgid();
	// 高32位是pid，低32位是线程ID
	pid = id &gt;&gt; 32;
	tid = (u32)id;

	// 如果pid和tid不同，则意味着是非主线程退出，忽略
	if (pid != tid)
		return 0;

	// 查找进程开始时间，并计算进程持续时间
	start_ts = bpf_map_lookup_elem(&amp;exec_start, &amp;pid);
	if (start_ts)
		duration_ns = bpf_ktime_get_ns() - *start_ts;
	else if (min_duration_ns)
		return 0;
	// 从Map删除条目
	bpf_map_delete_elem(&amp;exec_start, &amp;pid);

	// 忽略持续时间过短的进程
	if (min_duration_ns &amp;&amp; duration_ns &lt; min_duration_ns)
		return 0;

	// 从ring buffer分配空间
	e = bpf_ringbuf_reserve(&amp;rb, sizeof(*e), 0);
	if (!e)
		return 0;

	// 填充并提交给用户空间处理
	task = (struct task_struct *)bpf_get_current_task();

	e-&gt;exit_event = true;
	e-&gt;duration_ns = duration_ns;
	e-&gt;pid = pid;
	e-&gt;ppid = BPF_CORE_READ(task, real_parent, tgid);
	e-&gt;exit_code = (BPF_CORE_READ(task, exit_code) &gt;&gt; 8) &amp; 0xff;
	bpf_get_current_comm(&amp;e-&gt;comm, sizeof(e-&gt;comm));

	/* send data to user-space for post-processing */
	bpf_ringbuf_submit(e, 0);
	return 0;
}</pre>
<p>用户空间代码：</p>
<pre class="crayon-plain-tag">#include &lt;argp.h&gt;
#include &lt;signal.h&gt;
#include &lt;stdio.h&gt;
#include &lt;time.h&gt;
#include &lt;sys/resource.h&gt;
#include &lt;bpf/libbpf.h&gt;
#include "bootstrap.h"
#include "bootstrap.skel.h"

// 使用libc的argp做命令行参数解析
static struct env {
	bool verbose;
	long min_duration_ms;
} env;

const char *argp_program_version = "bootstrap 0.0";
const char *argp_program_bug_address = "&lt;bpf@vger.kernel.org&gt;";
const char argp_program_doc[] =
"BPF bootstrap demo application.\n"
"\n"
"It traces process start and exits and shows associated \n"
"information (filename, process duration, PID and PPID, etc).\n"
"\n"
"USAGE: ./bootstrap [-d &lt;min-duration-ms&gt;] [-v]\n";

static const struct argp_option opts[] = {
	{ "verbose", 'v', NULL, 0, "Verbose debug output" },
	{ "duration", 'd', "DURATION-MS", 0, "Minimum process duration (ms) to report" },
	{},
};

static error_t parse_arg(int key, char *arg, struct argp_state *state)
{
	switch (key) {
	case 'v':
		env.verbose = true;
		break;
	case 'd':
		errno = 0;
		env.min_duration_ms = strtol(arg, NULL, 10);
		if (errno || env.min_duration_ms &lt;= 0) {
			fprintf(stderr, "Invalid duration: %s\n", arg);
			argp_usage(state);
		}
		break;
	case ARGP_KEY_ARG:
		argp_usage(state);
		break;
	default:
		return ARGP_ERR_UNKNOWN;
	}
	return 0;
}

static const struct argp argp = {
	.options = opts,
	.parser = parse_arg,
	.doc = argp_program_doc,
};

static int libbpf_print_fn(enum libbpf_print_level level, const char *format, va_list args)
{
	if (level == LIBBPF_DEBUG &amp;&amp; !env.verbose)
		return 0;
	return vfprintf(stderr, format, args);
}

static void bump_memlock_rlimit(void)
{
	struct rlimit rlim_new = {
		.rlim_cur	= RLIM_INFINITY,
		.rlim_max	= RLIM_INFINITY,
	};

	if (setrlimit(RLIMIT_MEMLOCK, &amp;rlim_new)) {
		fprintf(stderr, "Failed to increase RLIMIT_MEMLOCK limit!\n");
		exit(1);
	}
}

static volatile bool exiting = false;

static void sig_handler(int sig)
{
	exiting = true;
}

static int handle_event(void *ctx, void *data, size_t data_sz)
{
	// 直接转换为event
	const struct event *e = data;
	struct tm *tm;
	char ts[32];
	time_t t;

	time(&amp;t);
	tm = localtime(&amp;t);
	strftime(ts, sizeof(ts), "%H:%M:%S", tm);

	if (e-&gt;exit_event) {
		printf("%-8s %-5s %-16s %-7d %-7d [%u]", ts, "EXIT", e-&gt;comm, e-&gt;pid, e-&gt;ppid, e-&gt;exit_code);
		if (e-&gt;duration_ns) printf(" (%llums)", e-&gt;duration_ns / 1000000);
		printf("\n");
	} else {
		printf("%-8s %-5s %-16s %-7d %-7d %s\n",  ts, "EXEC", e-&gt;comm, e-&gt;pid, e-&gt;ppid, e-&gt;filename);
	}
	return 0;
}

int main(int argc, char **argv)
{
	struct ring_buffer *rb = NULL;
	struct bootstrap_bpf *skel;
	int err;

	// 解析命令行参数
	err = argp_parse(&amp;argp, argc, argv, 0, NULL, NULL);
	if (err)
		return err;

	// 日追回调函数
	libbpf_set_print(libbpf_print_fn);

	// 设置RLIMIT_MEMLOCK
	bump_memlock_rlimit();

	// 去除默认Ctrl-C处理逻辑
	signal(SIGINT, sig_handler);
	signal(SIGTERM, sig_handler);

	// 由于有一个常量需要设置值，因此这里没有使用bootstrap_bpf__open_and_load()
	// 而是先open
	skel = bootstrap_bpf__open();
	if (!skel) {
		fprintf(stderr, "Failed to open and load BPF skeleton\n");
		return 1;
	}

	// 然后设置常量值
	skel-&gt;rodata-&gt;min_duration_ns = env.min_duration_ms * 1000000ULL;

	// 最后load，加载之后，用户空间只能读取，不能修改 skel-&gt;rodata-&gt;min_duration_ns
	err = bootstrap_bpf__load(skel);
	if (err) {
		fprintf(stderr, "Failed to load and verify BPF skeleton\n");
		goto cleanup;
	}

	// 挂钩BPF程序到内核
	err = bootstrap_bpf__attach(skel);
	if (err) {
		fprintf(stderr, "Failed to attach BPF skeleton\n");
		goto cleanup;
	}

	// 创建ring buffer轮询
	// 映射位ring_buffer类型   内核空间对应物              轮询到数据后的回调
	rb = ring_buffer__new(bpf_map__fd(skel-&gt;maps.rb), handle_event, NULL, NULL);
	if (!rb) {
		err = -1;
		fprintf(stderr, "Failed to create ring buffer\n");
		goto cleanup;
	}

	// 打印表头
	printf("%-8s %-5s %-16s %-7s %-7s %s\n",
	       "TIME", "EVENT", "COMM", "PID", "PPID", "FILENAME/EXIT CODE");
	// 接收到信号前，反复轮询
	while (!exiting) {
		err = ring_buffer__poll(rb, 100 /* timeout, ms */);
		/* Ctrl-C will cause -EINTR */
		if (err == -EINTR) {
			err = 0;
			break;
		}
		if (err &lt; 0) {
			printf("Error polling perf buffer: %d\n", err);
			break;
		}
	}

cleanup:
	// 需要清理ring buffer和BPF
	ring_buffer__free(rb);
	bootstrap_bpf__destroy(skel);

	return err &lt; 0 ? -err : 0;
}</pre>
<div class="blog_h3"><span class="graybg">uprobe</span></div>
<p>上面两个例子都是tracepoint BPF程序。tracepoint是内核中比较稳定的追踪机制，属于静态instrument，它由一系列预定义在内核源码中的挂钩点组成。</p>
<p>kprobe/uprobe则属于动态instrument，在运行时动态的进行instrument，可以对任何函数进行调试追踪。例如在函数的入口、出口地址、或者某一行值入代码，执行到这些代码的时候，你就可以获得上下文信息，例如当前函数的名字、参数、返回值、寄存器甚至全局数据结构信息。</p>
<p>kprobe/uprobe很强大，但是依赖于程序/内核的特定版本，因为函数的签名可能改变，函数也可能被删除。</p>
<p>libbpf-bootstrap提供了一个uprobe样例，此样例能够处理用户空间的entry/exit(return)探针。</p>
<p>内核空间代码：</p>
<pre class="crayon-plain-tag">#include &lt;linux/bpf.h&gt;
#include &lt;linux/ptrace.h&gt;
#include &lt;bpf/bpf_helpers.h&gt;
#include &lt;bpf/bpf_tracing.h&gt;

char LICENSE[] SEC("license") = "Dual BSD/GPL";

// 函数entry探针
SEC("uprobe/func")
// 这里是调用一个宏，在函数体中可以使用参数 struct pt_regs *ctx
//                     逐个列出函数的参数
int BPF_KPROBE(uprobe, int a, int b)
{
	bpf_printk("UPROBE ENTRY: a = %d, b = %d\n", a, b);
	return 0;
}

// 函数exit探针
SEC("uretprobe/func")
//                           列出函数的返回值
int BPF_KRETPROBE(uretprobe, int ret)
{
	bpf_printk("UPROBE EXIT: return = %d\n", ret);
	return 0;
}</pre>
<p>用户空间代码：</p>
<pre class="crayon-plain-tag">#include &lt;errno.h&gt;
#include &lt;stdio.h&gt;
#include &lt;unistd.h&gt;
#include &lt;sys/resource.h&gt;
#include &lt;bpf/libbpf.h&gt;
#include "uprobe.skel.h"

//...

/* 通过/proc/self/maps查找进程的base load address, 寻找第一个可执行的（r-xp）内存映射：
 * 绝对起始地址                    此区域的偏移量
 * 5574fd254000-5574fd258000 r-xp 00002000 fd:01 668759                     /usr/bin/cat
 * ^^^^^^^^^^^^                   ^^^^^^^^
 * 绝对起始地址 - 此区域的偏移量 即得到进程的base load address
 */
static long get_base_addr() {
	size_t start, offset;
	char buf[256];
	FILE *f;

	f = fopen("/proc/self/maps", "r");
	if (!f)
		return -errno;

	while (fscanf(f, "%zx-%*x %s %zx %*[^\n]\n", &amp;start, buf, &amp;offset) == 3) {
		if (strcmp(buf, "r-xp") == 0) {
			fclose(f);
			return start - offset;
		}
	}

	fclose(f);
	return -1;
}

// 这个是被追踪的函数
int uprobed_function(int a, int b)
{
	return a + b;
}

int main(int argc, char **argv)
{
	struct uprobe_bpf *skel;
	long base_addr, uprobe_offset;
	int err, i;

	// 注册日志回调钩子、调整rlimit、打开并加载BPF程序

	base_addr = get_base_addr();
	if (base_addr &lt; 0) {
		fprintf(stderr, "Failed to determine process's load address\n");
		err = base_addr;
		goto cleanup;
	}

	// uprobe/uretprobe期望获知被追踪函数的相对偏移量，这个偏移量是相对于process的base load address的
	// 这里直接得到目标函数的绝对地址，然后减去上面我们得到的base load address，即可得到偏移量
	//
	// 通常情况下，被追踪的函数不会在当前程序中，这可能需要解析其所在程序的ELF，
	// 函数相对base load address的偏移量 = .text段的偏移量 + 函数在.text段内的偏移量
	//
	//              得到函数的绝对地址
	uprobe_offset = (long)&amp;uprobed_function - base_addr;

	//                   挂钩
	skel-&gt;links.uprobe = bpf_program__attach_uprobe(skel-&gt;progs.uprobe,
							false /* entry钩子 */,
							0 /* 到当前进程 */,
							"/proc/self/exe", // 当前进程的二进制文件路径
							uprobe_offset); // 追踪的目标函数的偏移量
	err = libbpf_get_error(skel-&gt;links.uprobe);
	if (err) {
		fprintf(stderr, "Failed to attach uprobe: %d\n", err);
		goto cleanup;
	}

	// 这里示例了如何挂钩uprobe/uretprobe到任何现存的/未来创建的、使用同一二进制文件的进程
	skel-&gt;links.uretprobe = bpf_program__attach_uprobe(skel-&gt;progs.uretprobe,
							   true /* exit钩子 */,
							   -1 /* 任何进程 */,
							   "/proc/self/exe",
							   uprobe_offset);
	err = libbpf_get_error(skel-&gt;links.uretprobe);
	if (err) {
		fprintf(stderr, "Failed to attach uprobe: %d\n", err);
		goto cleanup;
	}

	printf("Successfully started! Please run `sudo cat /sys/kernel/debug/tracing/trace_pipe` "
	       "to see output of the BPF programs.\n");

	for (i = 0; ; i++) {
		// 调用目标函数，触发BPF程序
		fprintf(stderr, ".");
		uprobed_function(i, i + 1);
		sleep(1);
	}

cleanup:
	uprobe_bpf__destroy(skel);
	return -err;
}</pre>
<p>运行此程序，就可以看到函数uprobed_function每次调用的参数、返回值了。</p>
<div class="blog_h3"><span class="graybg">kprobe</span></div>
<p>kprobe和uprobe工作方式很类似，只是挂钩的是内核函数。</p>
<p>libbpf-bootstrap提供了一个kprobe样例，该样例挂钩到do_unlinkat()函数，并且打印PID、文件名、返回值等信息。</p>
<p>内核空间代码：</p>
<pre class="crayon-plain-tag">#include "vmlinux.h"
#include &lt;bpf/bpf_helpers.h&gt;
#include &lt;bpf/bpf_tracing.h&gt;
#include &lt;bpf/bpf_core_read.h&gt;

char LICENSE[] SEC("license") = "Dual BSD/GPL";

// 注解里必须包含目标内核函数的名字
SEC("kprobe/do_unlinkat")
int BPF_KPROBE(do_unlinkat, int dfd, struct filename *name)
{
	pid_t pid;
	const char *filename;

	pid = bpf_get_current_pid_tgid() &gt;&gt; 32;
	// 必须使用此助手函数访问指针参数，fentry则不必
	filename = BPF_CORE_READ(name, name);
	bpf_printk("KPROBE ENTRY pid = %d, filename = %s\n", pid, filename);
	return 0;
}

SEC("kretprobe/do_unlinkat")
int BPF_KRETPROBE(do_unlinkat_exit, long ret)
{
	pid_t pid;

	pid = bpf_get_current_pid_tgid() &gt;&gt; 32;
	bpf_printk("KPROBE EXIT: pid = %d, ret = %ld\n", pid, ret);
	return 0;
}</pre>
<p>用户空间代码：</p>
<pre class="crayon-plain-tag">#include &lt;stdio.h&gt;
#include &lt;unistd.h&gt;
#include &lt;signal.h&gt;
#include &lt;string.h&gt;
#include &lt;errno.h&gt;
#include &lt;sys/resource.h&gt;
#include &lt;bpf/libbpf.h&gt;
#include "kprobe.skel.h"

// ...

static volatile sig_atomic_t stop;

static void sig_int(int signo)
{
	stop = 1;
}

int main(int argc, char **argv)
{
	struct kprobe_bpf *skel;
	int err;

	// 注册日志回调钩子、调整rlimit、打开并加载BPF程序

	// 挂钩处理比起uprobe要简单的多，不需要计算目标函数的地址
	err = kprobe_bpf__attach(skel);
	if (err) {
		fprintf(stderr, "Failed to attach BPF skeleton\n");
		goto cleanup;
	}

	if (signal(SIGINT, sig_int) == SIG_ERR) {
		fprintf(stderr, "can't set signal handler: %s\n", strerror(errno));
		goto cleanup;
	}

	printf("Successfully started! Please run `sudo cat /sys/kernel/debug/tracing/trace_pipe` "
	       "to see output of the BPF programs.\n");

	while (!stop) {
		fprintf(stderr, ".");
		sleep(1);
	}

cleanup:
	kprobe_bpf__destroy(skel);
	return -err;
}</pre>
<div class="blog_h3"><span class="graybg">fentry</span></div>
<p>内核5.5引入了BPF trampoline，可以让内核和BPF程序更快速的相互调用。fentry/fexit是它的一个用例，fentry/fexit基本上等价于kprobe/kretprobe，但是调用BPF程序不会引起额外的overhead。</p>
<p>在XDP开发中BPF trampoline能很大程度上改善BPF相关的网络troubleshooting的体验。BPF trampoline支持将fentry/fexit BPF程序挂钩到任何网络BPF程序上，从而可以看到任何XDP/TC/lwt/cgroup程序的输入输出封包，同时不会产生干扰。</p>
<p>libbpf-bootstrap提供了一个fentry样例，类似于kprobe样例，它也是挂钩到do_unlinkat()函数，从而在文件被删除后触发BPF程序，记录各种信息。</p>
<p>fentry/fexit提升了性能的同时，也改善了开发体验。<span style="background-color: #c0c0c0;">访问指针参数时</span>，不再需要调用助手函数，<span style="background-color: #c0c0c0;">可以直接解引用</span>。<span style="background-color: #c0c0c0;">fexit</span>和kretprobe比起来，<span style="background-color: #c0c0c0;">可以同时返回输入参数和返回值</span>，后者只能访问返回值。</p>
<p>内核空间代码：</p>
<pre class="crayon-plain-tag">#include "vmlinux.h"
#include &lt;bpf/bpf_helpers.h&gt;
#include &lt;bpf/bpf_tracing.h&gt;

char LICENSE[] SEC("license") = "Dual BSD/GPL";

// 注解里必须包含目标内核函数的名字
SEC("fentry/do_unlinkat")
int BPF_PROG(do_unlinkat, int dfd, struct filename *name)
{
	pid_t pid;

	pid = bpf_get_current_pid_tgid() &gt;&gt; 32;
	bpf_printk("fentry: pid = %d, filename = %s\n", pid, name-&gt;name);
	return 0;
}

SEC("fexit/do_unlinkat")
//                             可以同时访问入参和返回值
int BPF_PROG(do_unlinkat_exit, int dfd, struct filename *name, long ret)
{
	pid_t pid;

	pid = bpf_get_current_pid_tgid() &gt;&gt; 32;
	bpf_printk("fexit: pid = %d, filename = %s, ret = %ld\n", pid, name-&gt;name, ret);
	return 0;
}</pre>
<p>用户空间代码：</p>
<pre class="crayon-plain-tag">#include &lt;stdio.h&gt;
#include &lt;unistd.h&gt;
#include &lt;signal.h&gt;
#include &lt;string.h&gt;
#include &lt;errno.h&gt;
#include &lt;sys/resource.h&gt;
#include &lt;bpf/libbpf.h&gt;
#include "fentry.skel.h"

static volatile sig_atomic_t stop;

int main(int argc, char **argv)
{
	struct fentry_bpf *skel;
	int err;

	// 注册日志回调钩子、调整rlimit、打开并加载BPF程序

	// 挂钩
	err = fentry_bpf__attach(skel);
	if (err) {
		fprintf(stderr, "Failed to attach BPF skeleton\n");
		goto cleanup;
	}

	while (!stop) {
		fprintf(stderr, ".");
		sleep(1);
	}

cleanup:
	fentry_bpf__destroy(skel);
	return -err;
}</pre>
<div class="blog_h2"><span class="graybg">内核样例程序</span></div>
<p>内核在samples/bpf/下包含了若干eBPF程序示例。这些程序都包含两部分：</p>
<ol>
<li><pre class="crayon-plain-tag">*_kern.c</pre>会编译为BPF内核程序</li>
<li><pre class="crayon-plain-tag">*_user.c</pre>为用户空间程序，加载上述内核程序</li>
</ol>
<p>这些样例没有封装，直接使用libbpf提供的函数完成各种功能，包括助手函数调用、ELF加载、BPF挂钩、Map创建和读写。</p>
<p>执行下面的命令构建示例：</p>
<pre class="crayon-plain-tag"># 可能需要进行必要的清理
make -C tools clean
make -C samples/bpf clean
make clean

# 当前正在构建版本的内核头文件，复制到usr/include子目录。构建samples会优先使用这些头文件
make headers_install

# 构建BPF样例
make M=samples/bpf</pre>
<div class="blog_h3"><span class="graybg">tracex4</span></div>
<p>eBPF程序代码：</p>
<pre class="crayon-plain-tag">#include &lt;linux/ptrace.h&gt;
#include &lt;linux/version.h&gt;
#include &lt;uapi/linux/bpf.h&gt;
#include &lt;bpf/bpf_helpers.h&gt;
#include &lt;bpf/bpf_tracing.h&gt;

struct pair {
	u64 val;
	u64 ip;
};

// 定义一个Map
struct {
    // eBPF提供多种Map，BPF_MAP_TYPE_HASH是其中之一
	__uint(type, BPF_MAP_TYPE_HASH);
	// 键值的类型
	__type(key, long);
	__type(value, struct pair);
	// 容量
	__uint(max_entries, 1000000);
} my_map SEC(".maps");
        // SEC宏用于在二进制文件中创建一个新的段

// 这个段实际上声明了挂钩点
SEC("kprobe/kmem_cache_free")
// 从Map中删除一个键值
int bpf_prog1(struct pt_regs *ctx)
{
	long ptr = PT_REGS_PARM2(ctx);

	bpf_map_delete_elem(&amp;my_map, &amp;ptr);
	return 0;
}

// 这个段实际上声明了挂钩点
SEC("kretprobe/kmem_cache_alloc_node")
// 添加一个键值到Map
int bpf_prog2(struct pt_regs *ctx)
{
	long ptr = PT_REGS_RC(ctx);
	long ip = 0;

	/* get ip address of kmem_cache_alloc_node() caller */
	BPF_KRETPROBE_READ_RET_IP(ip, ctx);

	struct pair v = {
		.val = bpf_ktime_get_ns(),
		.ip = ip,
	};

	bpf_map_update_elem(&amp;my_map, &amp;ptr, &amp;v, BPF_ANY);
	return 0;
}
char _license[] SEC("license") = "GPL";
u32 _version SEC("version") = LINUX_VERSION_CODE;</pre>
<p>用于空间程序源码：</p>
<pre class="crayon-plain-tag">#include &lt;stdio.h&gt;
#include &lt;stdlib.h&gt;
#include &lt;signal.h&gt;
#include &lt;unistd.h&gt;
#include &lt;stdbool.h&gt;
#include &lt;string.h&gt;
#include &lt;time.h&gt;
#include &lt;sys/resource.h&gt;

#include &lt;bpf/bpf.h&gt;
#include &lt;bpf/libbpf.h&gt;

struct pair {
	long long val;
	__u64 ip;
};

static __u64 time_get_ns(void)
{
	struct timespec ts;

	clock_gettime(CLOCK_MONOTONIC, &amp;ts);
	return ts.tv_sec * 1000000000ull + ts.tv_nsec;
}

static void print_old_objects(int fd)
{
	long long val = time_get_ns();
	__u64 key, next_key;
	struct pair v;

	key = write(1, "\e[1;1H\e[2J", 12); /* clear screen */

	key = -1;
	while (bpf_map_get_next_key(fd, &amp;key, &amp;next_key) == 0) {
		bpf_map_lookup_elem(fd, &amp;next_key, &amp;v);
		key = next_key;
		if (val - v.val &lt; 1000000000ll)
			/* object was allocated more then 1 sec ago */
			continue;
		printf("obj 0x%llx is %2lldsec old was allocated at ip %llx\n",
		       next_key, (val - v.val) / 1000000000ll, v.ip);
	}
}

int main(int ac, char **argv)
{
	struct rlimit r = {RLIM_INFINITY, RLIM_INFINITY};
	struct bpf_link *links[2];
	struct bpf_program *prog;
	struct bpf_object *obj;
	char filename[256];
	int map_fd, i, j = 0;

	if (setrlimit(RLIMIT_MEMLOCK, &amp;r)) {
		perror("setrlimit(RLIMIT_MEMLOCK, RLIM_INFINITY)");
		return 1;
	}

	snprintf(filename, sizeof(filename), "%s_kern.o", argv[0]);
	// 打开编译好的eBPF程序，tracex4_kern.o
	obj = bpf_object__open_file(filename, NULL);
	if (libbpf_get_error(obj)) {
		fprintf(stderr, "ERROR: opening BPF object file failed\n");
		return 0;
	}

	// 载入eBPF程序
	// 调用之后，定义在eBPF中的探针被添加到/sys/kernel/debug/tracing/kprobe_events
	//   cat /sys/kernel/debug/tracing/kprobe_events
	//   p:kprobes/kmem_cache_free kmem_cache_free
	//   r:kprobes/kmem_cache_alloc_node kmem_cache_alloc_node
	if (bpf_object__load(obj)) {
		fprintf(stderr, "ERROR: loading BPF object file failed\n");
		goto cleanup;
	}

	// 找到共享的Map
	map_fd = bpf_object__find_map_fd_by_name(obj, "my_map");
	if (map_fd &lt; 0) {
		fprintf(stderr, "ERROR: finding a map in obj file failed\n");
		goto cleanup;
	}

	// 执行挂钩
	bpf_object__for_each_program(prog, obj) {
		links[j] = bpf_program__attach(prog);
		if (libbpf_get_error(links[j])) {
			fprintf(stderr, "ERROR: bpf_program__attach failed\n");
			links[j] = NULL;
			// 如果出错，解除所有已经注册的钩子
			goto cleanup;
		}
		j++;
	}

	// 读取Map并打印
	for (i = 0; ; i++) {
		print_old_objects(map_fd);
		sleep(1);
	}

cleanup:
	for (j--; j &gt;= 0; j--)
		bpf_link__destroy(links[j]);

	bpf_object__close(obj);
	return 0;
}</pre>
<div class="blog_h2"><span class="graybg">XDP样例程序</span></div>
<div class="blog_h3"><span class="graybg">过滤IPv6</span></div>
<p>这个简单的、可以和iproute2一起工作的XDP样例，判断封包是否是IPv6的，如果是，则丢弃：</p>
<pre class="crayon-plain-tag">#define KBUILD_MODNAME "xdp_ipv6_filter"
#include &lt;uapi/linux/bpf.h&gt;
#include &lt;uapi/linux/if_ether.h&gt;
#include &lt;uapi/linux/if_packet.h&gt;
#include &lt;uapi/linux/if_vlan.h&gt;
#include &lt;uapi/linux/ip.h&gt;
#include &lt;uapi/linux/in.h&gt;
#include &lt;uapi/linux/tcp.h&gt;
#include "bpf_helpers.h"

#define DEBUG 1

#ifdef  DEBUG
/* Only use this for debug output. Notice output from  bpf_trace_printk()
 * end-up in /sys/kernel/debug/tracing/trace_pipe
 */
#define bpf_debug(fmt, ...)                     \
        ({                          \
            char ____fmt[] = fmt;               \
            // 这个函数打印消息到 /sys/kernel/debug/tracing/trace_pipe
            bpf_trace_printk(____fmt, sizeof(____fmt),  \
                     ##__VA_ARGS__);            \
        })
#else
#define bpf_debug(fmt, ...) { } while (0)
#endif

// 解析包头得到ethertype
static __always_inline bool parse_eth(struct ethhdr *eth, void *data_end, u16 *eth_type)
{
    u64 offset;

    offset = sizeof(*eth);
    if ((void *)eth + offset &gt; data_end)
        return false;
    *eth_type = eth-&gt;h_proto;
    return true;
}

// 定义一个名为prog的段
// 由于我们使用iproute2来挂钩，因此必须用prog这个段名。否则iproute2无法识别
SEC("prog")
// xdp_md结构包含访问封包所需的所有数据
int xdp_ipv6_filter_program(struct xdp_md *ctx)
{
    // 得到封包头尾指针
    void *data_end = (void *)(long)ctx-&gt;data_end;
    void *data     = (void *)(long)ctx-&gt;data;
    struct ethhdr *eth = data;
    u16 eth_type = 0;
    // 调用parse_eth函数，可以获取封包的ethertype
    if (!(parse_eth(eth, data_end, eth_type))) {
        bpf_debug("Debug: Cannot parse L2\n");
        return XDP_PASS;
    }

    bpf_debug("Debug: eth_type:0x%x\n", ntohs(eth_type));
    // 判断ethertype是否IPv6
    if (eth_type == ntohs(0x86dd)) {
        return XDP_PASS;
    } else {
        return XDP_DROP;
    }
}

char _license[] SEC("license") = "GPL";</pre>
<p>编译此程序后得到xdp_ipv6_filter.o文件。要将此文件加载到网络接口，可以使用两种方法：</p>
<ol>
<li>编写一个用户空间程序，加载对象文件，挂钩到一个网络接口</li>
<li>调用iproute2进行等价操作</li>
</ol>
<p>某些网络接口在驱动层支持XDP，包括ixgbe, i40e, mlx5, veth, tap, tun, virtio_net等。这种情况下，XDP钩子实现在网络栈的最底部，即NIC从Rx Ring中接收到封包之后，具有最好的性能。对于其它类型的网络接口，XDP钩子则在更高的层次实现，性能相对较差。</p>
<p>我们这里用一对veth来测试：</p>
<pre class="crayon-plain-tag">sudo ip link add dev veth0 type veth peer name veth1
sudo ip link set up dev veth0
sudo ip link set up dev veth1

# 挂钩eBPF程序到接口上，程序必须写在prog段中
sudo ip link set dev veth1 xdp object xdp_ipv6_filter.o

# 查看接口信息
sudo ip link show veth1
8: veth1@veth0: &lt;BROADCAST,MULTICAST,UP,LOWER_UP&gt; mtu 1500 xdp qdisc noqueue state UP mode DEFAULT group default qlen 1000
    link/ether 32:05:fc:9a:d8:75 brd ff:ff:ff:ff:ff:ff
    # 可以看到xdp标记
    prog/xdp id 32 tag bdb81fb6a5cf3154 jited</pre>
<p>为了验证eBPF程序能正常工作，我们通过tcpdump回放<a href="https://github.com/dpino/xdp_ipv6_filter/blob/master/ipv4-and-ipv6-data.pcap">一段录制的封包</a>：</p>
<pre class="crayon-plain-tag"># 在veth1上录制IPv6封包，如果程序正常工作，应该接收不到
sudo tcpdump "ip6" -i veth1 -w captured.pcap -c 10
tcpdump: listening on veth1, link-type EN10MB (Ethernet), capture size 262144 bytes
# 预期结果：
# 10 packets captured
# 10 packets received by filter
# 0 packets dropped by kernel


# 将IPv4/IPv6混合流量（此pcap中包含10个IPv4，10个IPv6）回放到veth0
# q由于eBPF程序的存在IPv6的封包应该不会发到veth1
sudo tcpreplay -i veth0 ipv4-and-ipv6-data.pcap</pre>
<p>在/sys/kernel/debug/tracing/trace_pipe 中可以看到日志：</p>
<pre class="crayon-plain-tag">sudo cat /sys/kernel/debug/tracing/trace_pipe
tcpreplay-4496  [003] ..s1 15472.046835: 0: Debug: eth_type:0x86dd
tcpreplay-4496  [003] ..s1 15472.046847: 0: Debug: eth_type:0x86dd
tcpreplay-4496  [003] ..s1 15472.046855: 0: Debug: eth_type:0x86dd
tcpreplay-4496  [003] ..s1 15472.046862: 0: Debug: eth_type:0x86dd
tcpreplay-4496  [003] ..s1 15472.046869: 0: Debug: eth_type:0x86dd
tcpreplay-4496  [003] ..s1 15472.046878: 0: Debug: eth_type:0x800
tcpreplay-4496  [003] ..s1 15472.046885: 0: Debug: eth_type:0x800
tcpreplay-4496  [003] ..s1 15472.046892: 0: Debug: eth_type:0x800
tcpreplay-4496  [003] ..s1 15472.046903: 0: Debug: eth_type:0x800
tcpreplay-4496  [003] ..s1 15472.046911: 0: Debug: eth_type:0x800
...</pre>
<div class="blog_h1"><span class="graybg">BCC</span></div>
<div class="blog_h2"><span class="graybg">转换为CO-RE</span></div>
<p>BCC提供了开发可移植的BPF程序的一套方法，但是比起新出现的BPF CO-RE有不少缺点，建议切换为BPF CO-RE。</p>
<div class="blog_h3"><span class="graybg">过渡</span></div>
<p>如果你需要同时支持BCC和CO-RE，可以利用<pre class="crayon-plain-tag">BCC_SEC</pre>宏，对于BCC来说，它会自动定义：</p>
<pre class="crayon-plain-tag">#ifdef BCC_SEC
#define __BCC__
#endif

#ifdef __BCC__
/* BCC-specific code */
#else
/* libbpf-specific code */
#endif</pre>
<div class="blog_h3"><span class="graybg">头文件</span></div>
<p>使用BPF CO-RE时，你不需要包含任何内核头文件（<pre class="crayon-plain-tag">#include &lt;linux/whatever.h&gt;</pre>），而仅仅需要包含一个<pre class="crayon-plain-tag">vmlinux.h</pre>即可。</p>
<pre class="crayon-plain-tag">#ifdef __BCC__
/* linux headers needed for BCC only */
#else /* __BCC__ */
#include "vmlinux.h"               /* all kernel types */
#include &lt;bpf/bpf_helpers.h&gt;       /* most used helpers: SEC, __always_inline, etc */
#include &lt;bpf/bpf_core_read.h&gt;     /* for BPF CO-RE helpers */
#include &lt;bpf/bpf_tracing.h&gt;       /* for getting kprobe arguments */
#endif /* __BCC__ */</pre>
<p>由于vmlinux.h中可能缺少一部分内核通过#define定义的常量，因此你可能需要重新声明他们，这些变量中最常用的，已经声明在<pre class="crayon-plain-tag">bpf_helpers.h</pre>中了。</p>
<div class="blog_h3"><span class="graybg"> 字段访问</span></div>
<p>BCC会自动将<pre class="crayon-plain-tag">tsk-&gt;parent-&gt;pid</pre>这样的point chasing转换为bpf_probe_read()调用，CO-RE则没有这么好用功能，你需要使用<pre class="crayon-plain-tag">bpf_core_read.h</pre>中定义的助手函数/宏，并将上述表达式改写为<pre class="crayon-plain-tag">BPF_CORE_READ(tsk, parent, pid)</pre>形式。</p>
<p>从5.5+开始，<pre class="crayon-plain-tag">tp_btf</pre>和<pre class="crayon-plain-tag">fentry</pre>/<pre class="crayon-plain-tag">fext</pre>类型的BPF程序类型，可以使用原生的C指针访问语法。考虑你的目标环境。</p>
<p><pre class="crayon-plain-tag">BPF_CORE_READ</pre>和BCC兼容，也就是说，你可以统一使用该宏，避免重复代码。</p>
<div class="blog_h3"><span class="graybg">声明BPF Maps</span></div>
<p>下面给出BCC和libbpf声明Map语法的对比：</p>
<pre class="crayon-plain-tag">/* Array */
#ifdef __BCC__
BPF_ARRAY(my_array_map, struct my_value, 128);
#else
struct {
    __uint(type, BPF_MAP_TYPE_ARRAY);
    __uint(max_entries, 128);
    __type(key, u32);
    __type(value, struct my_value);
} my_array_map SEC(".maps");
#endif

/* Hashmap */
#ifdef __BCC__
BPF_HASH(my_hash_map, u32, struct my_value);
#else
struct {
    __uint(type, BPF_MAP_TYPE_HASH);
    __uint(max_entries, 10240);
    __type(key, u32);
    __type(value, struct my_value);
} my_hash_map SEC(".maps")
#endif

/* per-CPU array */
#ifdef __BCC__
BPF_PERCPU_ARRAY(heap, struct my_value, 1);
#else
struct {
    __uint(type, BPF_MAP_TYPE_PERCPU_ARRAY);
    __uint(max_entries, 1);
    __type(key, u32);
    __type(value, struct my_value);
} heap SEC(".maps");
#endif</pre>
<p>注意：BCC中的Map默认大小一般是10240，使用libbpf时则需要精确的设置。</p>
<p>PERF_EVENT_ARRAY、STACK_TRACE以及一些其它的特殊Map（DEVMAP、CPUMAP）尚不支持BTF类型的键值，需要直接指定key_size/value_siz：</p>
<pre class="crayon-plain-tag">/* Perf event array (for use with perf_buffer API) */
#ifdef __BCC__
BPF_PERF_OUTPUT(events);
#else
struct {
    __uint(type, BPF_MAP_TYPE_PERF_EVENT_ARRAY);
    __uint(key_size, sizeof(u32));
    __uint(value_size, sizeof(u32));
} events SEC(".maps");
#endif</pre>
<div class="blog_h3"><span class="graybg">访问BPF Maps</span></div>
<p>BCC使用伪C++风格的语法，这些语法会自动被改写为BPF助手函数调用。使用CO-RE时候，你需要将：</p>
<pre class="crayon-plain-tag">some_map.operation(some, args)</pre>
<p>改写为：</p>
<pre class="crayon-plain-tag">bpf_map_operation_elem(&amp;some_map, some, args);</pre>
<p>下面是一些例子：</p>
<pre class="crayon-plain-tag">#ifdef __BCC__
    struct event *data = heap.lookup(&amp;zero);
#else
    struct event *data = bpf_map_lookup_elem(&amp;heap, &amp;zero);
#endif

#ifdef __BCC__
    my_hash_map.update(&amp;id, my_val);
#else
    bpf_map_update_elem(&amp;my_hash_map, &amp;id, &amp;my_val, 0 /* flags */);
#endif

#ifdef __BCC__
    events.perf_submit(args, data, data_len);
#else
    bpf_perf_event_output(args, &amp;events, BPF_F_CURRENT_CPU, data, data_len);
#endif</pre>
<div class="blog_h3"><span class="graybg">注册BPF Program</span></div>
<p>使用CO-RE时必须用<pre class="crayon-plain-tag">SEC()</pre>宏标记满足<a href="https://github.com/libbpf/libbpf/blob/787abf721ec8fac1a4a0a7b075acc79a927afed9/src/libbpf.c#L7935-L8075">约定</a>的段名：</p>
<pre class="crayon-plain-tag">#if !defined(__BCC__)
SEC("tracepoint/sched/sched_process_exec")
#endif
int tracepoint__sched__sched_process_exec(
#ifdef __BCC__
    struct tracepoint__sched__sched_process_exec *args
#else
    struct trace_event_raw_sched_process_exec *args
#endif
) {
/* ... */
}</pre>
<p>常用的约定例如：</p>
<p style="padding-left: 30px;"><pre class="crayon-plain-tag">tp/&lt;category&gt;/&lt;name&gt;</pre> 用于tracepoint</p>
<p style="padding-left: 30px;"><pre class="crayon-plain-tag">kprobe/&lt;func_name&gt;</pre> 用于kprobe；<pre class="crayon-plain-tag">kretprobe/&lt;func_name&gt;</pre> 用于kretprobe</p>
<p style="padding-left: 30px;"><pre class="crayon-plain-tag">raw_tp/&lt;name&gt;</pre> 用于raw tracepoint</p>
<p style="padding-left: 30px;"><pre class="crayon-plain-tag">cgroup_skb/ingress</pre>, <pre class="crayon-plain-tag">cgroup_skb/egress</pre>, <pre class="crayon-plain-tag">cgroup/&lt;subtype&gt;</pre></p>
<div class="blog_h3"><span class="graybg">关于tracepoint</span></div>
<p>BCC和libbpf关于tracepoint上下文类型的命名有所不同。BCC的格式是<pre class="crayon-plain-tag">tracepoint__&lt;category&gt;__&lt;name&gt;</pre>，BCC会在运行时编译的时候，自动生成对应的C类型。</p>
<p>libbpf则没有这种自动生成类型的能力，不过内核已经提供了类似的，包含了所有tracepoint数据的类型，其命名为<pre class="crayon-plain-tag">trace_event_raw_&lt;name&gt;</pre>。</p>
<p>某些内核中的tracepoint会复用公共类型，因此上述类型对应关系不一定可用。例如没有<pre class="crayon-plain-tag">trace_event_raw_sched_process_exit</pre>，你需要使用<pre class="crayon-plain-tag">trace_event_raw_sched_process_template</pre>，具体需要关注内核源码或者vmlinux.h。</p>
<p>BCC和libbpf访问tracepoint上下文大部分字段，用同样的字段名。除了一些可变长度字符串字段。BBC是<pre class="crayon-plain-tag">data_loc_&lt;some_field&gt;</pre> 而libbpf是 <pre class="crayon-plain-tag">__data_loc_&lt;some_field&gt;</pre>格式。</p>
<div class="blog_h3"><span class="graybg">关于kprobe</span></div>
<p>Kprobe BPF程序以一个<pre class="crayon-plain-tag">struct pt_regs</pre>作为上下文参数，BCC支持直接声明为函数参数，libbpf则需要借助<pre class="crayon-plain-tag">BPF_KPROBE</pre> / <pre class="crayon-plain-tag">BPF_KRETPROBE</pre>宏：</p>
<pre class="crayon-plain-tag">#ifdef __BCC__
int kprobe__acct_collect(struct pt_regs *ctx, long exit_code, int group_dead)
#else
SEC("kprobe/acct_collect")
int BPF_KPROBE(kprobe__acct_collect, long exit_code, int group_dead)
#endif
{
    // 对于libbpf，在这里也可以访问名为ctx的pt_regs*
}</pre>
<p>另外一个需要注意的点，在4.17+，系统调用函数发生了重命名，例如<pre class="crayon-plain-tag">sys_kill</pre>被重命名为<pre class="crayon-plain-tag">__x64_sys_kill</pre>（对于x86，其它平台下有不同的前缀）。使用kprobe时需要注意到这种变化，如果可能，尽量使用tracepoint。</p>
<p>从5.5+开始，可以考虑用<pre class="crayon-plain-tag">raw_tp</pre>/<pre class="crayon-plain-tag">fentry</pre>/<pre class="crayon-plain-tag">fexit</pre>代替<pre class="crayon-plain-tag">tracepoint</pre>/<pre class="crayon-plain-tag">kprobe</pre>/<pre class="crayon-plain-tag">kretprobe</pre>。这些新探针具有更好的性能、易用性。</p>
<div class="blog_h3"><span class="graybg">处理编译时#if</span></div>
<p>在BCC代码中依赖预处理器（#ifdef 或者 #if）是流行的做法，原因通常是为了适配不同版本的内核、根据程序配置启用/禁用代码片段。CO-RE提供类似的能力：Kconfig externs和struct flavors。</p>
<div class="blog_h1"><span class="graybg">附录</span></div>
<div class="blog_h2"><span class="graybg">相关项目</span></div>
<div class="blog_h3"><span class="graybg">BCC</span></div>
<p>参考：<a href="/bcc">基于BCC进行性能追踪和网络监控</a></p>
<p>BCC（BPF Compiler Collection，BPF编译器集合）是一个工具箱，它提供了很多追踪用途的BPF程序，加载这些程序时使用BCC提供的Python接口。</p>
<div class="blog_h3"><span class="graybg">bpftrace</span></div>
<p>一个DTrace风格的动态追踪工具，使用LLVM作为后端，将脚本编译为BPF字节码，并使用BCC和内核的BPF tracing基础设施进行交互。和原生BCC相比，它提供了更加高级的、用于实现追踪脚本的语言。</p>
<div class="blog_h3"><span class="graybg">perf</span></div>
<p>参考：<a href="/perf">利用perf剖析Linux应用程序</a></p>
<p>perf_events是内核2.6+的一部分，用户空间工具perf在包linux-tools-common中。它有三种工作模式，其中最新的是4.4引入的BPF模式，支持载入tracing BPF程序。</p>
<div class="blog_h2"><span class="graybg">参考</span></div>
<ol>
<li><a href="https://qmonnet.github.io/whirl-offload/2016/09/01/dive-into-bpf/">Dive into BPF: a list of reading material</a></li>
<li><a href="https://docs.cilium.io/en/v1.9/bpf">https://docs.cilium.io/en/v1.9/bpf</a></li>
<li><a href="https://github.com/iovisor/bpf-docs/blob/master/bpf_helpers.rst">list of eBPF helper functions</a></li>
</ol>
<p>&nbsp;</p>
</div><p>The post <a rel="nofollow" href="https://blog.gmem.cc/ebpf">eBPF学习笔记</a> appeared first on <a rel="nofollow" href="https://blog.gmem.cc">绿色记忆</a>.</p>
]]></content:encoded>
			<wfw:commentRss>https://blog.gmem.cc/ebpf/feed</wfw:commentRss>
		<slash:comments>4</slash:comments>
		</item>
		<item>
		<title>Cilium学习笔记</title>
		<link>https://blog.gmem.cc/cilium</link>
		<comments>https://blog.gmem.cc/cilium#comments</comments>
		<pubDate>Mon, 22 Jun 2020 10:56:30 +0000</pubDate>
		<dc:creator><![CDATA[Alex]]></dc:creator>
				<category><![CDATA[PaaS]]></category>
		<category><![CDATA[CNI]]></category>
		<category><![CDATA[eBPF]]></category>
		<category><![CDATA[K8S]]></category>

		<guid isPermaLink="false">https://blog.gmem.cc/?p=37551</guid>
		<description><![CDATA[<p>简介 Cilium Cilium是在Docker/K8S之类的容器管理平台下，透明的为应用程序服务提供安全网络连接的开源软件。Cilium的底层技术是eBPF，eBPF完全在内核中运行，因此改变Cilium的安全策略时不需要程序代码、容器配置的任何变更。 Hubble Hubble是一个完全分布式的网络和安全可观察性平台。它构建在Cilium + eBPF之上，它以完全透明的方式，实现了服务、网络基础设施的通信/行为的深度可观察性。 由于可观察性依赖于eBPF，因此是可动态编程的、成本最小化的、可深度定制的。 Hubble可以回答以下问题： 服务依赖和通信关系图： 两个服务是否通信，通信频度如何，服务之间的依赖关系是怎样的？ 进行了哪些HTTP调用？ 服务消费了哪些Kafka主题，发布了哪些Kafka主题 网络监控和报警： 网络通信是否失败，为何失败？是DNS导致的失败？还是L4/L7的原因 最近5分钟有哪些服务存在DNS解析问题 哪些服务出现连接超时、中断的问题 无应答SYN请求的频率是多高 应用程序监控： 特定服务/或者整个集群的5xx/4xx HTTP响应的频率是多高 HTTP请求延迟的95th/99th位数是多少 <a class="read-more" href="https://blog.gmem.cc/cilium">[...]</a></p>
<p>The post <a rel="nofollow" href="https://blog.gmem.cc/cilium">Cilium学习笔记</a> appeared first on <a rel="nofollow" href="https://blog.gmem.cc">绿色记忆</a>.</p>
]]></description>
				<content:encoded><![CDATA[<div class="wri_content_clear_both"><div class="blog_h1"><span class="graybg">简介</span></div>
<div class="blog_h2"><span class="graybg">Cilium</span></div>
<p>Cilium是在Docker/K8S之类的容器管理平台下，透明的为应用程序服务提供安全网络连接的开源软件。Cilium的底层技术是eBPF，eBPF完全在内核中运行，因此改变Cilium的安全策略时不需要程序代码、容器配置的任何变更。</p>
<div class="blog_h2"><span class="graybg">Hubble</span></div>
<p>Hubble是一个完全分布式的网络和安全可观察性平台。它构建在Cilium + eBPF之上，它以完全透明的方式，实现了服务、网络基础设施的通信/行为的深度可观察性。</p>
<p>由于可观察性依赖于eBPF，因此是可动态编程的、成本最小化的、可深度定制的。</p>
<p>Hubble可以回答以下问题：</p>
<ol>
<li>服务依赖和通信关系图：
<ol>
<li>两个服务是否通信，通信频度如何，服务之间的依赖关系是怎样的？</li>
<li>进行了哪些HTTP调用？</li>
<li>服务消费了哪些Kafka主题，发布了哪些Kafka主题</li>
</ol>
</li>
<li>网络监控和报警：
<ol>
<li>网络通信是否失败，为何失败？是DNS导致的失败？还是L4/L7的原因</li>
<li>最近5分钟有哪些服务存在DNS解析问题</li>
<li>哪些服务出现连接超时、中断的问题</li>
<li>无应答SYN请求的频率是多高</li>
</ol>
</li>
<li>应用程序监控：
<ol>
<li>特定服务/或者整个集群的5xx/4xx HTTP响应的频率是多高</li>
<li>HTTP请求延迟的95th/99th位数是多少</li>
<li>哪两个服务之间的延迟最高</li>
</ol>
</li>
<li>安全可观察性：
<ol>
<li>哪些服务因为网络策略而出现连接被阻止</li>
<li>哪些服务被从集群外部访问</li>
<li>哪些服务尝试解析了特定的域名</li>
</ol>
</li>
</ol>
<div class="blog_h2"><span class="graybg">优势</span></div>
<p>在监控（系统和应用）领域，从来没有一个技术能像eBPF一样做到如此的高性能、细粒度、透明化，以及动态性。</p>
<p>现代数据中心中运行的应用程序，通常基于微服务架构设计，应用程序被拆分为大量独立的小服务，这些服务基于轻量级的协议（例如HTTP）进行通信。这些微服务通常容器化部署，可以动态按需创建、销毁、扩缩容。</p>
<p>这种容器化的微服务架构，在连接安全性方面引入了挑战。传统的Linux网络安全机制，例如iptables，基于IP地址、TCP/UDP端口进行过滤。在容器化架构下IP地址会很快变化，这会导致ACL规则、LB表需要不断的、加速（随着业务规模扩大）更新。由于IP地址不稳定，给实现精准可观察性也带来了挑战。</p>
<p>依赖于eBPF，Cilium能够基于服务/Pod/容器的标识（而非IP地址），实现安全策略的更新。能够在L7/L4/L3进行过滤。</p>
<div class="blog_h2"><span class="graybg">能力</span></div>
<div class="blog_h3"><span class="graybg">透明的保护API</span></div>
<p>能够在L7进行过滤，支持REST/HTTP、gRPC、Kafka等协议。从而实现：</p>
<ol>
<li>允许对/public/.*的GET请求，禁止其它任何请求</li>
<li>允许service1在Kafka的主题topic1上发布消息，service2在topic1上消费消息，禁止其它Kafka消息</li>
<li>要求所有HTTP请求具有头X-Token: [0-9]+</li>
</ol>
<div class="blog_h3"><span class="graybg">基于身份标识的安全访问</span></div>
<p>经典的容器防火墙，基于IP地址/端口来进行封包过滤，每当有新的容器启动，都要求所有服务器更新防火墙规则。</p>
<p>Cilium支持为一组应用程序分配身份标帜，共享同一安全策略。身份标识将关联到容器发出的所有网络封包，在接收封包的节点，可以校验身份信息。</p>
<div class="blog_h3"><span class="graybg">外部服务安全访问</span></div>
<p>上一段提到了，Cilium支持基于身份标识的内部服务之间的安全访问机制。对于外部服务，Cilium支持经典的基于CIDR的ingress/egress安全策略。</p>
<div class="blog_h3"><span class="graybg">简单的容器网络</span></div>
<p>Cilium支持一个简单的、扁平的L3网络，能够跨越多个集群，连接所有容器。通过使用host scope的IP分配器，IP分配被保持简单，每个主机可以独立进行分配分配，不需要相互协作。</p>
<p>支持以下多节点网络模型：</p>
<ol>
<li>Overlay：目前内置支持VxLAN和Geneve，所有Linux支持的封装格式都可以启用</li>
<li>Native Routing：也叫Direct Routing，使用Linux宿主机的路由表，底层网络必须具有路由容器IP的能力。支持原生的IP6网络，能够和云网络路由器协作</li>
</ol>
<div class="blog_h3"><span class="graybg">负载均衡</span></div>
<p>Cilium实现了分布式的负载均衡，可以完全代替kube-proxy。LB基于eBPF实现，使用高效的、可无限扩容的哈希表来存储信息。</p>
<p>对于南北向负载均衡，Cilium作了最大化性能的优化。支持XDP、DSR（Direct Server Return，LB仅仅修改转发封包的目标MAC地址）。</p>
<p>对于东西向负载均衡，Cilium在内核套接字层（TCP连接时）执行高效的service-to-backend转换（通过eBPF直接修改封包），避免了更低层次（IP）中per-packet的NAT（依赖conntrack，在高并发或大量连接的情况下有若干问题）操作成本。</p>
<div class="blog_h3"><span class="graybg">带宽管理</span></div>
<p>Cilium利用eBPF实现高效的基于EDT（Earliest Departure Time）的egress限速，能够很大程度上避免HTB/TBF等经典qdisc的缺点，包括传输尾延迟，多队列NIC下的锁问题。</p>
<div class="blog_h3"><span class="graybg">监控和诊断</span></div>
<p>对于任何分布式系统，可观察性对于监控和故障诊断都非常重要。Cilium提供了更好的诊断工具：</p>
<ol>
<li>携带元数据的事件监控：当封包被丢弃时，不但报告源地址，而能提供 完整的发送者/接收者元数据</li>
<li>策略决策跟踪：支持跟踪并发现是什么策略导致封包丢弃或请求拒绝</li>
<li>支持通过Prometheus暴露指标</li>
<li>Hubble：一个专门为Cilium设计的可观察性平台，能够提供服务依赖图、监控和报警</li>
</ol>
<div class="blog_h1"><span class="graybg">架构</span></div>
<div class="blog_h2"><span class="graybg">整体架构</span></div>
<p>整体上的组件架构如下：</p>
<p><a href="https://blog.gmem.cc/wp-content/uploads/2020/06/cilium-arch.png"><img class="size-large wp-image-37593 aligncenter" src="https://blog.gmem.cc/wp-content/uploads/2020/06/cilium-arch-1024x912.png" alt="cilium-arch" width="710" height="632" /></a></p>
<div class="blog_h2"><span class="graybg">Cilium组件</span></div>
<div class="blog_h3"><span class="graybg">Agent</span></div>
<p>cilium-agent在集群的每个节点上运行，它通过K8S或API接收描述网络、服务负载均衡、网络策略、可观察性的配置信息。</p>
<p>cilium-agent监听来自容器编排系统的事件，从而知晓哪些容器被启动/停止，它管理所有eBPF程序，后者控制所有网络访问。</p>
<p>Cilium利用eBPF实现datapath的过滤、修改、监控、重定向，需要<span style="background-color: #c0c0c0;">Linux 4.8+才能运行，推荐使用4.9.17+内核</span>（因为4.8已经EOL）。Cilium会自动探测内核版本，识别可用特性。</p>
<div class="blog_h3"><span class="graybg">CLI</span></div>
<p>cilium和cilium-agent一起安装，它和cilium-agent的REST API交互，从而探测本地agent的状态。CLI也提供了直接访问eBPF map的工具。</p>
<div class="blog_h3"><span class="graybg">Operator</span></div>
<p>负责集群范围的工作，它不在任何封包转发/网络策略决策的关键路径上，即使operator暂时不可用，集群仍然能正常运作。</p>
<p>根据配置，operator持续不可用一段时间后，可能出现问题：</p>
<ol>
<li>如果需要operator来分配IP地址，则会出现IPAM延迟，因此导致新的工作负载的调度延迟</li>
<li>由于没有operator来更新kvstore的心跳，会导致agent认为kvstore不健康，并重启</li>
</ol>
<div class="blog_h3"><span class="graybg">CNI插件</span></div>
<p>cilium-cni和当前节点上的Cilium API交互，触发必要的datapath配置，以提供容器网络、LB、网络策略。</p>
<div class="blog_h2"><span class="graybg">Hubble组件</span></div>
<div class="blog_h3"><span class="graybg">Server</span></div>
<p>在所有节点上运行，从cilium中抓去eBPF的可观察性数据。它被嵌入在cilium-agent中，以实现高性能和低overhead。它提供了一个gRPC服务，用于抓取flow和Prometheus指标。</p>
<div class="blog_h3"><span class="graybg">Relay</span></div>
<p>hubble-relay是一个独立组件，能够连接到所有Server，通过Server的gRPC API，获取全集群的可观察性数据。这些数据又通过一个API来暴露出去。</p>
<div class="blog_h3"><span class="graybg">CLI</span></div>
<p>hubble是一个命令行工具，能够连接到gRPC API、hubble-relay、本地server，来获取flow events。</p>
<div class="blog_h3"><span class="graybg">GUI</span></div>
<p>hubble-ui能够利用hubble-relay的可观测性数据，提供图形化的服务依赖、连接图。</p>
<div class="blog_h2"><span class="graybg">数据存储</span></div>
<p>Cilium需要一个数据存储，用来在Agent之间传播状态。</p>
<div class="blog_h3"><span class="graybg">K8S CRD</span></div>
<p>默认数据存储。</p>
<div class="blog_h3"><span class="graybg">KV Store</span></div>
<p>外部键值存储，可以提供更好的性能。支持etcd和consul。</p>
<div class="blog_h1"><span class="graybg">概念</span></div>
<div class="blog_h2"><span class="graybg">术语</span></div>
<div class="blog_h3"><span class="graybg">Label</span></div>
<p>标签是定位大的资源集合的一种通用、灵活的方法。每当需要定位、选择、描述某些实体时，Cilium使用标签：</p>
<ol>
<li>Endpoint：从容器运行时、编排系统或者其它资源得到标签</li>
<li>Network Policy：根据标签来选择可以相互通信的一组Endpoint，网络策略自身也基于标签来识别</li>
</ol>
<p>标签就是键值对，值部分可以省略。键具有唯一性，一个实体上不会有两个相同键的标签。键通常仅仅包含字符<code class="docutils literal notranslate"><span class="pre">[a-z0-9-.]</span></code>。</p>
<p>标签从源提取，为了防止潜在的键冲突，Cilium为所有导入的键添加前缀。例如</p>
<ol>
<li><pre class="crayon-plain-tag">k8s:role=frontend</pre>：具有role=frontend的K8S Pod，对应的Cilium端点具有此标签</li>
<li><pre class="crayon-plain-tag">container:user=alex</pre>：通过docker run -l user=alex运行的容器，对应的Cilium端点具有此标签</li>
</ol>
<p>不同前缀含义如下：</p>
<p style="padding-left: 30px;"><pre class="crayon-plain-tag">container</pre>：从本地容器运行时得到的标签<br /><pre class="crayon-plain-tag">k8s</pre>：从Kubernetes得到的标签<br /><pre class="crayon-plain-tag">mesos</pre>：从Mesos得到的标签<br /><pre class="crayon-plain-tag">reserved</pre>：专用于特殊的保留标签<br /><pre class="crayon-plain-tag">unspec</pre>：未指定来源的标签</p>
<p>当通过标签来匹配资源时，使用前缀可以限定资源来源。如果不指定前缀，默认为<pre class="crayon-plain-tag">any:</pre>，标识匹配任何来源的资源。</p>
<div class="blog_h3"><span class="graybg">Endpoint</span></div>
<p>通过为容器分配IP，Cilium让它在网络上可见。多个应容器可能具有相同IP，典型的例子是Pod中的容器。任何享有同一IP的容器，在Cilium的术语里面，叫做端点。</p>
<p>Cilium的默认行为是，同时分配IPv4/IPv6地址给每个端点，你可以使用--enable-ipv4=false这样的选项来禁用某个IP版本。</p>
<p>在内部，Cilium为每个端点，在节点范围内，分配为唯一性的ID。</p>
<p>端点会从关联的容器自动提取端点元数据（Endpoint Metadata）。这些元数据用来安全、策略、负载均衡、路由上识别端点。端点元数据可能来自K8S的Pod标签、Mesos的标签、Docker的容器标签。</p>
<div class="blog_h3"><span class="graybg">Identity</span></div>
<p>任何端点都被分配身份标识（Identity），<span style="background-color: #c0c0c0;">身份标识通过端点的标签确定</span>，并且具有集群范围内的标识符（数字ID）。<span style="background-color: #c0c0c0;">端点被分配的身份标识，和它的安全相关标签匹配</span>，也就是说，具有相同安全相关标签的所有端点，共享同一身份标识。</p>
<p>仅仅安全相关标签用于确定端点的身份标识。<span style="background-color: #c0c0c0;">安全相关标签具有特定前缀</span>，默认情况下安全相关标签以<pre class="crayon-plain-tag">id.</pre>开头。启动cilium-agent时可以<span style="background-color: #c0c0c0;">指定自定义的前缀</span>。</p>
<p>特殊身份标识用于那些不被Cilium管理的端点，这些特殊标识以<pre class="crayon-plain-tag">reserved:</pre>作为前缀：</p>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 60px; text-align: center;">数字ID</td>
<td style="width: 200px; text-align: center;">身份标识</td>
<td style="text-align: center;">描述</td>
</tr>
</thead>
<tbody>
<tr>
<td>0</td>
<td>reserved:unknown</td>
<td>无法提取身份标识的任何端点</td>
</tr>
<tr>
<td>1</td>
<td>reserved:host</td>
<td>localhost，任何来自/发往本机IP的流量，都牵涉到该端点</td>
</tr>
<tr>
<td>2</td>
<td>reserved:world</td>
<td>任何集群外的端点</td>
</tr>
<tr>
<td>3</td>
<td>reserved:unmanaged</td>
<td>不被Cilium管理的网络端点，例如在Cilium安装前就存在的Pod</td>
</tr>
<tr>
<td>4</td>
<td>reserved:health</td>
<td>由cilium-agent发起健康检查流量而产生的端点</td>
</tr>
<tr>
<td>5</td>
<td>reserved:init</td>
<td>身份标识尚未提取的端点</td>
</tr>
<tr>
<td>6</td>
<td>reserved:remote-node</td>
<td>集群中所有其它节点的集合</td>
</tr>
</tbody>
</table>
<p>Cilium能够识别一些知名标签，包括k8s-app=kube-dns，并自动分配安全标识。这个特性的目的是，让Cilium顺利的在启用Policy的情况下自举并获得网络连接性。</p>
<p>Cilium利用分布式的KV存储，为身份标识产生数字ID。cilium-agent为会使用身份标识去查询，如果KV存储已经没有对应的数字ID，就会新创建一个。</p>
<div class="blog_h3"><span class="graybg">Node</span></div>
<p>集群的单个成员，每个节点都必须运行cilium-agent。</p>
<div class="blog_h2"><span class="graybg">网络安全</span></div>
<p>Cilium提供多个层次的安全特性，这些特性可以单独或者联合使用。</p>
<div class="blog_h3"><span class="graybg">基于身份标识</span></div>
<p>容器编排系统中倾向于产生大量的Pod，这些Pod具有独立IP。传统的基于IP的网络策略，在容器场景下需要大量的、频繁变动的规则。</p>
<p>Cilium则完全将网络地址和安全策略分开。作为代替，它总是基于Pod的身份标识（通过它的标签提取）应用安全策略。它能够允许任何具有role=frontend标签的Pod访问role=backend的Pod，不管Pod的数量多少。</p>
<div class="blog_h3"><span class="graybg">安全策略</span></div>
<p>如果运行从A到B发起通信，则自动意味着允许B到A的报文传输，但是不意味着B能够发起到A的通信。</p>
<p>安全策略可以在ingress/egress端应用。</p>
<p>如果不提供任何策略，默认行为是允许任何通信。一旦提供一个安全策略规则，则所有不在白名单中的流量都被丢弃。</p>
<div class="blog_h3"><span class="graybg">代理注入</span></div>
<p>Cilium能够透明的为任何网络连接注入L4代理，这是L7网络策略的基础。目前支持的代理实现是<a href="https://docs.cilium.io/en/v1.10/concepts/security/proxy/envoy/">Envoy</a>。</p>
<p>你可以用Go语言编写少量的、用于解析新协议的代码。这种Go代码能够完全利用Cilium提供的高性能的转发到/自Envoy代理的能力、丰富的L7感知策略定义语言、访问日志、基于kTLS的加密流量可观察性。总而言之，作为开发者你只需要使用Go语言编写协议解析代码，其它的事情Cilium+Envoy+eBPF会做好。</p>
<div class="blog_h2"><span class="graybg">网络数据路径概要</span></div>
<div class="blog_h3"><span class="graybg">L1-2</span></div>
<ol>
<li>当网卡接收到封包后，它可以通过PCI桥，将其存放到内存（ring buffer）中</li>
<li>内核中一般化的轮询机制NAPI Poll（epoll、驱动程序都会使用该机制），会拉取到ring buffer中的数据，开始处理</li>
</ol>
<p>&nbsp;</p>
<p><a href="https://blog.gmem.cc/wp-content/uploads/2020/06/datapath-l12.png"><img class="aligncenter wp-image-37985" src="https://blog.gmem.cc/wp-content/uploads/2020/06/datapath-l12.png" alt="datapath-l12" width="788" height="444" /></a></p>
<div class="blog_h3"><span class="graybg">L2</span></div>
<p>几乎所有驱动程序都会实现的<pre class="crayon-plain-tag">drvr_poll</pre>，它会调用第一个BPF程序，即XDP。</p>
<p>如果此程序返回<pre class="crayon-plain-tag">pass</pre>，内核会：</p>
<ol>
<li>调用clean_rx，在此Linux分配skb</li>
<li>如果启用GRO（Generic receive offload），则调用gro_rx，在此封包会被聚合，以一点延迟来换取吞吐量的提升。如果tcpdump时发现不可理解的巨大封包，可能是因为启用了GRO，你看到的是内核给的fake封包</li>
<li>调用receive_skb，开始L2接收处理</li>
</ol>
<p><a href="https://blog.gmem.cc/wp-content/uploads/2020/06/datapath-l2.png"><img class="wp-image-37983 aligncenter" src="https://blog.gmem.cc/wp-content/uploads/2020/06/datapath-l2.png" alt="datapath-l2" width="952" height="430" /></a></p>
<div class="blog_h3"><span class="graybg">L2-3</span></div>
<p>当调用receive_skb后：</p>
<ol>
<li>如果驱动没有实现XDP支持，则在此调用XDP BPF程序，这里的效率比较低</li>
<li>轮询所有的 socket tap，将包放到正确的（如果存在） tap 设备的缓冲区</li>
<li>调用tc BPF程序。这是Cilium最依赖的挂钩点，实现了修改封包（例如打标记）、重新路由、丢弃封包等操作。这里的BPF程序可能会影响qdisc统计信息，从而影响流量塑形。如果tc BPF程序返回OK，则进入netfilter</li>
<li>netfilter 也会对入向的包进行处理，它是<span style="background-color: #c0c0c0;">网络栈的下半部分</span>，iptables规则越多，对网络栈下半部分造成的瓶颈也就越大</li>
<li>取决于L3协议的类型（几乎都是IP），调用相应L3接收函数并进入网络栈第三层</li>
</ol>
<p><a href="https://blog.gmem.cc/wp-content/uploads/2020/06/datapath-l23.png"><img class="wp-image-37981 aligncenter" src="https://blog.gmem.cc/wp-content/uploads/2020/06/datapath-l23.png" alt="datapath-l23" width="967" height="435" /></a></p>
<div class="blog_h3"><span class="graybg">L3-4</span></div>
<p>当调用ip_rcv后：</p>
<ol>
<li>首先是netfilter钩子pre_routing，这里会从L4视角处理封包，会执行netfilter中的任何四层规则</li>
<li>netfilter处理完毕后，回调ip_rcv_finish</li>
<li>ip_rcv_finish会立即调用ip_routing对封包进行路由判断：是否位于lookback上，是否能够路由出去。如果Cilium没有使用隧道模式，则会使用到这里的路由功能</li>
<li>如果路由目的地是本机，则会调用ip_local_deliver。进而调用xfrm4_policy</li>
<li>xfrm4_policy负责完成包的封装、解封装、加解密。IPSec就是在此完成</li>
<li>根据L4协议的不同，调用相应的L4接收函数</li>
</ol>
<p><a href="https://blog.gmem.cc/wp-content/uploads/2020/06/datapath-l34.png"><img class="wp-image-37979 aligncenter" src="https://blog.gmem.cc/wp-content/uploads/2020/06/datapath-l34.png" alt="datapath-l34" width="964" height="353" /></a></p>
<div class="blog_h3"><span class="graybg">L4</span></div>
<p>这里以UDP为例，L4入口函数为udp_rcv：</p>
<ol>
<li>该函数会对封包的合法性进行验证，检查UDP的checksum</li>
<li>封包再次送到xfrm4_policy进行处理。这是因为某些transform policy能够指定L4协议，而此时L4协议才明确</li>
<li>根据端口，查找对应的套接字，然后将skb存放到一个链表s.rcv_q中</li>
<li>最后，调用sk_data_ready，标记套接字有数据待收取</li>
</ol>
<p><a href="https://blog.gmem.cc/wp-content/uploads/2020/06/datapath-l4.png"><img class="wp-image-37977 aligncenter" src="https://blog.gmem.cc/wp-content/uploads/2020/06/datapath-l4.png" alt="datapath-l4" width="788" height="428" /></a></p>
<div class="blog_h3"><span class="graybg">L4-userspace</span></div>
<ol>
<li>上节提到了，套接字（的等待队列）会被标记为有数据待收取。用户空间程序，通过epoll在等待队列上监听，而因获得通知</li>
<li>用户空间调用udp_recv_msg函数，后者会调用cgroup BPF程序。这种程序用来实现透明的客户端egressing负载均衡</li>
<li>最后是sock_ops BPF程序。用于socket level的细粒度流量塑形。对于某些功能来说这很重要，例如客户端限速</li>
</ol>
<p> <a href="https://blog.gmem.cc/wp-content/uploads/2020/06/l4-us.png"><img class="size-full wp-image-37989 aligncenter" src="https://blog.gmem.cc/wp-content/uploads/2020/06/l4-us.png" alt="l4-us" width="664" height="654" /></a></p>
<div class="blog_h2"><span class="graybg">BPF挂钩点和对象</span></div>
<p>Linux内核在网络栈中支持一系列的BPF挂钩点，用于挂接BPF程序。Cilium利用这些挂钩点来实现高层次的网络功能。</p>
<div class="blog_h3"><span class="graybg">挂钩点</span></div>
<p>Cilium用到的钩子包括：</p>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 150px; text-align: center;">钩子</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td>XDP</td>
<td>
<p>网络路径上最早的、可以软件介入的点，在驱动接收到封包之后，具有最好的封包处理性能</p>
<p>能够快速过滤恶意/非预期的流量，例如DDoS</p>
</td>
</tr>
<tr>
<td>tc ingress/egress</td>
<td>
<p>在封包已经开始最初的处理之后的挂钩点，此时内核L3处理尚未开始，但是已经能够访问大部分的封包元数据</p>
<p>适合进行本节点相关的处理，例如应用L3/L4端点策略，重定向流量到特定端点</p>
</td>
</tr>
<tr>
<td>socket operations</td>
<td>socket operation hook挂钩到特定的cgroup，并且当TCP事件发生时执行。Cilium挂钩到根cgroup，依此实现TCP状态转换的监控，特别是ESTABLISHED状态转换。当一个TCP套接字进入ESTABLISHED状态，并且它具有一个节点本地的对端（可能是一个本地的proxy），则自动执行socket send/recv钩子来进行加速</td>
</tr>
<tr>
<td>socket send/recv</td>
<td>
<p>每当TCP套接字执行send操作时触发，钩子可以探查消息，然后或者丢弃、或者将消息发送到TCP层，或者重定向给另外一个套接字</p>
<p>Cilium使用这种钩子来加速数据路径的重定向</p>
</td>
</tr>
</tbody>
</table>
<div class="blog_h3"><span class="graybg">网络对象</span></div>
<p>利用上面这些挂钩点，以及虚拟接口（cilium_host, cilium_net）、一个可选的Overlay接口（cilium_vxlan）、内核的crypto支持、以及用户空间代理Envoy，Cilium创建以下类型的网络对象：</p>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 150px; text-align: center;">网络对象</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td>Prefilter</td>
<td>
<p>这类对象运行XDP程序，提供一系列的预过滤规则，获得最大性能的封包过滤</p>
<p>通过Cillium Agent提供的CIDR map，被用<span style="background-color: #c0c0c0;">于快速查找，判定一个封包是否应该被丢弃</span>。例如，假设目的地址不是有效的端点，则应该快速丢弃</p>
</td>
</tr>
<tr>
<td>Endpoint Policy</td>
<td>
<p>这类对象实现Cilium端点策略，它使用一个Map来查询当前封包关联的身份标识，当端点数量很大时，性能不会变差</p>
<p>根据策略，在这一层可能<span style="background-color: #c0c0c0;">丢弃封包、转发给本地端点、转发给Service对象、转发给L7策略对象</span></p>
<p>在Cilium中，这是<span style="background-color: #c0c0c0;">映射封包到身份标识、以及<span style="background-color: #c0c0c0;">应用</span>L3/L4策略</span>的主要对象</p>
</td>
</tr>
<tr>
<td>Service</td>
<td>
<p>这类对象根据每个封包的目的地址来进行Map查找，寻找对应的Service，如果找到了，则封包被转发给Service的某个L3/L4端点</p>
<p>可以和Endpoint Policy对象集成；也可以实现独立的LB</p>
</td>
</tr>
<tr>
<td>L3 Encryption</td>
<td>
<p>在ingress端，L3 Encryption对象标记封包为待解密，随后封包被传递给内核的xfrm（transform）层进行解密，随后解密后的封包传回，并交给网络栈中的其它对象进行后续处理</p>
<p>在egress端，首先根据目的地址进行Map查找，判断是否需要加密，如果是，目标节点上哪些key可用。同时在两端可用的、最近的key被用来加密。封包随后被标记为待解密，传递给内核的xfrm层。加密后的封包，传递给下一层处理，可能是传递给Linux网络栈进行路由，使用overlay的情况下可能直接发起一个尾调用</p>
</td>
</tr>
<tr>
<td>Socket Layer Enforcement</td>
<td>
<p>使用两类钩子：socket operations、socket send/recv，来<span style="background-color: #c0c0c0;">监、控所有Cilium管理的端点（包括L7代理）的TCP连接</span></p>
<p><span style="background-color: #c0c0c0;">socket operations钩子</span>否则<span style="background-color: #c0c0c0;">识别候选的、可加速的套接字</span>。这些<span style="background-color: #c0c0c0;">可加速套接字包括所有本地端点之间的连接、任何发往Cilium代理的连接</span>。可加速套接字的所有封包都会被socket send/recv钩子处理 —— 通过BPF sockmap进行快速重定向</p>
</td>
</tr>
<tr>
<td>L7 Policy</td>
<td>该对象将代理流量重定向给Cilium的用户空间代理，也就是Envoy。Envoy随后要么转发流量，要么根据配置的L7策略生成适当的reject消息</td>
</tr>
</tbody>
</table>
<div class="blog_h3"><span class="graybg">BPF Maps</span></div>
<p>Cilium使用了大量的BPF Maps，这些Map创建的时候都进行了容量限制。超过限制后，无法插入数据，因此限制了数据路径的扩容能力。下表显示了默认容量：</p>
<table class="docutils full-width" border="1">
<tbody valign="top">
<tr class="row-even">
<td style="width: 150px; text-align: center;"><strong>Map类别</strong></td>
<td style="text-align: center;"><strong>作用域</strong></td>
<td style="width: 80px; text-align: center;"><strong>默认限制</strong></td>
<td style="text-align: center;"><strong>扩容影响</strong></td>
</tr>
<tr>
<td>Connection Tracking</td>
<td>node<br />endpoint</td>
<td>1M TCP<br />256k UDP</td>
<td>Max 1M concurrent TCP connections, max 256k expected UDP answers</td>
</tr>
<tr class="row-odd">
<td>NAT</td>
<td>node</td>
<td>512k</td>
<td>Max 512k NAT entries</td>
</tr>
<tr class="row-even">
<td>Neighbor Table</td>
<td>node</td>
<td>512k</td>
<td>Max 512k neighbor entries</td>
</tr>
<tr class="row-odd">
<td>Endpoints</td>
<td>node</td>
<td>64k</td>
<td>Max 64k local endpoints + host IPs per node</td>
</tr>
<tr class="row-even">
<td>IP cache</td>
<td>node</td>
<td>512k</td>
<td>Max 256k endpoints (IPv4+IPv6), max 512k endpoints (IPv4 or IPv6) across all clusters</td>
</tr>
<tr class="row-odd">
<td>Load Balancer</td>
<td>node</td>
<td>64k</td>
<td>Max 64k cumulative backends across all services across all clusters</td>
</tr>
<tr class="row-even">
<td>Policy</td>
<td>endpoint</td>
<td>16k</td>
<td>Max 16k allowed identity + port + protocol pairs for specific endpoint</td>
</tr>
<tr class="row-odd">
<td>Proxy Map</td>
<td>node</td>
<td>512k</td>
<td>Max 512k concurrent redirected TCP connections to proxy</td>
</tr>
<tr class="row-even">
<td>Tunnel</td>
<td>node</td>
<td>64k</td>
<td>Max 32k nodes (IPv4+IPv6) or 64k nodes (IPv4 or IPv6) across all clusters</td>
</tr>
<tr class="row-odd">
<td>IPv4 Fragmentation</td>
<td>node</td>
<td>8k</td>
<td>Max 8k fragmented datagrams in flight simultaneously on the node</td>
</tr>
<tr class="row-even">
<td>Session Affinity</td>
<td>node</td>
<td>64k</td>
<td>Max 64k affinities from different clients</td>
</tr>
<tr class="row-odd">
<td>IP Masq</td>
<td>node</td>
<td>16k</td>
<td>Max 16k IPv4 cidrs used by BPF-based ip-masq-agent</td>
</tr>
<tr class="row-even">
<td>Service Source Ranges</td>
<td>node</td>
<td>64k</td>
<td>Max 64k cumulative LB source ranges across all services</td>
</tr>
<tr class="row-odd">
<td>Egress Policy</td>
<td>endpoint</td>
<td>16k</td>
<td>Max 16k endpoints across all destination CIDRs across all clusters</td>
</tr>
</tbody>
</table>
<p>部分BPF Map的容量上限可以通过cilium-agent的命令行选项覆盖：</p>
<p style="padding-left: 30px;">--bpf-ct-global-tcp-max<br />--bpf-ct-global-any-max<br />--bpf-nat-global-max<br />--bpf-neigh-global-max<br />--bpf-policy-map-max<br />--bpf-fragments-map-max<br />--bpf-lb-map-max</p>
<p>如果指定了--bpf-ct-global-tcp-max或/和--bpf-ct-global-any-max，则NAT表（<pre class="crayon-plain-tag">--bpf-nat-global-max</pre>）的大小不能超过前面两个表合计大小的2/3。</p>
<p>使用<pre class="crayon-plain-tag">--bpf-map-dynamic-size-ratio=0.0025</pre>，则cilium-agent在启动时能够动态根据总计内存来调整Map的容量。该选项取值0.0025则0.25%的系统内存用于BPF Map。 该标记会影响消耗大部分内存的Map，包括：</p>
<p style="padding-left: 30px;">cilium_ct_{4,6}_global<br />cilium_ct_{4,6}_any<br />cilium_nodeport_neigh{4,6}<br />cilium_snat_v{4,6}_external<br />cilium_lb{4,6}_reverse_sk</p>
<p>Cilium使用自己的，基于BPF Map实现的连接跟踪表，--bpf-map-dynamic-size-ratio影响容量，但是不会小于131072。</p>
<div class="blog_h2"><span class="graybg">封包生命周期</span></div>
<div class="blog_h3"><span class="graybg">从端点到端点</span></div>
<p><a href="https://blog.gmem.cc/wp-content/uploads/2020/06/cilium_bpf_endpoint.png"><img class="alignnone wp-image-37657 " src="https://blog.gmem.cc/wp-content/uploads/2020/06/cilium_bpf_endpoint.png" alt="cilium_bpf_endpoint" width="1070" height="1124" /></a></p>
<p>上图包含两个部分：</p>
<ol>
<li>上半部分：本地端点到端点数据流图，显示了Cilium如何配合L7代理进行封包重定向的细节</li>
<li>下半部分：启用了Socket Layer Enforcement后的数据流图。这种情况下，TCP连接的握手阶段，需要遍历Endpoint Policy，直到ESTABLISHED，之后仅仅需要L7 Policy</li>
</ol>
<p>如果启用了L7规则，则流量会被转发给用户空间代理，代理处理完后，转发给目的端点的代理，后者再转发给目的端点的Pod。转发都是由bpf_redir负责，直接修改封包。</p>
<div class="blog_h3"><span class="graybg">端点到Egress</span></div>
<p><a href="https://blog.gmem.cc/wp-content/uploads/2020/06/cilium_bpf_egres.png"><img class="alignnone wp-image-37661 " src="https://blog.gmem.cc/wp-content/uploads/2020/06/cilium_bpf_egres.png" alt="cilium_bpf_egres" width="1079" height="724" /></a></p>
<p>跨节点的封包流，可能牵涉到overlay，默认情况下overlay接口的名字是cilium_vxlan。</p>
<p>如果需要L3 Encryption，则Endpoint端的tc钩子会将其流量传递给L3 Encryption处理。需要注意tc BPF程序的<span id="crayon-60d3fea497f3d798084325" class="crayon-syntax crayon-syntax-inline  crayon-theme-gmem-github crayon-theme-gmem-github-inline crayon-font-consolas" style="font-size: 15px !important; line-height: 20px !important;"><span class="crayon-pre crayon-code" style="font-size: 15px !important; line-height: 20px !important; -moz-tab-size: 4; -o-tab-size: 4; -webkit-tab-size: 4; tab-size: 4;"><span class="crayon-v">da</span></span></span>模式，能够直接对封包进行修改、转发，而不需要外部的tc action模块。</p>
<p>和端点到端点流量类似，当启用Socket Layer Enforcement时，并且使用L7代理，则对于TCP流量可以避免运行端点和L7代理之间的Endpoint Policy。</p>
<div class="blog_h3"><span class="graybg">Ingress到端点</span></div>
<p><a href="https://blog.gmem.cc/wp-content/uploads/2021/06/cilium_bpf_ingress.svg"><img class="alignnone" src="https://blog.gmem.cc/wp-content/uploads/2021/06/cilium_bpf_ingress.svg" alt="" width="1068" height="507" /></a></p>
<p>和端点到端点流量类似，当启用Socket Layer Enforcement时，并且使用L7代理，则对于TCP流量可以避免运行端点和L7代理之间的Endpoint Policy。</p>
<p>这种封包流可以被Prefilter快速处理，决定是否需要丢弃封包、是否需要进行负载均衡处理。</p>
<div class="blog_h2"><span class="graybg">iptables</span></div>
<p>依赖于实际使用的Linux内核版本，Cilium能够利用eBPF datapath全部或部分特性。如果Linux内核版本较低，某些功能可能基于iptables实现。</p>
<p>下图显示了Cilium和kube-proxy安装的iptables规则以及相互关系：<a href="https://blog.gmem.cc/wp-content/uploads/2021/06/kubernetes_iptables.svg"><img class="alignnone" src="https://blog.gmem.cc/wp-content/uploads/2021/06/kubernetes_iptables.svg" alt="" width="1078" height="962" /></a></p>
<div class="blog_h1"><span class="graybg">安装</span></div>
<div class="blog_h2"><span class="graybg">前提条件</span></div>
<div class="blog_h3"><span class="graybg">内核</span></div>
<p>要求内核版本4.9.17或者更高。</p>
<div class="blog_h3"><span class="graybg">K8S</span></div>
<p>必须启用CNI作为网络插件。</p>
<div class="blog_h2"><span class="graybg">下载命令行工具</span></div>
<pre class="crayon-plain-tag">curl -L --remote-name-all https://github.com/cilium/cilium-cli/releases/latest/download/cilium-linux-amd64.tar.gz{,.sha256sum}
sha256sum --check cilium-linux-amd64.tar.gz.sha256sum
sudo tar xzvfC cilium-linux-amd64.tar.gz /usr/local/bin
rm cilium-linux-amd64.tar.gz{,.sha256sum}</pre>
<div class="blog_h2"><span class="graybg">安装到K8S</span></div>
<div class="blog_h3"><span class="graybg">通过命令行工具</span></div>
<p>参考下面的命令进行安装：</p>
<pre class="crayon-plain-tag"># 安装到当前Kubernetes context
cilium install

cilium install
  --agent-image string              # Cilium Agent镜像
  --operator-image                  # Cilium Operator镜像
  --cluster-id int                  # 多集群模式下唯一ID
  --cluster-name string             # 多集群模式下此集群的名字
  --config strings                  # 添加Cilium配置条目，对应ConfigMap中的一个键值
  --context string                  # 使用的K8S Context
  --datapath-mode                   # 使用的datapath模式
  --disable-check strings           # 禁用指定的校验
  --encryption string               # 所有工作负载流量的加密：disabled（默认） | ipsec | wireguard
  --inherit-ca string               # 从另外一个集群继承/导入CA
  --ipam string                     # IPAM模式
  --kube-proxy-replacement string   # kube-proxy replacement工作模式：disabled（默认） | probe | strict
  -n, --namespace                   # Cilium安装到什么命名空间，默认kube-system
  --native-routing-cidr string      # 直接路由的CIDR，和PodCIDR一致
  --node-encryption                 # 加密所有节点到节点流量
  --restart-unmanaged-pods          # 重启所有没有被Cilium管理的Pod，默认true，保证所有Pod获得Cilium提供的容器网络
  --wait                            # 等待安装完毕，默认true


cilium install  \
  --agent-image=docker.gmem.cc/cilium/cilium:v1.10.1 \
  --operator-image=docker.gmem.cc/cilium/operator-generic:v1.10.1</pre>
<p>如果安装失败，可以通过命令<pre class="crayon-plain-tag">cilium status</pre> 查看整体部署状态，查看日志。</p>
<p>安装完毕后，使用下面的命令来检查状态、进行连通性测试：</p>
<pre class="crayon-plain-tag">cilium status --wait

cilium connectivity test</pre>
<div class="blog_h3"><span class="graybg">通过Helm</span></div>
<pre class="crayon-plain-tag">helm repo add cilium https://helm.cilium.io/



helm install cilium cilium/cilium --version 1.10.1 \
  --namespace kube-system

  # 是否启用调试日志
  --set debug.enabled=true

  # 集群ID，整数，范围1-255，网格中所有集群都必须有唯一ID
  --set cluster.id=1
  # 集群的名字，仅对于集群网格需要
  --set cluster.name=gmem

  # 容器网络路径选项，veth或者ipvlan
  --set datapathMode=veth

  # 禁用隧道，使用直接路由
  --set tunnel=disabled
  # 如果所有节点位于L2网络中，下面的选项用于自动在工作节点之间同步PodCIDR的路由
  # 如果不指定该选项，节点之间的路由不会同步，会出现节点A无法访问B上Pod IP的问题
  --set autoDirectNodeRoutes=true
  # 指定可以直接进行路由（访问时不需要进行IP遮掩）的CIDR，对应K8S配置的cluster-cidr（PodCIDR）
  # 禁用隧道后必须手工设置，否则报错
  --set nativeRoutingCIDR=172.27.0.0/16

  # 当ConfigMap改变时，滚动更新cilium-agent
  --set rollOutCiliumPods=false
  # 为cilium-config这个ConfigMap配置额外的键值
  --set extraConfig={}

  # 传递cilium-agent的额外命令行选项
  --set extraArgs=[]

  # 传递cilium-agent的额外环境变量
  --set extraEnv={}
  
  # 是否在Cilium中启用内置BGP支持
  --set bgp.enabled=false
  # 是否分配、宣告LoadBalancer服务的IP地址
  --set bgp.announce.loadbalancerIP=false

  # 强制cilium-agent在init容器中等待eBPF文件系统已挂载
  --set bpf.waitForMount=false
  # 是否预先分配eBPF Map的键值，会增加内存消耗并降低延迟
  --set bpf.preallocateMaps=false
  # TCP连接跟踪表的最大条目数量
  --set bpf.ctTcpMax=524288
  # 非TCP连接跟踪表的最大条目数量
  --set bpf.ctAnyMax=262144
  # 负载均衡表中最大服务条目
  --set bpf.lbMapMax=65536
  # NAT表最大条目数量
  --set bpf.natMax=524288
  # neighbor表最大条目数量
  --set bpf.neighMax=524288
  # 端点策略映射最大条目数量
  --set bpf.policyMapMax=16384
  # 配置所有BPF Map的自动sizing，根据可用内存
  --set bpf.mapDynamicSizeRatio=0.0025
  # 监控通知（monitor notifications）的聚合级别 none, low, medium, maximum
  --set bpf.monitorAggregation=medium
  # 活动连接的监控通知的间隔
  --set bpf.monitorInterval=5s
  # 哪些TCP flag第一次出现在某个连接中，会触发通知
  --set bpf.monitorFlags=all
  # 允许从外部访问集群的ClusterIP
  --set bpf.lbExternalClusterIP=false
  # 即用基于eBPF的IP遮掩支持
  --set bpf.masquerade=true
  # 直接路由模式，是通过宿主机网络栈进行（true），还是（如果内核支持）使用更直接的、高效的eBPF（false）
  # 后者的副作用是，跳过宿主机的netfilter
  --set bpf.hostRouting=true
  # 是否启用基于eBPF的TPROXY，以便在实现L7策略时减少对iptables的依赖
  --set bpf.tproxy=true
  # NodePort反向NAT处理时，是否跳过FIB查找
  --set bpf.lbBypassFIBLookup=true

  # 每当cilium-agnet重启时，清空BPF状态
  --set cleanBpfState=false
  # 每当cilium-agnet重启时，清空所有状态
  --set cleanState=false

  # 和其它CNI插件组成链，可选值none generic-veth portmap
  --set cni.chainingMode=none
  # 让Cilium管理/etc/cni/net.d目录，将其它CNI插件的配置改为*.cilium_bak
  --set cni.exclusive=true
  # 如果你希望通过外部机制将CNI配置写入，则设置为true
  --set cni.customConf=false
  --set cni.confPath: /etc/cni/net.d
  --set cni.binPath: /opt/cni/bin

  # 配置容器运行时集成  containerd crio docker none auto
  --set containerRuntime.integration=none

  # 支持对自定义BPF程序的尾调用
  --set customCalls.enabled=false

  # IPAM模式
  --set ipam.mode=cluster-pool
  # IPv4 CIDR
  --set ipam.operator.clusterPoolIPv4PodCIDR=0.0.0.0/8
  --set ipam.operator.clusterPoolIPv4MaskSize=24
  # IPv6 CIDR
  --set ipam.operator.clusterPoolIPv6PodCIDR=fd00::/104
  --set ipam.operator.clusterPoolIPv6MaskSize=120

  # 配置基于eBPF的ip-masq-agent
  --set ipMasqAgent.enabled=false

  # IP协议版本支持
  --set ipv4.enabled=true
  --set ipv6.enabled=false

  # 如果启用，这重定向、SNAT离开集群的流量
  --set egressGateway.enabled=false

  # 启用监控sidecar
  --set monitor.enabled=false

  # 配置Service负载均衡
  # 是否启用独立的、不连接到kube-apiserver的L4负载均衡器
  --set loadBalancer.standalone=false
  # 负载均衡算法 random或者maglev
  --set loadBalancer.algorithm=random
  # 对于远程后端，LB操作模式 snat, dsr, hybrid
  --set loadBalancer.mode=snat
  # 是否基于XDP来加速服务处理
  --set loadBalancer.acceleration=disabled
  # 是否利用IP选项/IPIP封装，来将Service的IP/端口信息传递到远程后端
  --set loadBalancer.dsrDispatch=opt

  # 是否对从端点离开节点的流量进行IP遮掩
  --set enableIPv4Masquerade=true
  --set enableIPv6Masquerade=true

  # 支持L7网络策略
  --set l7Proxy=true

  # 镜像
  --set image.repository=docker.gmem.cc/cilium/cilium
  --set image.useDigest=false
  --set operator.image.repository=docker.gmem.cc/cilium/operator
  --set operator.image.useDigest=false


# 重启所有被有被cilium管理的Pod
kubectl get pods --all-namespaces -o custom-columns=NAMESPACE:.metadata.namespace,NAME:.metadata.name,HOSTNETWORK:.spec.hostNetwork --no-headers=true | grep '&lt;none&gt;' | awk '{print "-n "$1" "$2}' | xargs -L 1 -r kubectl delete pod</pre>
<div class="blog_h2"><span class="graybg">高级安装</span></div>
<div class="blog_h3"><span class="graybg">使用外部Etcd</span></div>
<p>使用独立的外部Etcd（而非K8S自带的）可以提供更好的性能适用于更大的部署环境。</p>
<p>选用外部Etcd的时机可能是：</p>
<ol>
<li>超过250节点，5000个Pod。或者，在通过Kubernetes evnets进行状态传播时，出现了很高的overhead</li>
<li>你不希望利用CRD来存储Cilium状态</li>
</ol>
<pre class="crayon-plain-tag">helm install cilium cilium/cilium --version 1.10.1 \
  --namespace kube-system \
  --set etcd.enabled=true \
  --set "etcd.endpoints[0]=http://etcd-endpoint1:2379" \
  --set "etcd.endpoints[1]=http://etcd-endpoint2:2379" \
  --set "etcd.endpoints[2]=http://etcd-endpoint3:2379" \
  # 不使用CRD来存储状态
  --set identityAllocationMode=kvstore


# 使用SSL
kubectl create secret generic -n kube-system cilium-etcd-secrets \
    --from-file=etcd-client-ca.crt=ca.crt \
    --from-file=etcd-client.key=client.key \
    --from-file=etcd-client.crt=client.crt

helm install cilium cilium/cilium --version 1.10.1 \
  --namespace kube-system \
  --set etcd.enabled=true \
  --set etcd.ssl=true \
  --set "etcd.endpoints[0]=https://etcd-endpoint1:2379" \
  --set "etcd.endpoints[1]=https://etcd-endpoint2:2379" \
  --set "etcd.endpoints[2]=https://etcd-endpoint3:2379"</pre>
<div class="blog_h3"><span class="graybg">CNI Chaining</span></div>
<p>CNI Chaining允许联用Cilium和其它CNI插件。联用时某些Cilium高级特性不可用，包括：</p>
<ol>
<li><a href="https://github.com/cilium/cilium/issues/12454">L7策略</a></li>
<li><a href="https://github.com/cilium/cilium/issues/15596">IPsec透明加密</a></li>
</ol>
<p>你需要创建一个CNI配置，使用plugin list：</p>
<pre class="crayon-plain-tag">apiVersion: v1
kind: ConfigMap
metadata:
  name: cni-configuration
  namespace: kube-system
data:
  cni-config: |-
    {
      "name": "generic-veth",
      "cniVersion": "0.3.1",
      "plugins": [
        {
          "type": "calico",
          "log_level": "info",
          "datastore_type": "kubernetes",
          "mtu": 1440,
          "ipam": {
              "type": "calico-ipam"
          },
          "policy": {
              "type": "k8s"
          },
          "kubernetes": {
              "kubeconfig": "/etc/cni/net.d/calico-kubeconfig"
          }
        },
        {
          "type": "portmap",
          "snat": true,
          "capabilities": {"portMappings": true}
        },
        {
          "type": "cilium-cni"
        }
      ]
    }</pre><br />
<pre class="crayon-plain-tag">helm install cilium cilium/cilium --version 1.10.1 \
  --namespace=kube-system \
  --set cni.chainingMode=generic-veth \
  --set cni.customConf=true \
  --set cni.configMap=cni-configuration \
  --set tunnel=disabled \
  --set enableIPv4Masquerade=false \
  --set enableIdentityMark=false</pre>
<div class="blog_h1"><span class="graybg">K8S集成</span></div>
<p>Cilium能够为K8S带来：</p>
<ol>
<li>基于CNI的容器网络支持</li>
<li>基于身份标识实现的NetworkPolicy，用于隔离L3/L4连接性</li>
<li>CRD形式的NetworkPolicy扩展，支持：
<ol>
<li>L7策略，目前支持HTTP、Kafka等协议</li>
<li>Egress策略支持CIDR</li>
</ol>
</li>
<li>ClusterIP实现，提供分布式的负载均衡。完全兼容kube-proxy模型</li>
</ol>
<p>支持的K8S版本为1.16+，内核版本4.9+。</p>
<p>K8S能够自动分配per-node的CIDR，通过kube-controller-manager的命令行选项<pre class="crayon-plain-tag">--allocate-node-cidrs</pre>启用此特性。Cilium会自动使用分配的CIDR。</p>
<div class="blog_h2"><span class="graybg">ConfigMap</span></div>
<p>Cilium使用名为cilium-config的ConfigMap：</p>
<pre class="crayon-plain-tag">apiVersion: v1
kind: ConfigMap
metadata:
  name: cilium-config
  namespace: kube-system
data:
  # The kvstore configuration is used to enable use of a kvstore for state
  # storage.
  kvstore: etcd
  kvstore-opt: '{"etcd.config": "/var/lib/etcd-config/etcd.config"}'

  # Etcd配置
  etcd-config: |-
    ---
    endpoints:
      - https://node-1:31079
      - https://node-2:31079
    trusted-ca-file: '/var/lib/etcd-secrets/etcd-client-ca.crt'
    key-file: '/var/lib/etcd-secrets/etcd-client.key'
    cert-file: '/var/lib/etcd-secrets/etcd-client.crt'

  # 是否让Cilium运行在调试模式下
  debug: "false"
  # 是否启用IPv4地址支持
  enable-ipv4: "true"
  # 是否启用IPv6地址支持
  enable-ipv6: "true"
  # 在启动cilium-agent时，从文件系统中移除所有eBPF状态。这会导致进行中的连接中断、负载均衡决策丢失
  # 所有的eBPF状态将从源（例如K8S或kvstore）重新构造
  # 该选项用于缓和严重的eBPF maps有关的问题，并且在打开、重启cilium-agent后，立即关闭
  clean-cilium-bpf-state: "false"
  # 清除所有Cilium状态，包括钉在文件系统中的eBPF状态、CNI配置文件、端点状态
  # 当前被Cilium管理的Pod可能继续正常工作，但是可能在没有警告的情况下不再工作
  clean-cilium-state: "false"
  # 该选项启用在cilium monitor中的追踪事件的聚合
  monitor-aggregation: none, low, medium, maximum
  # 启用Map条目的预分配，这样可以降低per-packet的延迟，代价是提前的为Map中条目分配内存
  # 如果此选项改变，则cilium-agnet下次重启会导致具有活动连接的端点临时性中断
  preallocate-bpf-maps: "true"</pre>
<p>修改此ConfigMap后，你需要重新启动cilium-agent才能生效。需要注意，K8S的ConfigMap变更可能需要2分钟才能传播到所有节点。</p>
<div class="blog_h2"><span class="graybg">NetworkPolicy</span></div>
<p>K8S标准的NetworkPolicy，可以用来指定L3/L4 ingress策略，以及受限的egress策略。详细参考<a href="/kubernetes-study-note#network-policy">Kubernetes学习笔记</a>。</p>
<div class="blog_h2"><span class="graybg">CiliumNetworkPolicy</span></div>
<p>功能类似于标准的NetworkPolicy，但是提供丰富的多的特性，能够配置L3/L4/L7策略。</p>
<div class="blog_h2"><span class="graybg">L3策略</span></div>
<p>L3策略用于提供端点之间基本的连接性。支持通过以下方式来指定：</p>
<ol>
<li>基于标签：当通信双方端点都被Cilium管理（因而被提取了标签）时，使用此方式。此方式的优点是IP地址之类的易变信息不会编码在策略中</li>
<li>基于服务：自动提取、维护编排系统的服务的后端IP列表（对于K8S就是Service的Endpoint的IP地址列表）。即使端点不会Cilium管理，这种方式也可以避免硬编码IP到策略中</li>
<li>基于实体：实体用于描述那些被归类的、不需要知道其IP地址的端点。例如具有reserved:身份标识的那些端点</li>
<li>基于IP/CIDR：当外部服务不是一个端点时使用</li>
<li>基于DNS：先进行DNS查找，然后转换为IP</li>
</ol>
<div class="blog_h3"><span class="graybg">基于标签</span></div>
<pre class="crayon-plain-tag">## ingress示例

# 允许frontend访问backend
apiVersion: "cilium.io/v2"
kind: CiliumNetworkPolicy
metadata:
  name: "l3-rule"
spec:
  endpointSelector:
    matchLabels:
      role: backend
  ingress:
  - fromEndpoints:
    - matchLabels:
        role: frontend

# 允许所有端点访问victim
kind: CiliumNetworkPolicy
metadata:
  name: "allow-all-to-victim"
spec:
  endpointSelector:
    matchLabels:
      role: victim
  ingress:
  - fromEndpoints:
    - {}



## egress示例

# 允许frontend访问backend
apiVersion: "cilium.io/v2"
kind: CiliumNetworkPolicy
metadata:
  name: "l3-egress-rule"
spec:
  endpointSelector:
    matchLabels:
      role: frontend
  egress:
  - toEndpoints:
    - matchLabels:
        role: backend

# 允许frontend访问所有端点
apiVersion: "cilium.io/v2"
kind: CiliumNetworkPolicy
metadata:
  name: "allow-all-from-frontend"
spec:
  endpointSelector:
    matchLabels:
      role: frontend
  egress:
  - toEndpoints:
    - {}

# 禁止restricted访问任何端点
apiVersion: "cilium.io/v2"
kind: CiliumNetworkPolicy
metadata:
  name: "deny-all-egress"
spec:
  endpointSelector:
    matchLabels:
      role: restricted
  egress:
  - {}</pre>
<p>在设计策略时，通常遵循关注点分离原则。CiliumNetworkPolicy支持设置任何连接性发生所需要的“前提条件”。字段fromRequires用于为任何fromEndpoints指定前提条件。类似的还有toRequires。</p>
<pre class="crayon-plain-tag">apiVersion: "cilium.io/v2"
kind: CiliumNetworkPolicy
metadata:
  name: "requires-rule"
specs:
  # 对于生产环境下的Pod，允许访问它的端点，必须也在生产环境中（前提条件）
  - description: "For endpoints with env=prod, only allow if source also has label env=prod"
    endpointSelector:
      matchLabels:
        env: prod
    ingress:
    - fromRequires:
      - matchLabels:
          env: prod</pre>
<p>上面这个 fromRequires规则本身不会允许任何流量，它必须和fromEndpoints规则进行“与”才能允许特定流量：</p>
<pre class="crayon-plain-tag">apiVersion: "cilium.io/v2"
kind: CiliumNetworkPolicy
metadata:
  name: "l3-rule"
specs:
  # 配合上面的规则，效果就是，生产环境下的前端组件，可以访问生产环境下的端点
  - description: "For endpoints with env=prod, allow if source also has label role=frontend"
    endpointSelector:
      matchLabels:
        env: prod
    ingress:
    - fromEndpoints:
      - matchLabels:
          role: frontend</pre>
<div class="blog_h3"><span class="graybg">基于服务</span></div>
<p>运行在集群中的服务，可以在Egress规则的白名单中列出：</p>
<pre class="crayon-plain-tag"># 使用服务名
apiVersion: "cilium.io/v2"
kind: CiliumNetworkPolicy
metadata:
  name: "service-rule"
spec:
  # 策略控制的目标端点，总是通过标签选择
  endpointSelector:
    matchLabels:
      id: app2
  egress:
  # 允许访问特定服务
  - toServices:
    - k8sService:
        serviceName: myservice
        namespace: default


# 使用服务选择器
apiVersion: "cilium.io/v2"
kind: CiliumNetworkPolicy
metadata:
  name: "service-labels-rule"
spec:
  endpointSelector:
    matchLabels:
      id: app2
  egress:
  - toServices:
    - k8sServiceSelector:
        selector:
          matchLabels:
            head: none</pre>
<p>fromEntities用于描述哪些实体可以访问选择的端点； toEntities则用于描述选择的端点能够访问哪些实体。</p>
<div class="blog_h3"><span class="graybg">基于身份标识</span></div>
<p>支持的实体参考前文描述的具有reserved:前缀的特殊身份标识。</p>
<pre class="crayon-plain-tag">apiVersion: "cilium.io/v2"
kind: CiliumNetworkPolicy
metadata:
  name: "dev-to-host"
spec:
  endpointSelector:
    matchLabels:
      env: dev
  # 允许开发环境端点访问其本机上的实体
  egress:
    - toEntities:
      - host


apiVersion: "cilium.io/v2"
kind: CiliumNetworkPolicy
metadata:
  name: "to-dev-from-nodes-in-cluster"
spec:
  endpointSelector:
    matchLabels:
      env: dev
  # 允许本机、集群远程机器访问开发环境端点
  # 注意，K8S默认允许从宿主机访问任何本地端点，cilium-agnet选项 --allow-localhost=policy可以禁用这默认行为
  ingress:
    - fromEntities:
      - host
      - remote-node


apiVersion: "cilium.io/v2"
kind: CiliumNetworkPolicy
metadata:
  name: "from-world-to-role-public"
spec:
  endpointSelector:
    matchLabels:
      role: public
  # 允许集群外部访问role:public的端点
  ingress:
    - fromEntities:
      - world</pre>
<div class="blog_h3"><span class="graybg">基于CIDR</span></div>
<p>不被Cilium管理的实体，没有标签，不属于端点。这些实体通常是运行在特定子网中的外部服务、VM、裸金属机器。这类实体在策略中，可以用CIDR规则来描述。</p>
<p>CIDR规则不能用在通信两端都是以下之一的场景：</p>
<ol>
<li>被Cilium管理的端点</li>
<li>使用属于集群节点的IP的实体，包括使用host networking的Pod</li>
</ol>
<pre class="crayon-plain-tag">apiVersion: "cilium.io/v2"
kind: CiliumNetworkPolicy
metadata:
  name: "cidr-rule"
spec:
  endpointSelector:
    matchLabels:
      app: myService
  # 允许访问外部CIDR
  egress:
  - toCIDR:
    - 20.1.1.1/32
  - toCIDRSet:
    - cidr: 10.0.0.0/8
      except:
      - 10.96.0.0/12</pre>
<div class="blog_h3"><span class="graybg">基于DNS</span></div>
<p>使用DNS名称来指定不被Cilium管理的实体也是支持的，由matchName/matchPattern规则给出的DNS信息，会被cilium-agent收集为IP地址。</p>
<pre class="crayon-plain-tag">apiVersion: "cilium.io/v2"
kind: CiliumNetworkPolicy
metadata:
  name: "to-fqdn"
spec:
  endpointSelector:
    matchLabels:
      app: test-app
  egress:
    # 通过DNS Proxy拦截DNS请求，这样，当应用程序发起对my-remote-service.com的DNS查询时
    # Cilium能够学习到域名对应的IP地址
    - toEndpoints:
      - matchLabels:
          "k8s:io.kubernetes.pod.namespace": kube-system
          "k8s:k8s-app": kube-dns
      toPorts:
        - ports:
           - port: "53"
             protocol: ANY
          # DNS代理允许查询的域名
          rules:
            dns:
              - matchPattern: "*"
    - toFQDNs:
        # 将精确匹配此名称的IP地址插入到网路策略中
        - matchName: "my-remote-service.com"
        # 将匹配此模式的所有名称对应的IP地址插入到网络策略中
        # * 匹配所有域名，导致所有缓存的DNS IPs插入到规则
        # *.gmem.cc 匹配子域名，不匹配gmem.cc
        - matchPattern: "*"</pre>
<p>很多情况下，应用程序打开的长连接，生存期大于DNS的TTL，如果没有发生后续、针对此长连接域名的查询，则DNS缓存会过期。这种情况下，已经建立的长连接会继续运行。DNS缓存的TTL可以通过<pre class="crayon-plain-tag">--tofqdns-min-ttl</pre>配置。</p>
<p>相反的，对于短连接场景，可能由于反复的DNS查询（服务backed by大量主机）导致FQDN映射的IP地址很快增加，到达默认 <pre class="crayon-plain-tag">--tofqdns-max-ip-per-hostname=50</pre>的限制，并导致最旧的IP被剔除。这种情况下，已经建立的短连接也不会受到影响，直到它断开。</p>
<div class="blog_h3"><span class="graybg">关于DNS Proxy</span></div>
<p>DNS代理能够拦截DNS请求，记录IP和域名的对应关系。为了实现拦截，必须配置一个管理DNS请求的策略规则。</p>
<p><span style="background-color: #c0c0c0;">某些常用的容器镜像（例如alpine/musl）将DNS的Refused应答（当DNS代理拒绝某个查询时）看作更一般性的错误</span>，并且停止遍历/etc/resolv.conf的search list。例如，当Pod访问gmem.cc时，它会首先查询gmem.cc.svc.cluster.local.而得到DNS Proxy的Refused应答，停止遍历，不再查询gmem.cc.并且最终导致Pod认为DNS查询失败。</p>
<p>要解决此问题，可以配置<pre class="crayon-plain-tag">--tofqdns-dns-reject-response-code</pre>，默认值是refused，可以改为nameError，这样DNS代理会返回NXDomain应答。</p>
<div class="blog_h2"><span class="graybg">L4策略</span></div>
<p>主要是在L3的基础上，进行端口限制。</p>
<div class="blog_h3"><span class="graybg">限制端口</span></div>
<pre class="crayon-plain-tag">apiVersion: "cilium.io/v2"
kind: CiliumNetworkPolicy
metadata:
  name: "l4-rule"
spec:
  endpointSelector:
    matchLabels:
      app: myService
  # 允许myService访问80端口
  egress:
    - toPorts:
      - ports:
        - port: "80"
          protocol: TCP</pre>
<div class="blog_h3"><span class="graybg">标签依赖的端口限制</span></div>
<p>下面的例子，允许针对特定标签（所关联的端点）的端口的访问：</p>
<pre class="crayon-plain-tag">apiVersion: "cilium.io/v2"
kind: CiliumNetworkPolicy
metadata:
  name: "l4-rule"
spec:
  endpointSelector:
    matchLabels:
      role: backend
  # 允许frontend服务访问backend服务的80端口
  ingress:
  - fromEndpoints:
    - matchLabels:
        role: frontend
    toPorts:
    - ports:
      - port: "80"
        protocol: TCP</pre>
<div class="blog_h3"><span class="graybg">CIDR依赖的端口限制</span></div>
<pre class="crayon-plain-tag">apiVersion: "cilium.io/v2"
kind: CiliumNetworkPolicy
metadata:
  name: "cidr-l4-rule"
spec:
  endpointSelector:
    matchLabels:
      role: crawler
  # 允许爬虫访问192.0.2.0/24的80端口
  egress:
  - toCIDR:
    - 192.0.2.0/24
    toPorts:
    - ports:
      - port: "80"
        protocol: TCP</pre>
<div class="blog_h2"><span class="graybg">L7策略</span></div>
<p>目前Cilium支持的L7协议很有限，仅仅HTTP和Kafka（beta）。</p>
<div class="blog_h3"><span class="graybg">HTTP</span></div>
<p>策略可以根据URL路径、HTTP方法、主机名、HTTP头来设置。</p>
<pre class="crayon-plain-tag"># 限定URL路径
apiVersion: "cilium.io/v2"
kind: CiliumNetworkPolicy
metadata:
  name: "rule1"
spec:
  description: "Allow HTTP GET /public from env=prod to app=service"
  endpointSelector:
    matchLabels:
      app: service
  # 允许生产环境访问service的/public
  ingress:
  - fromEndpoints:
    - matchLabels:
        env: prod
    toPorts:
    - ports:
      - port: "80"
        protocol: TCP
      rules:
        http:
        - method: "GET"
          path: "/public"


# 限定URL和请求头
apiVersion: "cilium.io/v2"
kind: CiliumNetworkPolicy
metadata:
  name: "l7-rule"
spec:
  endpointSelector:
    matchLabels:
      app: myService
  ingress:
  - toPorts:
    - ports:
      - port: '80'
        protocol: TCP
      rules:
        http:
        - method: GET
          path: "/path1$"
        - method: PUT
          path: "/path2$"
          headers:
          - 'X-My-Header: true'</pre>
<div class="blog_h3"><span class="graybg">Deny策略</span></div>
<p>用于明确的拒绝特定的流量，<span style="background-color: #c0c0c0;">优先级比Allow策略（CiliumNetworkPolicy/CiliumClusterwideNetworkPolicy/NetworkPolicy）高</span>，上文提及的所有策略都是Allow策略。</p>
<pre class="crayon-plain-tag">apiVersion: "cilium.io/v2"
kind: CiliumClusterwideNetworkPolicy
metadata:
  name: "external-lockdown"
spec:
  endpointSelector: {}
  # 明确禁止外部访问
  ingressDeny:
  - fromEntities:
    - "world"
  ingress:
  - fromEntities:
    - "all"</pre>
<div class="blog_h2"><span class="graybg">CiliumClusterwideNetworkPolicy</span></div>
<p>类似于上面的CiliumNetworkPolicy，区别是：</p>
<ol>
<li>不限定到某个命名空间，集群范围的</li>
<li>支持使用节点选择器</li>
</ol>
<div class="blog_h3"><span class="graybg">主机策略</span></div>
<p>使用节点选择器，可以将策略应用到特定的一个/一组节点。主机策略仅仅应用到宿主机的初始命名空间，包括使用hostnetwork的Pod。</p>
<p>要支持主机策略，需要使用Helm值： <pre class="crayon-plain-tag">--set devices='{interface}'</pre>、<pre class="crayon-plain-tag">--set hostFirewall=true</pre>。</p>
<pre class="crayon-plain-tag">apiVersion: "cilium.io/v2"
kind: CiliumClusterwideNetworkPolicy
metadata:
  name: "lock-down-ingress-worker-node"
spec:
  # 允许据表标签type=ingress-worker的宿主机的所有指定端口的入站流量
  description: "Allow a minimum set of required ports on ingress of worker nodes"
  nodeSelector:
    matchLabels:
      type: ingress-worker
  ingress:
  - fromEntities:
    - remote-node
    - health
  - toPorts:
    - ports:
      - port: "6443"
        protocol: TCP
      - port: "22"
        protocol: TCP
      - port: "2379"
        protocol: TCP
      - port: "4240"
        protocol: TCP
      - port: "8472"
        protocol: UDP
      - port: "REMOVE_ME_AFTER_DOUBLE_CHECKING_PORTS"
        protocol: TCP</pre>
<div class="blog_h2"><span class="graybg">CiliumEndpoint</span></div>
<p>管理K8S中的Pod的过程中，Cilium会自动创建CiliumEndpoint对象，和对应Pod具有相同的namespace+name。</p>
<p>CiliumEndpoint和<pre class="crayon-plain-tag">cilium endpoint get</pre>命令得到的<pre class="crayon-plain-tag">.status</pre>字段有相同的信息：</p>
<pre class="crayon-plain-tag">kubectl get ciliumendpoints.cilium.io  nginx-0 -o jsonpath="{.status}" | jq
{
  // 通信加密设置
  "encryption": {},
  "external-identifiers": {
    "container-id": "eac9972f57187a7afe7bb3edf97c4e70eff8edff26b6923dda8f398d7e622ec9",
    "k8s-namespace": "default",
    "k8s-pod-name": "nginx-0",
    "pod-name": "default/nginx"
  },
  // 端点ID，每个端点都有唯一的ID
  "id": 2318,
  "identity": {
    // 身份标识
    "id": 34796,
    // 具有相同标签的Pod，共享同一身份标识
    "labels": [
      "k8s:app=nginx",
      "k8s:io.cilium.k8s.policy.cluster=default",
      "k8s:io.cilium.k8s.policy.serviceaccount=default",
      "k8s:io.kubernetes.pod.namespace=default"
    ]
  },
  "networking": {
    "addressing": [
      {
        "ipv4": "172.27.2.23"
      }
    ],
    "node": "10.0.3.1"
  },
  "state": "ready"
}

kubectl -n kube-system exec -it cilium-skvr6 -- cilium endpoint get 2318</pre>
<p>每个cilium-agent会创建一个名为<pre class="crayon-plain-tag">cilium-health-&lt;node-name&gt;</pre>的CiliumEndpoint，表示inter-agent健康检查端点。</p>
<div class="blog_h1"><span class="graybg">Istio集成</span></div>
<p>Cilium和Istio都使用Envoy作为七层代理。</p>
<p>集成Cilium和Istio，可以为启用了mTLS的Istio流量提供L7网络策略。如果不进行集成，则可以在Istio Sidecar之外应用应用L7策略，且不能识别mTLS流量。</p>
<div class="blog_h2"><span class="graybg">cilium-istioctl</span></div>
<p>Cilium增强的Istio版本，可以通过cilium-istioctl安装，当前版本1.8.2：</p>
<pre class="crayon-plain-tag">curl -L https://github.com/cilium/istio/releases/download/1.8.2/cilium-istioctl-1.8.2-linux-amd64.tar.gz | tar xz</pre>
<p>运行下面的命令安装Istio：</p>
<pre class="crayon-plain-tag"># 使用默认的Istio配置安装
cilium-istioctl install -y</pre>
<p>启用Istio自动的Envoy Sidecar注入：</p>
<pre class="crayon-plain-tag">kubectl label namespace default istio-injection=enabled</pre>
<p>&nbsp;</p>
<div class="blog_h1"><span class="graybg">网络策略</span></div>
<div class="blog_h2"><span class="graybg">准备</span></div>
<p>我们在default命名空间下，创建以下Pod，用于测试Cilium的功能：</p>
<pre class="crayon-plain-tag">apiVersion: v1
kind: Pod
metadata:
  labels:
    app: nginx
  name: nginx-0
spec:
  containers:
  - args:
    - -g
    - daemon off;
    command:
    - nginx-debug
    image: docker.gmem.cc/library/nginx:1.19.3
    imagePullPolicy: Always
    name: nginx
    ports:
    - containerPort: 80
      protocol: TCP

---

apiVersion: v1
kind: Pod
metadata:
  labels:
    app: nginx
  name: nginx-1
spec:
  containers:
  - args:
    - -g
    - daemon off;
    command:
    - nginx-debug
    image: docker.gmem.cc/library/nginx:1.19.3
    imagePullPolicy: Always
    name: nginx
    ports:
    - containerPort: 80
      protocol: TCP

---

apiVersion: v1
kind: Pod
metadata:
  labels:
    app: alpine
  name: alpine
spec:
  containers:
  - args:
    - -c
    - sleep 365d
    command:
    - /bin/sh
    image: docker.gmem.cc/alpine:3.11
    imagePullPolicy: Always
    name: apline

---

apiVersion: v1
kind: Pod
metadata:
  name: ubuntu
  labels:
    app: ubuntu
spec:
  containers:
  - args:
    - -c
    - sleep 365d
    command:
    - /bin/sh
    image: docker.gmem.cc/ubuntu:16.04
    imagePullPolicy: Always
    name: ubuntu</pre>
<p>当Pod就绪后，查看端点状态：</p>
<pre class="crayon-plain-tag"># 在端点所在节点的cilium-agent中执行cilium endpoint list
# kubectl -n kube-system exec -it cilium-skvr6 -- cilium endpoint list | grep -E 'ubuntu|alpine|nginx'

ENDPOINT   POLICY (ingress)   POLICY (egress)   IDENTITY   LABELS (source:key[=value]) IPv6   IPv4           STATUS   
           ENFORCEMENT        ENFORCEMENT                                                                                                              
888        Disabled           Disabled          5371       k8s:app=alpine                     172.27.2.118   ready   
931        Disabled           Disabled          34796      k8s:app=nginx                      172.27.2.97    ready   
1781       Disabled           Disabled          34796      k8s:app=nginx                      172.27.2.148   ready   
2363       Disabled           Disabled          42034      k8s:app=ubuntu                     172.27.2.162   ready</pre>
<p>可以看到两个Nginx的Pod具有相同的身份标识，这是因为它们的标签一样。由于没有应用任何策略，因此ingress/egress policy为Disabled。</p>
<p>在为两个Nginx端点创建一个服务：</p>
<pre class="crayon-plain-tag">kubectl create service clusterip nginx --tcp=80:80</pre>
<p>确认客户端可以访问服务：</p>
<pre class="crayon-plain-tag">kubectl exec alpine -- curl -s -o /dev/null -I -w "%{http_code}\n" nginx
# 200
kubectl exec ubuntu -- curl -s -o /dev/null -I -w "%{http_code}\n" nginx
# 200</pre>
<div class="blog_h2"><span class="graybg">身份感知和HTTP感知</span></div>
<div class="blog_h3"><span class="graybg">身份感知（L3/L4策略）</span></div>
<p>下面我们增加一个策略，允许app=alpine访问app=nginx：</p>
<pre class="crayon-plain-tag">apiVersion: "cilium.io/v2"
kind: CiliumNetworkPolicy
metadata:
  name: "nginx-ingress"
spec:
  endpointSelector:
    matchLabels:
      app: nginx
  ingress:
  - fromEndpoints:
    - matchLabels:
        app: alpine
    toPorts:
    - ports:
      - port: "80"
        protocol: TCP</pre>
<p>应用上述策略后，通过cilium endpoint list可以看到，两个Nginx的ingress policy为Enabled。</p>
<p>现在，在alpine中还能够访问nginx：</p>
<pre class="crayon-plain-tag">kubectl exec alpine -- curl -s -o /dev/null -I -w "%{http_code}\n" nginx
# 200</pre>
<p>在ubuntu中不能访问：</p>
<pre class="crayon-plain-tag">kubectl exec ubuntu -- curl -s -o /dev/null -I -w "%{http_code}\n" nginx
# 000
# 超时
# command terminated with exit code 7</pre>
<p>这说明策略生效，并且是白名单 —— 如果对某个identity应用了（accept）ingress policy，则只有明确声明的fromEndpoints才具有访问权限。</p>
<div class="blog_h3"><span class="graybg">HTTP感知（L7策略）</span></div>
<p>现在alpine能够访问nginx，假设我们向限制它仅仅能访问/welcome这个URL路径，就需要用到Cilium的L7策略：</p>
<pre class="crayon-plain-tag"># kubectl edit cnp nginx-ingress
apiVersion: "cilium.io/v2"
kind: CiliumNetworkPolicy
metadata:
  name: "nginx-ingress"
spec:
  endpointSelector:
    matchLabels:
      app: nginx
  ingress:
  - fromEndpoints:
    - matchLabels:
        app: alpine
    toPorts:
    - ports:
      - port: "80"
        protocol: TCP
      # 在L4策略的基础上，添加以下内容
      rules:
        http:
        - method: "GET"
          # 支持正则式，例如/welcome/.*
          path: "/welcome"</pre>
<p>现在，alpine访问index.html时会得到403（禁止访问）错误，而GET /welcome则正常访问：</p>
<pre class="crayon-plain-tag">kubectl exec alpine -- curl -s -o /dev/null -I -w "%{http_code}\n" nginx
# 403

kubectl exec alpine -- curl -s -o /dev/null -I -w "%{http_code}\n" -X GET http://nginx/welcome
# 200</pre>
<p>你可以通过下面的命令对流量进行监控：</p>
<pre class="crayon-plain-tag">kubectl -n kube-system exec -it cilium-skvr6 -- cilium monitor -v --type l7

&lt;- Request http from 0 ([k8s:app=alpine k8s:io.cilium.k8s.policy.cluster=default k8s:io.cilium.k8s.policy.serviceaccount=default k8s:io.kubernetes.pod.namespace=default]) 
                to 931 ([k8s:io.cilium.k8s.policy.cluster=default k8s:app=nginx k8s:io.kubernetes.pod.namespace=default k8s:io.cilium.k8s.policy.serviceaccount=default]), 
                identity 5371-&gt;34796, verdict Denied HEAD http://nginx/welcome =&gt; 403

&lt;- Request http from 0 ([k8s:io.cilium.k8s.policy.serviceaccount=default k8s:io.kubernetes.pod.namespace=default k8s:app=alpine k8s:io.cilium.k8s.policy.cluster=default]) 
                to 931 ([k8s:io.cilium.k8s.policy.cluster=default k8s:app=nginx k8s:io.kubernetes.pod.namespace=default k8s:io.cilium.k8s.policy.serviceaccount=default]), 
                identity 5371-&gt;34796, verdict Forwarded GET http://nginx/welcome =&gt; 0
&lt;- Response http to 0 ([k8s:io.kubernetes.pod.namespace=default k8s:app=alpine k8s:io.cilium.k8s.policy.cluster=default k8s:io.cilium.k8s.policy.serviceaccount=default]) 
                from 931 ([k8s:io.cilium.k8s.policy.serviceaccount=default k8s:io.cilium.k8s.policy.cluster=default k8s:app=nginx k8s:io.kubernetes.pod.namespace=default]), 
                identity 5371-&gt;34796, verdict Forwarded GET http://nginx/welcome =&gt; 200</pre>
<div class="blog_h2"><span class="graybg">使用DNS规则</span></div>
<div class="blog_h3"><span class="graybg">锁死外部访问</span></div>
<p>假设我们想仅允许nginx访问docker.gmem.cc，可以使用下面的策略：</p>
<pre class="crayon-plain-tag">apiVersion: "cilium.io/v2"
kind: CiliumNetworkPolicy
metadata:
  name: nginx-egress
spec:
  endpointSelector:
    matchLabels:
      app: nginx
  egress:
  # 两个规则
  # 第一个规则：允许访问域名docker.gmem.cc
  - toFQDNs:
    # 支持使用通配符，例如 *.gmem.cc
    - matchName: "docker.gmem.cc"  
  # 第二个规则：允许访问kube-dns
  - toEndpoints:
    - matchLabels:
        "k8s:io.kubernetes.pod.namespace": kube-system
        "k8s:k8s-app": kube-dns
    toPorts:
    - ports:
      - port: "53"
        protocol: ANY
      # 这个规则提示Cilium检查匹配pattern的DNS查询，并将结果缓存
      rules:
        dns:
        - matchPattern: "*"</pre>
<p>第二个规则的作用在于，允许nginx访问kube-dns服务，进行域名查询。同时，让Cilium的DNS Proxy能够记录nginx执行的所有DNS查询，并且记录域名和IP地址的对应关系。</p>
<p>Cilium缓存的DNS查询结果中的IP地址，才是真正放到BPF Map中的、允许访问的白名单。</p>
<p>应用上述策略后，nginx将无法访问任何集群内部服务，除了kube-dns，除非你配置额外的策略。测试一下效果：</p>
<pre class="crayon-plain-tag">kubectl exec nginx-0 -- curl -s -o /dev/null -I -w "%{http_code}\n" --insecure https://docker.gmem.cc         
# 200
kubectl exec nginx-0 -- curl -s -o /dev/null -I -w "%{http_code}\n" --insecure https://blog.gmem.cc
# 000
# command terminated with exit code 7
kubectl exec nginx-0 -- curl -s -o /dev/null -I -w "%{http_code}\n" --insecure http://nginx
# 000
# command terminated with exit code 7</pre>
<div class="blog_h3"><span class="graybg">联用toPorts </span></div>
<p>可以联合使用toFQDNs和toPorts，以限制访问外部服务使用的端口、通信协议：</p>
<pre class="crayon-plain-tag"># ...
  egress:
  - toFQDNs:
    - matchPattern: "*.gmem.cc" 
    toPorts:
    - ports:
      - port: "443"
        protocol: TCP</pre>
<div class="blog_h2"><span class="graybg">拦截和探查TLS </span></div>
<p>Cilium支持透明的探查TLS加密连接的内容。 基于这个能力，即使是HTTPS流量，Cilium也能做到API感知并应用L7策略。这种能力完全基于软件实现，并且是策略驱动的，仅仅探测策略选中的网络连接。</p>
<p>我们需要以下步骤，以实现TLS拦截/探查：</p>
<ol>
<li>创建一个内部使用的CA，并基于此CA创建办法证书，以实现TLS拦截。端点访问外部TLS服务时，请求被Cilium拦截，并使用此内部CA颁发的证书为端点提供TLS服务</li>
<li>使用Cilium网络策略的DNS规则，选择需要拦截的流量</li>
<li>进行TLS探查，例如：
<ol>
<li>利用cilium monitor来探查HTTP请求的详细内容</li>
<li>使用L7策略过滤/修改HTTP请求</li>
<li>通过Hubble进行观察</li>
</ol>
</li>
</ol>
<div class="blog_h3"><span class="graybg">创建CA和证书</span></div>
<pre class="crayon-plain-tag"># 自签名CA证书
openssl genrsa -des3 -out ca.key 2048
openssl req -x509 -new -nodes -key ca.key -sha256 -days 1825 -out ca.crt

# 生成被探查目标服务，这里是docker.gmem.cc的证书，注意填写正确的Common Name
openssl genrsa -out gmem.cc.key 2048
openssl req -new -key gmem.cc.key -out gmem.cc.csr

# 签名证书
openssl x509 -req -days 360 -in gmem.cc.csr -CA ca.crt -CAkey ca.key -CAcreateserial \
    -out gmem.cc.crt -sha256

# 将证书和密钥写入为secret备用
kubectl create secret tls gmem-tls-data -n kube-system --cert=gmem.cc.crt --key=gmem.cc.key</pre>
<div class="blog_h3"><span class="graybg">将CA加入受信根证书列表</span></div>
<p>上面的自签名CA，需要加到源端点（客户端Pod）的受信任根证书列表：</p>
<pre class="crayon-plain-tag">kubectl cp ca.crt default/ubuntu:/usr/local/share/ca-certificates/ca.crt
kubectl exec ubuntu -- update-ca-certificates</pre>
<p>目标服务的CA证书，则需要写入secret备用。最简单办法是，将系统所有受信任证书的列表，一起写入：</p>
<pre class="crayon-plain-tag">kubectl cp default/ubuntu:/etc/ssl/certs/ca-certificates.crt ca-certificates.crt
kubectl -n kube-system create secret generic tls-orig-data --from-file=ca.crt=./ca-certificates.crt</pre>
<div class="blog_h3"><span class="graybg">创建DNS/TLS感知Egress策略 </span></div>
<pre class="crayon-plain-tag">apiVersion: "cilium.io/v2"
kind: CiliumNetworkPolicy
metadata:
  name: "l7-visibility-tls"
spec:
  endpointSelector:
    matchLabels:
      app: ubuntu
  egress:
  - toFQDNs:
    - matchName: "docker.gmem.cc"
    toPorts:
    - ports:
      - port: "443"
        protocol: "TCP"
      # 第一个TLS连接，也就是Cilium扮演服务端的连接，使用的证书（和密钥）
      terminatingTLS:
        secret:
          namespace: "kube-system"
          name: "gmem-tls-data"
      # 第二个TLS连接，也就是Cilium扮演客户端的连接，使用的受信任证书列表
      originatingTLS:
        secret:
          namespace: "kube-system"
          name: "tls-orig-data"
      # 启用L7策略
      rules:
        http:
        # 允许所有HTTP流量
        - {}
  - toPorts:
    - ports:
      - port: "53"
        protocol: ANY
      rules:
        dns:
          - matchPattern: "*"</pre>
<p>应用上述策略后，尝试从ubuntu访问docker.gmem.cc，然后通过cilium monitor -v --type l7探查发生的流量。</p>
<div class="blog_h2"><span class="graybg">gRPC安全策略</span></div>
<p>gRPC是基于HTTP2协议的，Cilium不支持gRPC的原语，但是gRPC服务/方法是映射到特定URL路径的POST方法的：</p>
<pre class="crayon-plain-tag">apiVersion: "cilium.io/v2"
kind: CiliumNetworkPolicy
metadata:
  name: "rule1"
spec:
  endpointSelector:
    matchLabels:
      app: nginx
  ingress:
  - fromEndpoints:
    - matchLabels:
        app: alpine
    toPorts:
    - ports:
      - port: "80"
        protocol: TCP
      rules:
        http:
        - method: "POST"
          #      gRPC服务          gRPC方法
          path: "/gmem.UserManager/GetName"</pre>
<div class="blog_h2"><span class="graybg">Kafka安全策略 </span></div>
<pre class="crayon-plain-tag">apiVersion: "cilium.io/v2"
kind: CiliumNetworkPolicy
metadata:
  name: "rule2"
spec:
  endpointSelector:
    matchLabels:
      app: kafka
  ingress:
  - fromEndpoints:
    - matchLabels:
        # 允许alpine访问kafka
        app: alpine
    toPorts:
    - ports:
      - port: "9092"
        protocol: TCP
      rules:
        kafka:
        # 允许消费msgs主题
        - role: "consume"
          topic: "msgs"</pre>
<div class="blog_h2"><span class="graybg">Cassanadra安全策略</span></div>
<p>目前Cilium提供了对Apache Cassanadra的Beta支持。</p>
<p>Apache Cassanadra是一种NoSQL数据库，专注于提供高性能的（特别是写）事务能力，同时不以牺牲可用性和可扩容性为代价。Cassanadra以集群方式运行，客户端通过Cassanadra协议与集群通信。</p>
<p>Cilium能理解Cassanadra协议，从而控制客户端可以访问哪些表，可以对表进行哪些操作。</p>
<pre class="crayon-plain-tag">apiVersion: "cilium.io/v2"
kind: CiliumNetworkPolicy
metadata:
  name: "secure-empire-cassandra"
specs:
    endpointSelector:
      matchLabels:
        app: cass-server
    ingress:
    - fromEndpoints:
      - matchLabels:
          app: alpine
      toPorts:
      - ports:
        - port: "9042"
          protocol: TCP
        rules:
          # Cassanadra协议
          l7proto: cassandra
          l7:
            # 允许的操作
          - query_action: "select"
            # 操作针对的表，正则式。指定表需要&lt;keyspace&gt;.&lt;table&gt;形式
            query_table: "system\\..*"
          - query_action: "select"
            query_table: "system_schema\\..*"
          - query_action: "insert"
            query_table: "attendance.daily_records"</pre>
<div class="blog_h2"><span class="graybg">本地重定向策略</span></div>
<p>所谓本地重定向，是指Pod发向IP地址/Service的流量，被重定向到本机Pod的情况。本地重定向策略管理这种流量 —— 它可以将<span style="background-color: #c0c0c0;">匹配策略的流量重定向到本机</span>。</p>
<p>该特性需要4.19+内核。使用选项 <pre class="crayon-plain-tag">--set localRedirectPolicy=true</pre> 开启该特性。</p>
<p>本地重定向策略对应自定义资源<pre class="crayon-plain-tag">CiliumLocalRedirectPolicy</pre>。以下配置字段：</p>
<ol>
<li><pre class="crayon-plain-tag">ServiceMatcher</pre>：用于被重定向的ClusterIP类型的服务</li>
<li><pre class="crayon-plain-tag">AddressMatcher</pre>：用于目的地是IP地址，不属于任何服务的情况</li>
</ol>
<p>当启用本地重定向策略后，非backend Pod访问frontend时，Cilium BPF数据路径会将frontend地址转换为一个本地backend Pod地址。如果流量从backend Pod发往frontend地址，则不会进行进行转换（导致的结果是访问frontend的原始端点），否则就导致循环。Cilium通过调用sk_lookup_助手函数实现这一逻辑。</p>
<div class="blog_h3"><span class="graybg">根据地址匹配</span></div>
<p>下面这个例子，将发往169.254.169.254:8080的TCP流量，重定向到本机的app=proxy端点的80端口：</p>
<pre class="crayon-plain-tag">apiVersion: "cilium.io/v2"
kind: CiliumLocalRedirectPolicy
metadata:
  name: "lrp-addr"
spec:
  redirectFrontend:
    addressMatcher:
      ip: "169.254.169.254"
      toPorts:
        - port: "8080"
          protocol: TCP
  redirectBackend:
    localEndpointSelector:
      matchLabels:
        app: proxy
    toPorts:
      - port: "80"
        protocol: TCP</pre>
<div class="blog_h3"><span class="graybg">根据服务匹配</span></div>
<p>下面的例子，如果访问default/my-service，则重定向到本机的app=proxy端点的80端口：</p>
<pre class="crayon-plain-tag">apiVersion: "cilium.io/v2"
kind: CiliumLocalRedirectPolicy
metadata:
  name: "lrp-svc"
spec:
  redirectFrontend:
    serviceMatcher:
      serviceName: my-service
      namespace: default
  redirectBackend:
    localEndpointSelector:
      matchLabels:
        app: proxy
    toPorts:
      - port: "80"
        protocol: TCP</pre>
<div class="blog_h3"><span class="graybg">限制条件</span></div>
<ol>
<li>策略应用之前，匹配策略的已经存在的连接，不受策略影响</li>
<li>此策略不支持更新，只能删除重建</li>
</ol>
<div class="blog_h3"><span class="graybg">应用场景</span></div>
<p>本地重定向策略的一个应用场景是节点本地DNS缓存。 </p>
<p>节点本地DNS缓存在一个静态的IP地址上监听，配合本地重定向策略，可以拦截来自应用程序Pod的、发往kubed-dns ClusterIP的流量。</p>
<p>策略定义示例：</p>
<pre class="crayon-plain-tag">apiVersion: "cilium.io/v2"
kind: CiliumLocalRedirectPolicy
metadata:
  name: "node-local-dns"
  namespace: kube-system
spec:
  # 如果访问kube-dns
  redirectFrontend:
    serviceMatcher:
      serviceName: kube-dns
      namespace: kube-system
  redirectBackend:
    # 那么重定向到Node local DNS
    localEndpointSelector:
      matchLabels:
        k8s-app: node-local-dns
    # TCP和UDP都支持
    toPorts:
      - port: "53"
        name: dns
        protocol: UDP
      - port: "53"
        name: dns-tcp
        protocol: TCP</pre>
<div class="blog_h1"><span class="graybg">网络连接</span></div>
<div class="blog_h2"><span class="graybg">路由</span></div>
<div class="blog_h3"><span class="graybg">Encapsulation</span></div>
<p>如果不提供任何配置，Cilium自动运行在overlay（encapsulation）模式下，这种模式对底层网络的要求最小。overlay模式下所有节点组成基于UDP封装的网格。支持的封装协议包括：</p>
<ol>
<li>VxLAN：默认封装模式，占用8472/UDP端口</li>
<li>Geneve：占用6081/UDP端口</li>
</ol>
<p>所有Cilium节点之间的流量都被封装。</p>
<p>overlay模式的优点：</p>
<ol>
<li>简单：集群节点所在的网络，不需要对PodCIDR有任何感知。只要底层网络支持IP/UDP，即可构建出overlay网络</li>
<li>地址空间：由于不依赖底层网络，因而可以使用很大的IP地址范围，支持很大规模的Pod数量</li>
<li>自动配置：在编排系统中，每个节点可以被分配一个IP前缀，并独立进行IPAM</li>
<li>身份标识上下文：利用封装协议，可以为网络封包附带元数据。Cilium利用这种能力，来传输源节点的安全标识信息，让目标节点不必查询封包所属的实体</li>
</ol>
<p>overlay模式的缺点：</p>
<ol>
<li>MTU overhead：由于额外的封装头，导致有效MTU比native-routing小。对于VxLAN每个封包的有效MTU减少50字节。这会导致单个特定网络连接的最大吞吐率减小。使用Jumbo frames则实际影响大大减小</li>
</ol>
<div class="blog_h3"><span class="graybg">Native-Routing</span></div>
<p>配置<pre class="crayon-plain-tag">tunnel: disabled</pre>可以启用此datapath，这种模式下，目的地不是本机的封包，被委托给Linux的路由子系统处理。这要求连接节点的网络能够正确处理路由：</p>
<ol>
<li>要么所有节点直接位于L2网络中，可以配置<pre class="crayon-plain-tag">auto-direct-node-routes: true</pre></li>
<li>要么连接它们的路由器能够处理路由：
<ol>
<li>在云环境下，VPC需要和Cilium进行集成，以获得路由信息。目前主流云厂商已经支持</li>
<li>在支持BGP的路由器的配合下，基于BGP协议分发路由。可以通过kube-router来运行BGP守护程序</li>
</ol>
</li>
</ol>
<p>配置<pre class="crayon-plain-tag">native-routing-cidr: x.x.x.x/y</pre>指定可以进行native-routing的CIDR。</p>
<div class="blog_h2"><span class="graybg">IPAM</span></div>
<p>IPAM负责分配和管理网络端点（容器或其它）的IP地址。Cilium支持多种IPAM模式。</p>
<div class="blog_h3"><span class="graybg">kubernetes</span></div>
<p>使用Kubernetes自带的host-scope IPAM。地址分配委托给每个节点进行，per-node的Pod CIDR存放在v1.Node中。</p>
<div class="blog_h3"><span class="graybg">cluster-pool</span></div>
<p>这是默认的IPAM mode，它分配per-node的Pod CIDR，并在每个节点上使用host-scope的分配器来分配IP地址。</p>
<p>此模式和kubernetes类似，区别在于后者在v1.Node资源中存储per-node的Pod CIDR，而Cilium在<pre class="crayon-plain-tag">v2.CiliumNode</pre>中存储此信息。</p>
<p>此模式下，cilium-agent在启动时会等待v2.CiliumNode中的<pre class="crayon-plain-tag">Spec.IPAM.PodCIDRs</pre>字段可用。</p>
<p>通过Helm安装时，使用下面的值来启用此模式：</p>
<pre class="crayon-plain-tag">helm install ...
  --set ipam.mode=cluster-pool

  --set ipam.operator.clusterPoolIPv4PodCIDR=&lt;IPv4CIDR&gt;
  # 调整每个节点的CIDR规模
  --set ipam.operator.clusterPoolIPv4MaskSize=&lt;IPv4MaskSize&gt;

  --set ipam.operator.clusterPoolIPv6PodCIDR=&lt;IPv6CIDR&gt;
  --set ipam.operator.clusterPoolIPv6MaskSize=&lt;IPv6MaskSize&gt;</pre>
<p>在运行时，使用下面的命令查询IP分配错误：</p>
<pre class="crayon-plain-tag">kubectl get ciliumnodes -o jsonpath='{range .items[*]}{.metadata.name}{"\t"}{.status.operator.error}{"\n"}{end}'</pre>
<p>使用下面的命令查看IP分配情况：</p>
<pre class="crayon-plain-tag">cilium status --all-addresses</pre>
<div class="blog_h3"><span class="graybg">crd</span></div>
<p><a href="https://blog.gmem.cc/wp-content/uploads/2020/06/crd_arch.png"><img class="size-full wp-image-37601 aligncenter" src="https://blog.gmem.cc/wp-content/uploads/2020/06/crd_arch.png" alt="crd_arch" width="787" height="250" /></a></p>
<p>此模式下，cilium-agent会监听当前节点同名的v2.CiliumNode资源，每当CiliumNode被更新，cilium-agent会利用列在<pre class="crayon-plain-tag">spec.ipam.available</pre>的IP地址来更新本节点的IP池。如果<span style="background-color: #c0c0c0;">已经分配的IP地址从spec.ipam.available中移除</span>，仍然可以正常使用，但是释放后不能重新分配。</p>
<p>当IP被分配出去之后，会记录到<pre class="crayon-plain-tag">status.ipam.inuse</pre>字段。</p>
<p>你需要开发一个Operator，将IP地址分配给特定节点，此模式提供了很大的灵活性。</p>
<div class="blog_h2"><span class="graybg">IP遮掩</span></div>
<p>对于IPv4，容器访问外部流量时Cilium会自动进行SNAT，替换源地址为节点的IP地址。对于IPv6，IP遮掩仅在iptables模式下被支持。</p>
<p>使用选项<pre class="crayon-plain-tag">enable-ipv4-masquerade: false</pre>和<pre class="crayon-plain-tag">enable-ipv6-masquerade: false</pre>可以改变上述默认行为。</p>
<p>如果Pod IP在节点网络中可以路由，可以配置<pre class="crayon-plain-tag">native-routing-cidr</pre>，如果<span style="background-color: #c0c0c0;">目的地址</span>在此CIDR中，则不进行IP遮掩。</p>
<p>Cilium支持多种IP遮掩的实现模式。</p>
<div class="blog_h3"><span class="graybg">ebpf</span></div>
<p>最高效的实现，要求内核版本4.19+，默认启用。对应Helm值<pre class="crayon-plain-tag">bpf.masquerade=true</pre>。当前版本此特性依赖<a href="https://docs.cilium.io/en/v1.10/gettingstarted/kubeproxy-free/#kubeproxy-free">BPF NodePort</a>特性。</p>
<p>基于eBPF的IP遮掩，只能发生在挂钩了eBPF masquerading程序的节点出口设备上。哪些出口设备进行挂钩，可以通过Helm值<pre class="crayon-plain-tag">devices</pre>指定，如果不指定则基于<a href="https://docs.cilium.io/en/v1.10/gettingstarted/kubeproxy-free/#nodeport-devices">BPF NodePort device detection metchanism</a>自动选择。</p>
<p>使用cilium status命令可以检查哪些设备挂钩了：</p>
<pre class="crayon-plain-tag">kubectl exec -it -n kube-system cilium-xxxxx -- cilium status | grep Masquerading
#                                       已挂钩设备     不遮掩的CIDR
# Masquerading:   BPF (ip-masq-agent)   [eth0, eth1]  10.0.0.0/16</pre>
<p>该模式支持TCP/UDP/ICMP这三类IPv4的L4协议，其中ICMP仅仅支持Echo请求/应答。</p>
<p>除了配置native-routing-cidr，你还可以配置Helm值<pre class="crayon-plain-tag">ipMasqAgent.enabled=true</pre>，更细粒度的控制，访问哪些目的IP时不需要进行遮掩。这个能力是依靠Cilium开发的eBPF版本的<a href="https://github.com/kubernetes-sigs/ip-masq-agent">ip-masq-agent</a>来实现的。</p>
<div class="blog_h3"><span class="graybg">iptables</span></div>
<p>遗留模式，支持在所有版本的内核上运行。</p>
<div class="blog_h2"><span class="graybg">IP分片处理</span></div>
<p>默认情况下，Cilium配置eBPF数据路径，进行IP分片跟踪，以允许不支持分段的协议能透明的通过网络传输大报文。</p>
<p>IP分片跟踪在eBPF中通过LRU Map实现，要求4.10+内核。该特性通过以下选项启用：</p>
<ol>
<li><pre class="crayon-plain-tag">enable-ipv4-fragment-tracking</pre>：启用或禁用IPv4分片跟踪，默认启用</li>
<li><pre class="crayon-plain-tag">bpf-fragments-map-max</pre>：控制使用IP分配的活动并发连接的数量</li>
</ol>
<p>UDP这样的协议，它没有TCP那种分段和重组的能力，大报文只能依赖于IP层的分片机制。由于IP分片缺乏重传机制，因此大UDP报文一旦丢失一个片段，就需要整个报文的重传。</p>
<div class="blog_h2"><span class="graybg">BGP</span></div>
<div class="blog_h3"><span class="graybg">集成BIRD</span></div>
<p>BIRD是一个开源软件，支持BGP协议。利用BIRD可以将Cilium管理的端点暴露到集群外部。</p>
<p>通过下面的命令安装bird2：</p>
<pre class="crayon-plain-tag"># Ubuntu
sudo apt install bird2
# CentOS
yum install -y bird2

sudo systemctl enable bird
sudo systemctl restart bird</pre>
<p>节点配置文件示例：</p>
<pre class="crayon-plain-tag"># 其它节点只是ID不同
router id 10.0.3.1;

debug protocols all;

# 如果使用直接路由模式
filter cnionly {
    if net ~ 172.27.0.0/16 &amp;&amp; ifname != "cilium_host" then accept;
    else reject;
}

protocol kernel {
    learn;
    scan time 10;
    ipv4 {
        import none;            # 如果使用隧道模式
        import filter cnionly;  # 如果使用直接路由模式
        export none;
    };
}

protocol device {
    scan time 5;
}

# 直接添加到BIRD的路由表
protocol static {
    ipv4;
    # 宣告Pod CIDR
    route 172.27.0.0/16 via "cilium_host";  # 如果使用隧道模式
    # 宣告ClusterIP CIDR。不能和kube-proxy replacement联用，因为后者不允许集群外访问ClusterIP
    route 10.96.0.0/24  via "eth0";
    # 宣告LoadBalancer CIRD
    route 10.0.10.0/24 via "eth0";
}

# 连接到上游路由器，并宣告上面的静态路由
protocol bgp k8s {
    local as 65000;
    neighbor 10.0.0.1 as 65000;
    direct;
    ipv4 {
        export all;
    };
}
# 查看路由     birdc show route
# 查看BGP状态  birdc show protocols all k8s</pre>
<p>上游路由（反射器）配置示例：</p>
<pre class="crayon-plain-tag">log syslog all;

router id 10.0.0.1;

debug protocols all;

protocol kernel {
    scan time 10;
    ipv4 {
        import none;
        export all;
    };
}

protocol device {
    scan time 5;
}

protocol bgp k8s {
    local as 65000;
    neighbor range 10.0.3.0/24 as 65000;
    direct;
    rr client;
    ipv4 {
        import all;
        export all;
    };
}</pre>
<p>可以启用双向转发检测（Bidirectional Forwarding Detection，BFD），以加入路径故障检测（path failure detection）。BFD由一系列几乎独立的BFD会话组成，每个会话在<span style="background-color: #c0c0c0;">双方都启用了</span>BFD的路由器之间进行双向单播路径的监控。监控方式是周期性的、双向发送控制封包。</p>
<p>BFD不会进行邻居发现，BFD会话是按需（例如被BGP协议请求）创建的。</p>
<pre class="crayon-plain-tag">protocol bfd {
    interface "eth*" {
        min rx interval 100 ms;
        min tx interval 100 ms;
        idle tx interval 300 ms;
        multiplier 10;
    };
    # 不需要按需创建，直接初始化和这些邻居的BFD会话
    neighbor 10.0.3.2;
    neighbor 10.0.3.3;
}

protocol bgp k8s {
    # BGP支持使用BFD来发现邻居是否存活
    bfd on;
}</pre>
<p>下面的命令查看BFD会话状态：</p>
<pre class="crayon-plain-tag">birdc show bfd sessions

bfd1:
IP address                Interface  State      Since         Interval  Timeout
10.0.3.2                  virbr0     Up         11:56:01.055    0.100    1.000
10.0.3.1                  virbr0     Up         11:56:00.094    0.100    1.000
10.0.3.3                  virbr0     Up         11:56:00.389    0.100    1.000</pre>
<p>为了某些特殊目的，例如L4负载均衡，你需要在多个节点上配置Pod CIDR的静态路由，并且在Bird中配置<a href="/network-faq#ecmp">ECMP（Equal-cost multi-path）路由</a>。</p>
<pre class="crayon-plain-tag">protocol kernel {
    merge paths yes limit 3;
}</pre>
<div class="blog_h3"><span class="graybg">宣告LoadBalancerIP</span></div>
<p>Cilium可以原生支持，将LoadBalancer服务分配IP地址、并通过BGP协议将地址宣告出去。是否宣告LoadBalancer服务的IP，取决于服务的externalTrafficPolicy设置。</p>
<p>使用下面的Helm值启用该特性：<pre class="crayon-plain-tag">--set bgp.enabled=true --set bgp.announce.loadbalancerIP=true</pre>。该特性依赖于MetalLB。</p>
<p>添加bgp-config这个ConfigMap，参考：</p>
<pre class="crayon-plain-tag">apiVersion: v1
kind: ConfigMap
metadata:
  name: bgp-config
  namespace: kube-system
data:
  config.yaml: |
    peers:
      - peer-address: 10.0.0.1
        peer-asn: 65000
        my-asn: 65000
    address-pools:
      - name: default
        protocol: bgp
        addresses:
          - 10.0.10.0/24 </pre>
<div class="blog_h2"><span class="graybg">IPVLAN模式</span></div>
<div class="blog_h3"><span class="graybg">VETH vs IPVLAN<br /></span></div>
<p>容器通常都使用虚拟设备，例如veth对，作为连接初始命名空间的桥梁。通过在宿主机端veth挂钩tc ingress钩子，Cilium能够监控容器的任何流量。</p>
<p>veth对处理流量时，需要两次通过网络栈，相比起ipvlan有性能上的劣势。对于两个在同一节点上的容器veth端点，一个封包需要4次通过网络栈。</p>
<p>Cilium CNI也支持L3/L3S的ipvlan，这种模式下，宿主机物理设备作为ipvlan master，而容器端的ipvlan虚拟设备是slave。使用ipvlan时<span style="background-color: #c0c0c0;">将封包从其它网络命名空间推入ipvlan slave设备时消耗更少的资源，因而可能改善网络延迟</span>。使用ipvlan时Cilium在容器命名空间中挂钩BPF程序到ipvlan slave设备的egress钩子，以便应用L3/L4策略（因为初始命名空间下所有容器共享单个设备）。同时挂钩到ipvlan master的tc ingress钩子，可以对节点的所有入站流量应用网络策略。</p>
<p>为了支持老版本的不支持ipvlan hairpin模式的内核，Cilium在ipvlan slave设备（位于容器网络命名空间）的tc gress上挂钩了BPF程序。</p>
<p>当前版本的ipvlan支持有以下限制：</p>
<ol>
<li>NAT64不被支持</li>
<li>基于Envoy的L7 Policy不被支持</li>
<li>容器到host-local的通信不被支持</li>
<li>Service不支持LB到本地端点</li>
</ol>
<div class="blog_h3"><span class="graybg">启用IPVLAN</span></div>
<p>Cilium默认使用veth提供容器网络连接。你可以选用Beta支持的IPVLAN，目前尚未提供的特性包括：</p>
<ol>
<li>IPVLAN L2模式</li>
<li>L7策略支持</li>
<li>FQDN策略支持</li>
<li>NAT64</li>
<li>IPVLAN+隧道</li>
<li>基于eBPF的IP遮掩</li>
</ol>
<p>这些特性将在未来版本提供。</p>
<p>由于使用IPVLAN L3模式，需要4.12+的内核。如果使用L3S模式（流量经过宿主机网络栈因而被netfilter处理），这需要修复<a href="https://git.kernel.org/pub/scm/linux/kernel/git/netdev/net.git/commit/?id=d5256083f62e2720f75bb3c5a928a0afe47d6bc3">d5256083f62e</a>（4.19.20）的稳定版内核。</p>
<p>参考下面的方式进行安装：</p>
<pre class="crayon-plain-tag">helm install cilium cilium/cilium --version 1.10.1 \
  --namespace kube-system \
  # 启用IPVLAN
  --set datapathMode=ipvlan \
  # 选择IPVLAN主设备，要求所有节点主设备名字相同
  --set ipvlan.masterDevice=eth0 \
  # IPVLAN数据路径目前仅支持直接路由，因此必须禁用tunnel
  --set tunnel=disabled  \
  # 要让IPVLAN跨节点工作，每个主机都必须安装正确的路由
  # 路由要么手工设置，要么由Cilium自动安装。对于后者，设置：
  --set autoDirectNodeRoutes="true" \
  # 下面的选项，用于控制是否安装iptables规则，这些规则主要用于和kube-proxy交互
  # 如果设置为false，则不安装，并且IPVLAN工作在L3模式
  # 默认值为true，IPVLAN工作在L3S模式，初始命名空间的netfilter会对容器封包进行过滤
  --set installIptablesRules="true"  \
  # 对所有离开IPVLAN master设备的流量进行IP遮掩
  --set masquerade="true"</pre>
<p>IPVLAN L3模式中宿主机的netfilters钩子被绕过，因此无法进行IP遮掩，必须使用L3S模式（会降低性能）。</p>
<div class="blog_h2"><span class="graybg">透明加密（IPsec）</span></div>
<p>Cilium支持使用IPsec/WireGuard透明的加密：</p>
<ol>
<li>Cilium管理的宿主机之间</li>
<li>Cilium管理的端点之间</li>
</ol>
<p>的流量。</p>
<p>为了确定某个连接是否可以被加密，Cilium需要明确封包目的地址是否是受管理的端点。在明确之前，流量可能不被加密。</p>
<p>同一主机内部的流量不会被加密。</p>
<p>如果在其它CNI插件之上链接Cilium，则目前无法支持透明加密特性。</p>
<div class="blog_h3"><span class="graybg">生成和导入PSK</span></div>
<pre class="crayon-plain-tag">kubectl create -n kube-system secret generic cilium-ipsec-keys \
    --from-literal=keys="3 rfc4106(gcm(aes)) $(echo $(dd if=/dev/urandom count=20 bs=1 2&gt; /dev/null | xxd -p -c 64)) 128"</pre>
<div class="blog_h3"><span class="graybg">启用加密</span></div>
<pre class="crayon-plain-tag">helm install cilium cilium/cilium --version 1.10.1 \
  --namespace kube-system \
  # 启用Pod之间流量的加密
  --set encryption.enabled=true \
  # 启用节点流量加密（Beta）
  --set encryption.nodeEncryption=false \
  # 算法，默认ipsec
  --set encryption.type=ipsec    \
  # 如果启用直接路由（不使用隧道），则不指定下面选项的时候，会查询路由表，选择默认路由对应的网络接口
  --set encryption.ipsec.interface=ethX</pre>
<div class="blog_h2"><span class="graybg">宿主机可达服务</span></div>
<p>通过本节的配置，可以让服务从初始命名空间无需NAT的访问。</p>
<p>此特性要求4.19.57, 5.1.16, 5.2.0等版本以上的内核。如果仅要支持TCP（不支持UDP）则需要4.17.0。</p>
<pre class="crayon-plain-tag">helm install cilium cilium/cilium --version 1.10.1 \
  --namespace kube-system \
  # 启用此特性
  --set hostServices.enabled=true \
  # 仅仅支持TCP
  --set hostServices.protocols=tcp</pre>
<p>此特性的工作原理：在connect系统调用（TCP, connected UDP），或者 sendmsg/recvmsg系统调用（UDP）时，Cilium会检查目的地址，如果它是一个Service IP，则<span style="background-color: #c0c0c0;">直接将目的地址更换为一个后端的地址</span>。这样，套接字实际上会直接连接真实后端，<span style="background-color: #c0c0c0;">不会在更低层次的数据路径上发生NAT</span>，也就是对数据路径的更低层次透明。</p>
<p>宿主机可达服务，允许从宿主机/Pod中，以多种IP:NODE_PORT访问到NodePort服务。这些IP包括：环回地址、服务ClusterIP、节点本地接口（除了docker*）地址。</p>
<div class="blog_h2"><span class="graybg">kube-proxy replacement<br /></span></div>
<p>Cilium能够完全代替kube-proxy。此特性依赖“宿主机可达服务”，因此对内核有着相同的要求。Cilium还利用5.3/5.8添加的额外特性，进行了更进一步的优化。</p>
<div class="blog_h3"><span class="graybg">移除kube-proxy</span></div>
<p>基于kubeadm安装K8S时，可以用下面的命令跳过kube-proxy：</p>
<pre class="crayon-plain-tag">kubeadm init --skip-phases=addon/kube-proxy</pre>
<p>需要注意：如果节点有多网卡，确保kubelet的<pre class="crayon-plain-tag">--node-ip</pre>设置正确，否则Cilium可能无法正常工作。</p>
<p>如果集群已经安装了kube-proxy，可以使用下面的命令移除：</p>
<pre class="crayon-plain-tag">kubectl -n kube-system delete ds kube-proxy
# 删除cm，可以防止升级K8S（1.19+）时候重新安装kube-proxy
kubectl -n kube-system delete cm kube-proxy</pre>
<div class="blog_h3"><span class="graybg">启用kube-proxy replacement<br /></span></div>
<pre class="crayon-plain-tag">helm install cilium cilium/cilium --version 1.10.1 \
    --namespace kube-system \
    # 代替kube-proxy，取值：
    #   strict，如果内核不支持，则导致cilium-agent退出
    #   probe，探测内核特性，自动禁用不支持的特性子集。该取值假设kube-proxy不被删除，作为可能的fallback
    --set kubeProxyReplacement=strict \
    # 替换为API Server的地址和端口
    --set k8sServiceHost=10.0.3.1 \
    --set k8sServicePort=6443</pre>
<p>使用如上命令安装的Cilium，可以作为ClisterIP、NodePort、LoadBalancer，以及具有externalIP的服务的控制器。在此之上，eBPF kube-proxy replacement<span style="background-color: #c0c0c0;">还能够支持容器的hostPort，从而不再需要portmap</span>。</p>
<p>kube-proxy replacement同时支持直接路由和隧道模式。</p>
<p>使用下面的命令可以验证kube-proxy replacement已经正常安装：</p>
<pre class="crayon-plain-tag"># kubectl exec -it -n kube-system cilium-ch5qk --  cilium status --verbose 
# ...
KubeProxyReplacement:   Strict   [eth0 10.0.3.2 (Direct Routing)]
# ...
KubeProxyReplacement Details:
  Status:                Strict
  Socket LB Protocols:   TCP, UDP
  Devices:               eth0 10.0.3.2 (Direct Routing)
  Mode:                  SNAT
  Backend Selection:     Random
  Session Affinity:      Enabled
  XDP Acceleration:      Disabled
  Services:
  - ClusterIP:      Enabled
  - NodePort:       Enabled (Range: 30000-32767) 
  - LoadBalancer:   Enabled 
  - externalIPs:    Enabled 
  - HostPort:       Enabled</pre>
<div class="blog_h3"><span class="graybg">磁悬浮一致性哈希</span></div>
<p>kube-proxy replacement支持<a href="https://storage.googleapis.com/pub-tools-public-publication-data/pdf/44824.pdf">磁悬浮（Maglev）一致性哈希算法</a>的变体，作为负载均衡算法。一致性哈希是一类算法，它将后端（RS）计算哈希值后，分布在一个环上。进行负载均衡时，对5元组计算哈希，然后看落在环上哪两个RS之间，取哈希值较小的RS作为LB目标。磁悬浮算法通过将每个RS在环上映射多次，减少当RS数量增加/减少时，必须映射到其它RS的五元组的数量。自然，减少一个RS之后，原先映射到其上的5元组必然需要重新映射，磁悬浮的目标是尽量减少除此之外的重新映射</p>
<p>该算法增强了故障时的弹性。新增节点后，在不需要和其它节点同步的前提下，能够对任意指定5元组能够保持相同的、一致性的后端选择；移除节点后，除了那些后端对应被移除节点的5元组，不超过1% difference in reassignments</p>
<p>通过<pre class="crayon-plain-tag">--set loadBalancer.algorithm=maglev</pre>启用</p>
<p>需要注意，<span style="background-color: #c0c0c0;">该LB算法仅用于外部（南北向）流量</span>。对于集群内部（东西向）流量，套接字直接分配到服务的后端，也就是说在TCP connect的时候，目的地址被修改为后端，不会使用该算法</p>
<p>Cilium XDP加速支持磁悬浮一致性哈希算法。</p>
<p>该算法有两个专用的配置项：</p>
<ol>
<li><pre class="crayon-plain-tag">maglev.tableSize</pre>：每单个服务的Maglev查找表的大小。理想值M，应当大大大于期望后端数N的质数。最好大于100*N，以确保当后端发生变化时最多1% difference in reassignments。支持的取值包括251 509 1021 2039 4093 8191 16381 32749 65521 131071。取值16381用于大概160个后端的服务</li>
<li><pre class="crayon-plain-tag">maglev.hashSeed</pre>：用于避免受限于Cilium内置的固定的seed，seed是base64编码的16byte随机数。所有节点必须具有相同的seed</li>
</ol>
<div class="blog_h3"><span class="graybg">保留客户端源IP</span></div>
<p>Cilium基于eBPF的kube-proxy replacement实现了保留客户端源IP的能力。</p>
<p>Service的<pre class="crayon-plain-tag">externalTrafficPolicy</pre>选项决定Cilium的行为：</p>
<ol>
<li>Local：集群内的服务可以相互访问，也可以从没有该服务后端的节点上访问。集群内的端点，不需要SNAT就能实现访问服务时的负载均衡</li>
<li>Cluster：默认值。有多种途径保留客户端源IP。如果仅TCP服务需要暴露到集群外部，可以让kube-proxy replacement运行在DSR/Hybrid模式</li>
</ol>
<div class="blog_h3"><span class="graybg">直接服务器返回（DSR）</span></div>
<p>默认情况下，Cilium的eBPF NodePort实现，在SNAT模式下运作。也就是说，当来自外部的、访问集群服务的流量到达时，如果<span style="background-color: #c0c0c0;">入群节点判断出服务</span>（LoadBalancer/NodePort/其它具有ExternalIP的服务）<span style="background-color: #c0c0c0;">的后端位于其它节点</span>，<span style="background-color: #c0c0c0;">它就需要将请求重定向到远程节点。这个重定向时需要SNAT，将外部流量的源地址换成入群节点的地址</span></p>
<p>这个<span style="background-color: #c0c0c0;">SNAT的代价是，访问链路多了一跳，同时丢失了源IP信息</span>。为了进行reverse SNAT，返回报文还必须经过入群节点，然后传回给外部客户端</p>
<p>设置<pre class="crayon-plain-tag">loadBalancer.mode=dsr</pre>，可以让Cilium的eBPF NodePort实现切换到DSR模式。这种模式下，后端直接应答外部客户端，不经过入群节点。<span style="background-color: #c0c0c0;">这一特性必须和Direct Routing一起使用</span>，也就是不能使用隧道。</p>
<p>DSR模式的另外一个优势是，源IP地址被保留，因此，运行<span style="background-color: #c0c0c0;">在服务后端节点上的Cilium策略，可以正确的根据依据源IP进行过滤</span>。</p>
<p>由于一个后端可能被多个Service引用，<span style="background-color: #c0c0c0;">后端（所在节点的cilium-agent）需要知道生成（直接回复给原始客户端的）应答报文时，使用什么Service IP/Port（作为源地址）</span>。Cilium的解决办法是，<span style="background-color: #c0c0c0;">使用IPv4选项</span>或IPv6 Destination选项扩展，<span style="background-color: #c0c0c0;">将Service IP/Port信息编码到IP头中</span>，代价是MTU变小。对于TCP服务，<span style="background-color: #c0c0c0;">仅仅SYN封包需要编码Service IP/Port信息，因此MTU变小不会有影响</span>。</p>
<p>需要注意，在某些公有云环境下，DSR模式可能无法工作。原因可能是：</p>
<ol>
<li>底层Fabric<span style="background-color: #c0c0c0;">可能丢弃掉Cilium的IP选项</span></li>
<li>某些云实现了源/目的地址检查，你需要禁用此特性DSR才能正常工作</li>
</ol>
<p>为了避免UDP的MTU变小问题，可以设置<pre class="crayon-plain-tag">loadBalancer.mode=hybrid</pre>，这样对于UDP协议，会工作在SNAT模式，对于TCP则工作在DSR模式</p>
<div class="blog_h3"><span class="graybg">NodePort XDP加速</span></div>
<p>对于LoadBalancer/NodePort/其它具有ExternalIP的服务，如果外部流量入群节点上没有服务后端，则入群节点需要将请求转发给其它节点。Cilium 1.8+支持基于XDP进行加速这一转发行为。XDP工作在驱动层，大部分支持10G+bps的驱动都支持native XDP。云上环境中大多数具有SR-IOV变体的驱动也支持native XDP。在裸金属环境下，XDP加速可以和MetalLB这样的LoadBalancer控制器联用</p>
<p>要启用XDP加速，需要设置<pre class="crayon-plain-tag">loadBalancer.acceleration=native</pre>，默认值<pre class="crayon-plain-tag">disabled</pre>。对于大规模环境，可以考虑调优Map的容量：<pre class="crayon-plain-tag">config.bpfMapDynamicSizeRatio</pre></p>
<p>XDP加速可以和loadBalancer.mode：DSR/SNAT/hybrid一起使用。</p>
<div class="blog_h3"><span class="graybg">NodePort设备/端口/绑定设置</span></div>
<p>启用Cilium的eBPF kube-proxy replacement时，默认情况下，LoadBalancer/NodePort/其它具有ExternalIP的服务，可以通过这样的网络接口访问：</p>
<ol>
<li>具有默认路由的接口</li>
<li>被分配的K8S节点的InternalIP / ExternalIP的接口</li>
</ol>
<p>要改变设备，可以配置devices选项，例如<pre class="crayon-plain-tag">devices='{eth0,eth1,eth2}'</pre>。需要注意每个节点的名字必须一致，如果不一致，可以考虑用通配符<pre class="crayon-plain-tag">devices=eth+</pre></p>
<p>如果使用多个网络接口，仅其中单个可用于Cilium节点之间的直接路由。Cilium会选择具有InternalIP / ExternalIP的接口，InternalIP优先。你也可以手工指定直接路由设备<pre class="crayon-plain-tag">nodePort.directRoutingDevice=eth1</pre>，如果该选项中的设备，不在<pre class="crayon-plain-tag">devices</pre>中，Cilium会自动加入</p>
<p>直接路由设备也用于NodePort XDP加速，也就是说该设备的驱动应该支持native XDP</p>
<p>如果kube-apiserver使用了非默认的NodePort范围，则相同的配置必须传递给Cilium，例如<pre class="crayon-plain-tag">nodePort.range="10000\,32767"</pre></p>
<p>如果NodePort返回和内核临时端口范围（net.ipv4.ip_local_port_range）重叠，则Cilium会将NodePort范围附加到保留端口范围（net.ipv4.ip_local_reserved_ports）。这可以避免NodePort服务劫持宿主机本地应用程序发起的（源端口在和NodePort冲突的）连接。要禁用这种端口范围保护的行为，设置<pre class="crayon-plain-tag">nodePort.autoProtectPortRanges=false</pre></p>
<p>默认情况下，NodePort实现禁止应用程序对NodePort服务端口的bind系统调用，应用程序会接收到bind: Operation not permitted 错误。对于5.7+内核，在Pod内部bind不会报此错误。如果需要完全允许（包括老版本内核、5.7+在初始命名空间）bind，可以设置<pre class="crayon-plain-tag">nodePort.bindProtection=false</pre></p>
<div class="blog_h3"><span class="graybg">容器HostPort支持</span></div>
<p>尽管不是kube-proxy的一部分，Cilium的eBPF kube-proxy replacement也原生实现了hostPort，因此不需要使用CNI chaining：<pre class="crayon-plain-tag">cni.chainingMode=portmap</pre></p>
<p>如果启用了eBPF kube-proxy replacement，hostPort就自动支持，不需要额外配置。其它情况下，可以使用<pre class="crayon-plain-tag">hostPort.enabled=true</pre>启用此特性</p>
<p>如果指定hostPort时没有额外指定hostIP，则Pod的端口将通过宿主机用于暴露NodePort服务的那些IP地址，对外暴露出去。包括K8S的InternalIP/ExternalIP、环回地址。如果指定了hostIP则仅仅从该IP暴露，hostIP指定为0.0.0.0效果等于未指定。</p>
<div class="blog_h3"><span class="graybg">kube-proxy混合模式</span></div>
<p>除了完全代替kube-proxy，Cilium的eBPF kube-proxy replacement还可以与自共存，成为混合模式。混合模式的目的是解决某些内核版本不足以实现完全的kube-proxy replacement的问题。</p>
<p>kubeProxyReplacement取值：</p>
<ol>
<li>strict：严格完全替代或者失败</li>
<li>probe：混合模式。自动探测内核，并尽量替代</li>
<li>partial：混合模式。手工指定需要启用哪些eBPF kube-proxy replacement组件。取该值时必须设置<pre class="crayon-plain-tag">enableHealthCheckNodeport=false</pre>，以确保cilium-agent不会启动NodePort健康检查服务器。可以手工开启的特性如下，默认全部false：
<ol>
<li><pre class="crayon-plain-tag">hostServices.enabled</pre></li>
<li><pre class="crayon-plain-tag">nodePort.enabled</pre></li>
<li><pre class="crayon-plain-tag">externalIPs.enabled</pre></li>
<li><pre class="crayon-plain-tag">hostPort.enabled</pre></li>
</ol>
</li>
</ol>
<div class="blog_h3"><span class="graybg">会话绑定</span></div>
<p>Cilium的eBPF kube-proxy replacement支持K8S服务的会话绑定设置。对于<pre class="crayon-plain-tag">sessionAffinity: ClientIP</pre>，它会确保同一个Pod/宿主机总是被LB到同一个服务后端。会话绑定的默认超时为3h，可通过K8S的<pre class="crayon-plain-tag">sessionAffinityConfig</pre>改变。</p>
<p>会话绑定的依据，取决于请求的来源：</p>
<ol>
<li>对于集群外部发送给服务的请求，源IP地址用于会话绑定</li>
<li>对于集群内部发起的请求，则客户端网络命名空间的cookie用于会话绑定。这个特性5.7+内核支持，用于在socket layer实现会话绑定（此时源IP尚不可用，封包结构还没被内核创建）</li>
</ol>
<p>如果启用了eBPF kube-proxy replacement，则会话绑定默认启用。要启用，设置<pre class="crayon-plain-tag">config.sessionAffinity=false</pre></p>
<p>如果用户内核版本比较老，不支持网络命名空间cookie。则可以使用fallback的in-cluster模式，该模式使用一个固定的cookie，导致同一主机上，所有端点会绑定到某个服务的同一个后端。</p>
<div class="blog_h3"><span class="graybg">健康检查服务器</span></div>
<p>eBPF kube-proxy replacement包含一个health check server。要启用，需要设置<pre class="crayon-plain-tag">kubeProxyReplacementHealthzBindAddr</pre>。例如<pre class="crayon-plain-tag">kubeProxyReplacementHealthzBindAddr='0.0.0.0:10256'</pre>。/healthz端点用于访问健康状态。</p>
<div class="blog_h3"><span class="graybg">LoadBalancer源地址范围检查</span></div>
<p>如果LoadBalancer服务指定了spec.loadBalancerSourceRanges。则eBPF kube-proxy replacement会限制外部流量对服务的访问。仅仅允许spec.loadBalancerSourceRanges指定的CIDR白名单。从集群内部访问时，忽略此字段。</p>
<p>此特性默认启用，要禁用，设置<pre class="crayon-plain-tag">config.svcSourceRangeCheck=false</pre>。</p>
<div class="blog_h3"><span class="graybg">service-proxy-name</span></div>
<p>和kube-proxy类似，eBPF kube-proxy replacement遵从服务的<pre class="crayon-plain-tag">service.kubernetes.io/service-proxy-name</pre>注解。此注解声明什么服务代理（kube-proxy / replacement...）应该管理此服务。</p>
<p>eBPF kube-proxy replacement的服务代理名通过<pre class="crayon-plain-tag">k8s.serviceProxyName</pre>设置。默认值为空，意味着仅仅没有设置service.kubernetes.io/service-proxy-name的服务可以被replacement管理。</p>
<div class="blog_h3"><span class="graybg">限制条件</span></div>
<p>使用Cilium的eBPF kube-proxy replacement时，有很多限制条件需要注意：</p>
<ol>
<li>不能和透明加密一起使用</li>
<li>依赖宿主机可达服务这一特性。该特性需要依赖于eBPF cgroup hooks来实现服务转换。而eBPF中的getpeername需要5.8+内核才能支持。这意味着replacement无法和libceph一起工作</li>
<li>XDP加速仅支持单个设备的hairpin LB场景。如果具有多个网卡，并且cilium自动检测并选择多个网卡，则必须通过devices选项指定一个</li>
<li>DSR NodePort模式目前不能很好的在启用了TCP Fast Open（TFO）的环境下使用，建议切换到SNAT模式</li>
<li>不支持SCTP协议</li>
<li>不支持Pod配置的hostPort和NodePort范围冲突。这种情况下hostPort被忽略，并且cilium-agent会打印警告日志</li>
<li><span style="background-color: #c0c0c0;">不允许从集群外部访问ClusterIP</span></li>
<li>不支持ping ClusterIP，不像IPVS</li>
</ol>
<div class="blog_h2"><span class="graybg">带宽管理器</span></div>
<p>利用Cilium的带宽管理器，可以有效的在EDT（Earliest Departure Time）、eBPF的帮助下，管理每个Pod的带宽占用。</p>
<p>Cilium的带宽管理器，不依赖于CNI chaining，而是在Cilium内部实现的，它不使用<a href="https://kubernetes.io/docs/concepts/extend-kubernetes/compute-storage-net/network-plugins/#support-traffic-shaping">bandwidth CNI</a>这个插件。出于可扩容性考虑（特别是对于多队列的网卡），不建议使用bandwith CNI插件，因为它基于qdisc TBF而非EDT。</p>
<p>Cilium带宽管理器支持Pod注解<pre class="crayon-plain-tag">kubernetes.io/egress-bandwidth</pre>，它在“原生宿主网络设备”上控制egress流量带宽。不管是直接路由还是隧道，都可以进行流量限制。</p>
<p>Pod注解<pre class="crayon-plain-tag">kubernetes.io/ingress-bandwidth</pre>不被支持，也不推荐使用。</p>
<p>带宽限制天然应该发生在egress以降低/整平在网线上的带宽使用。如果在ingress段进行带宽限制，会额外的、通过ifb设备，在节点的关键fast-path增加一层缓冲队列，这种情况下，流量需要被重定向到ifb设备的egress端以实现塑形。这本质上没有意义，因为流量已经占用了网线山的带宽，节点也已经消耗了资源处理它，唯一的作用就是引入ifb让上层应用遭受带宽限制的痛苦。</p>
<p>带宽管理器需要Linux 5.1+内核。</p>
<p>带宽管理器默认启用，不需要在安装时指定特殊的选项。如果想禁用，设置<pre class="crayon-plain-tag">bandwidthManager=false</pre>。</p>
<p>所谓“原生宿主网络设备”是指具有默认路由的网络接口，或者分配了InternalIP/ExternalIP的接口，分配InternalIP的接口优先。如果要手工指定设置，设置<pre class="crayon-plain-tag">devices</pre>选项。</p>
<p>对Pod进行带宽限制的例子：</p>
<pre class="crayon-plain-tag">apiVersion: apps/v1
kind: Deployment
metadata:
  name: netperf
spec:
  selector:
    matchLabels:
      run: netperf
  replicas: 1
  template:
    metadata:
      labels:
        run: netperf
      annotations:
        kubernetes.io/egress-bandwidth: "10M"
    spec:
      nodeName: foobar
      containers:
      - name: netperf
        image: cilium/netperf
        ports:
        - containerPort: 12865</pre>
<div class="blog_h3"><span class="graybg">限制条件 </span></div>
<p>目前带宽管理器不能和L7策略联用。如果L7策略选择了Pod，则Pod上设置的注解被忽略，不进行带宽限制。</p>
<div class="blog_h2"><span class="graybg">联用Kata Containers</span></div>
<p>Cilium可以和Kata联用，后者提供计算层安全性。根据你使用的容器运行时，配置Cilium：</p>
<ol>
<li>如果使用CRI-O：<pre class="crayon-plain-tag">--set containerRuntime.integration=crio</pre></li>
<li>如果使用CRI-containerd：<pre class="crayon-plain-tag">--set containerRuntime.integration=containerd</pre></li>
</ol>
<p>Kata containers不支持宿主机可达服务特性，因而也不支持kube-proxy replacement的strict模式。</p>
<div class="blog_h2"><span class="graybg">Egress网关</span></div>
<p>出口网关允许将Pod的出口流量重定向到特定的网关节点，功能类似于Istio的出口网关。参考下面的选项启用该特性：</p>
<pre class="crayon-plain-tag">helm upgrade cilium cilium/cilium
   --namespace kube-system \
   --reuse-values \
   --set egressGateway.enabled=true \
   --set bpf.masquerade=true \
   --set kubeProxyReplacement=strict</pre>
<p>你需要配置<pre class="crayon-plain-tag">CiliumEgressNATPolicy</pre>才能让Egress网关对特定端点生效：</p>
<pre class="crayon-plain-tag">apiVersion: cilium.io/v2alpha1
kind: CiliumEgressNATPolicy
metadata:
  name: egress-sample
spec:
  egress:
  - podSelector:
      matchLabels:
        # 如果端点是运行在default命名空间的app=alpine
        app: alpine
        io.kubernetes.pod.namespace: default
    # 也可以用命名空间选择器，匹配多个命名空间（中的所有Pod）
    # namespaceSelector:
    #  matchLabels:
    #    ns: default
  # 并且尝试访问下面的CIDR（集群外部服务）
  destinationCIDRs:
  - 192.168.33.13/32
  # 那么将流量转发给Egress网关，该网关（节点）配置了IP地址192.168.33.100
  # 出集群封包将SNAT为192.168.33.100
  egressSourceIP: "192.168.33.100"</pre>
<p>作为Egress网关的节点，需要在网络接口上配置额外的IP（对应上面的 egressSourceIP）。</p>
<div class="blog_h1"><span class="graybg">集群网格</span></div>
<p>Cluster Mesh将网络数据路径延伸到多个集群，支持以下特性：</p>
<ol>
<li>实现<span style="background-color: #c0c0c0;">所有集群的Pod之间相互连通</span>，不管使用直接路由还是隧道模式。不需要额外的网关节点或代理</li>
<li>支持全局服务，可以在所有集群访问</li>
<li>支持全局性的安全策略</li>
<li>支持跨集群边界通信的透明加密</li>
</ol>
<div class="blog_h2"><span class="graybg">应用场景</span></div>
<div class="blog_h3"><span class="graybg">高可用</span></div>
<p>两个（位于不同Region或AZ的）集群组成高可用，当一个集群的后端服务（不是整个AZ不可用）出现故障时，可以failover到另外一个集群的对等物。</p>
<p><a href="https://blog.gmem.cc/wp-content/uploads/2020/06/usecase_ha.png"><img class="size-large wp-image-37965 aligncenter" src="https://blog.gmem.cc/wp-content/uploads/2020/06/usecase_ha-1024x342.png" alt="usecase_ha" width="710" height="237" /></a></p>
<div class="blog_h3"><span class="graybg">共享服务</span></div>
<p>最初的K8S用法是，倾向于创建巨大的、多租户的集群。而现在，更场景的用法是为每个租户创建独立的集群，甚至为不同类型的服务（例如安全级别不同）创建独立的集群。尽管如此，仍然有一些服务具有共享特征，不适合在每个集群中都部署一份。这类服务包括：日志、监控、DNS、密钥管理，等等。</p>
<p>使用集群网格，可以将共享服务独立部署在一个集群中，租户集群可以访问其中的全局服务。</p>
<p><a href="https://blog.gmem.cc/wp-content/uploads/2020/06/usecase_shared_services.png"><img class="size-large wp-image-37969 aligncenter" src="https://blog.gmem.cc/wp-content/uploads/2020/06/usecase_shared_services-1024x444.png" alt="usecase_shared_services" width="710" height="307" /></a></p>
<div class="blog_h3"><span class="graybg">联合Istio Multicluster<br /></span></div>
<p>Cilium Clustermesh和Istio Multicluster可以相互补充。典型的用法是，<span style="background-color: #c0c0c0;">Cilium提供跨集群的Pod IP可路由性，而这是Istio Multiplecluster所需要的前置条件</span>。</p>
<div class="blog_h2"><span class="graybg">前提条件</span></div>
<ol>
<li>所有集群的Pod CIDR不冲突</li>
<li>所有节点的IP地址不冲突</li>
<li>所有集群的节点，都具有IP层的相互连接性。可能需要创建对等/VPN隧道</li>
<li>集群之间的网络必须允许跨集群通信，到底需要哪些端口本章后续会详述</li>
</ol>
<div class="blog_h2"><span class="graybg">启用网格</span></div>
<div class="blog_h3"><span class="graybg">指定集群标识</span></div>
<p>每个集群都需要唯一的名字和ID：</p>
<pre class="crayon-plain-tag">helm upgrade cilium cilium/cilium \
   --namespace kube-system \
   --reuse-values \
   --set cluster.name=k8s --set cluster.id=27</pre>
<p>注意，如果改变正在运行的集群的ID/名字，其中所有工作负载都需要重新启动。因为ID用于生成安全标识（security identity），安全标识需要重新创建才能创建跨集群的通信。</p>
<pre class="crayon-plain-tag">helm --kube-context tke install cilium cilium/cilium --version 1.10.1                        \
  --namespace kube-system                                                                    \
  --set debug.enabled=true                                                                   \
  --set cluster.id=28                                                                        \
  --set rollOutCiliumPods=true                                                               \
  --set cluster.name=tke                                                                     \
  --set image.repository=docker.gmem.cc/cilium/cilium                                        \
  --set preflight.image.repository=docker.gmem.cc/cilium/cilium                              \
  --set image.useDigest=false                                                                \
  --set operator.image.repository=docker.gmem.cc/cilium/operator                             \
  --set operator.image.useDigest=false                                                       \
  --set certgen.image.repository=docker.gmem.cc/cilium/certgen                               \
  --set hubble.relay.image.repository=docker.gmem.cc/cilium/hubble-relay                     \
  --set hubble.relay.image.useDigest=false                                                   \
  --set hubble.ui.backend.image.repository=docker.gmem.cc/cilium/hubble-ui-backend           \
  --set hubble.ui.backend.image.tag=v0.7.9                                                   \
  --set hubble.ui.frontend.image.repository=docker.gmem.cc/cilium/hubble-ui                  \
  --set hubble.ui.frontend.image.tag=v0.7.9                                                  \
  --set hubble.ui.proxy.image.repository=docker.gmem.cc/envoyproxy/envoy                     \
  --set hubble.ui.proxy.image.tag=v1.18.2                                                    \
  --set etcd.image.repository=docker.gmem.cc/cilium/cilium-etcd-operator                     \
  --set etcd.image.tag=v2.0.7                                                                \
  --set nodeinit.image.repository=docker.gmem.cc/cilium/startup-script                       \
  --set nodeinit.image.tag=62bfbe88c17778aad7bef9fa57ff9e2d4a9ba0d8                          \
  --set clustermesh.apiserver.image.repository=docker.gmem.cc/cilium/clustermesh-apiserver   \
  --set clustermesh.apiserver.image.useDigest=false                                          \
  --set clustermesh.apiserver.etcd.image.repository=docker.gmem.cc/coreos/etcd               \
  --set tunnel=disabled                                                                      \
  --set autoDirectNodeRoutes=true                                                            \
  --set nativeRoutingCIDR=172.28.0.0/16                                                      \
  --set bpf.hostRouting=true                                                                 \
  --set ipam.mode=cluster-pool                                                               \
  --set ipam.operator.clusterPoolIPv4PodCIDR=172.28.0.0/16                                   \
  --set ipam.operator.clusterPoolIPv4MaskSize=24                                             \
  --set fragmentTracking=true                                                                \
  --set bpf.masquerade=true                                                                  \
  --set hostServices.enabled=true                                                            \
  --set kubeProxyReplacement=strict                                                          \
  --set k8sServiceHost=10.2.0.61                                                             \
  --set k8sServicePort=6443                                                                  \
  --set loadBalancer.algorithm=maglev                                                        \
  --set loadBalancer.mode=hybrid                                                             \
  --set bandwidthManager=true</pre>
<div class="blog_h3"><span class="graybg">创建cilium-ca</span></div>
<p>集群网格会基于此CA创建其API Server的数字证书：</p>
<pre class="crayon-plain-tag">kubectl -n kube-system create secret generic --from-file=ca.key=ca.key --from-file=ca.crt=ca.crt cilium-ca </pre>
<div class="blog_h3"><span class="graybg">启用网格</span></div>
<p>需要在组成网格的两个集群中都执行cilium clustermesh enable命令：</p>
<pre class="crayon-plain-tag">cilium clustermesh enable --context k8s --service-type LoadBalancer \
    --apiserver-image docker.gmem.cc/cilium/clustermesh-apiserver:v1.10.1
cilium clustermesh enable --context tke --service-type LoadBalancer \
    --apiserver-image docker.gmem.cc/cilium/clustermesh-apiserver:v1.10.1</pre>
<p>上述命令会：</p>
<ol>
<li>部署clustermesh-apiserver到集群</li>
<li>生成所有必须的数字证书、保存为Secret</li>
<li>自动检测最佳的service类型，以暴露集群网格的控制平面给其它集群。某些时候，service类型不能自动检测，你可手工通过<pre class="crayon-plain-tag">--service-type</pre>指定</li>
</ol>
<p>通过下面的命令等待集群网格组件就绪：<pre class="crayon-plain-tag">cilium clustermesh status --wait</pre>，如果服务类型选择LoadBalancer，该命令也会等待LoadBalancer IP就绪。</p>
<div class="blog_h3"><span class="graybg">连接集群</span></div>
<p>最后一步是连接集群，只需要在网格的一端进行连接即可。对向连接会自动创建：</p>
<pre class="crayon-plain-tag">cilium clustermesh connect --context k8s --destination-context tke</pre>
<p>通过下面的命令等待连接成功：<pre class="crayon-plain-tag">cilium clustermesh status --wait</pre></p>
<div class="blog_h3"><span class="graybg">测试跨集群连接性</span></div>
<pre class="crayon-plain-tag">cilium connectivity test --context k8s --multi-cluster tke</pre>
<p>注意：<span style="background-color: #c0c0c0;">两个集群的Pod网络会被打通，你可以从一个集群直接访问另外一个集群的Pod</span>。<span style="background-color: #c0c0c0;">默认情况下，Cilium不允许从集群外部访问PodCIDR，可以ping但是访问端口会RST</span>。</p>
<div class="blog_h3"><span class="graybg">查看网格状态</span></div>
<pre class="crayon-plain-tag">cilium clustermesh status --context k8s
cilium clustermesh status --context tke</pre>
<div class="blog_h3"><span class="graybg">限制条件</span></div>
<p>目前最多支持相互连接在一起的集群数量为255，未来此限制会放开，当：</p>
<ol>
<li>运行在直接路由模式时</li>
<li>运行在隧道模式，且启用加密时</li>
</ol>
<div class="blog_h2"><span class="graybg">服务发现</span></div>
<p>Cilium的集群网格，支持跨集群的服务发现和负载均衡。</p>
<div class="blog_h3"><span class="graybg">全局服务</span></div>
<p>跨集群的负载均衡，依赖于全局服务。所谓全局服务：</p>
<ol>
<li>在所有集群中具有相同的namespace和name</li>
<li>设置了注解<pre class="crayon-plain-tag">io.cilium/global-service: "true"</pre>，注意，<span style="background-color: #c0c0c0;">所有集群的服务都要添加此注解</span></li>
</ol>
<p>Cilium会自动跨越多个集群进行负载均衡。A集群中的Pod可能访问到B集群的后端。</p>
<pre class="crayon-plain-tag">apiVersion: v1
kind: Service
metadata:
  name: nginx
  annotations:
    io.cilium/global-service: "true"
    # 下面这个注解是隐含的
    io.cilium/shared-service: "true"
spec:
  type: ClusterIP
  ports:
  - port: 80
  selector:
    app: nginx</pre>
<div class="blog_h3"><span class="graybg">远程服务</span></div>
<p>如果设置<pre class="crayon-plain-tag">io.cilium/shared-service: "false"</pre>，则该服务的端点，仅由远程集群提供。</p>
<div class="blog_h2"><span class="graybg">网络策略</span></div>
<p>CiliumNetworkPolicy、NetworkPolicy自然就能跨集群生效，这是因为Cilium解耦了网络安全和网络连接性。但是这些对象不会自动复制到各集群，你需要手工处理。</p>
<p>下面的网络策略，允许特定端点跨集群的访问服务：</p>
<pre class="crayon-plain-tag"># 允许k8s中的alpine访问tke中的nginx
apiVersion: "cilium.io/v2"
kind: CiliumNetworkPolicy
metadata:
  name: "allow-cross-cluster"
spec:
  endpointSelector:
    matchLabels:
      app: alpine
      io.cilium.k8s.policy.cluster: k8s
  egress:
  - toEndpoints:
    - matchLabels:
        app: nginx
        io.cilium.k8s.policy.cluster: tke</pre>
<div class="blog_h3"><span class="graybg">限制条件</span></div>
<p>L7策略仅仅在以下条件下可以跨集群生效：</p>
<ol>
<li>启用直接路由模式，也就是禁用隧道</li>
<li>节点安装了路由，允许路由所有集群的Pod IP</li>
</ol>
<p>第2点，如果节点L2互联，可以通过设置--auto-direct-node-routes=true满足。</p>
<div class="blog_h2"><span class="graybg">集群外工作负载</span></div>
<p>你可以将外部工作负载（例如VM）加入到K8S集群，并且应用安全策略。</p>
<div class="blog_h3"><span class="graybg"> 前提条件</span></div>
<ol>
<li>必须配置基于K8S进行身份标识（identity）分配，即<pre class="crayon-plain-tag">identityAllocationMode=crd</pre>（默认值）</li>
<li>外部工作负载必须基于4.17+的内核，这样它才能访问K8S服务</li>
<li>外部工作负载必须和集群节点IP层互联。如果在同一VPC中运行虚拟机和K8S，通常可以满足。否则，可能需要在K8S集群网络和外部工作负载网络之间进行对等/VPN连接</li>
<li>外部工作负载必须具有唯一的IP地址，和集群内节点不冲突</li>
<li>目前此特性仅在VXLAN隧道模式下测试过</li>
</ol>
<div class="blog_h3"><span class="graybg">启用集群网格</span></div>
<pre class="crayon-plain-tag">cilium install --config tunnel=vxlan ...
cilium clustermesh enable</pre>
<div class="blog_h3"><span class="graybg">测试工作负载</span></div>
<p>必须创建<pre class="crayon-plain-tag">CiliumExternalWorkload</pre>来通知集群，外部工作负载的存在。该自定义资源：</p>
<ol>
<li>为外部工作负载指定命名空间和身份标识标签</li>
<li>名字必须和外部工作负载的主机名（hostname命令输出）一致</li>
<li>为外部工作负载分配一个很小的CIDR</li>
</ol>
<p>可以通过命令创建CiliumExternalWorkload：</p>
<pre class="crayon-plain-tag">#                  vm是子命令external-workload的别名
#                            工作负载名字
#                                      加入的命名空间
#                                              分配的CIDR
cilium clustermesh vm create zircon -n default --ipv4-alloc-cidr 10.0.0.1/32</pre>
<p>下面的命令可以查看现有外部工作负载的状态：</p>
<pre class="crayon-plain-tag">cilium clustermesh vm status</pre>
<p>此时，可以看到 zircon 的<pre class="crayon-plain-tag">IP</pre>状态为<pre class="crayon-plain-tag">N/A</pre>，这提示工作负责尚未加入集群。</p>
<p>下面的命令会生成一个安装脚本：</p>
<pre class="crayon-plain-tag">cilium clustermesh vm install install-external-workload.sh</pre>
<p>该脚本从集群中抽取了TLS证书、其它访问信息 ，可用于在外部工作负责中安装Cilium并连接到你的K8S集群。脚本中嵌入了clustermesh-apiserver服务的IP地址，如果你没有使用LoadBalancer类型而是使用NodePort，则IP是第一个K8S节点的地址。</p>
<p>拷贝install-external-workload.sh到外部工作负载节点，然后执行，该脚本会：</p>
<ol>
<li>创建并运行一个名为cilium的容器</li>
<li>拷贝cilium CLI到文件系统</li>
<li>等待节点连接到集群，集群服务可用。然后修改/etc/resolv.conf，将kube-dns地址设置到其中</li>
</ol>
<p>注意，如果外部工作负载有多个网络接口，在运行脚本之前你需要设置环境变量<pre class="crayon-plain-tag">HOST_IP</pre>。</p>
<p>在外部工作负载执行命令<pre class="crayon-plain-tag">cilium status</pre>检查连接性。</p>
<div class="blog_h1"><span class="graybg">运维</span></div>
<div class="blog_h2"><span class="graybg">状态解读</span></div>
<p>每个cilium-agnet节点的各种状态信息，可以通过cilium status命令获得：</p>
<pre class="crayon-plain-tag"># kubectl -n kube-system exec cilium-m8wf2 -- cilium status
# 是否启用外部的KV存储
KVStore:                Ok   Disabled
# K8S状态
Kubernetes:             Ok   1.20 (v1.20.5) [linux/amd64]
Kubernetes APIs:        ["cilium/v2::CiliumClusterwideNetworkPolicy", "cilium/v2::CiliumEndpoint", "cilium/v2::CiliumNetworkPolicy", "cilium/v2::CiliumNode", "core/v1::Namespace", "core/v1::Node", "core/v1::Pods", "core/v1::Service", "discovery/v1beta1::EndpointSlice", "networking.k8s.io/v1::NetworkPolicy"]
# kube-proxy replacement工作模式
KubeProxyReplacement:   Strict   [eth0 10.0.3.1 (Direct Routing)]
# cilium-agnet状态
Cilium:                 Ok   1.10.1 (v1.10.1-e6f34c3)
NodeMonitor:            Listening for events on 8 CPUs with 64x4096 of shared memory
Cilium health daemon:   Ok   
# 本节点IP池信息           已分配/总计                分配的节点CIDR
IPAM:                   IPv4: 9/254 allocated from 172.27.2.0/24, 
# 集群网格状态
ClusterMesh:            0/0 clusters ready, 0 global-services
# 带宽管理器             这里提示基于qdisc EDT实现，在eth0上进行带宽管理
BandwidthManager:       EDT with BPF   [eth0]
# 直接路由需要通过宿主机的网络栈
Host Routing:           Legacy
# 基于BPF实现IP遮掩                     不进行遮掩的CIDR
Masquerading:           BPF   [eth0]   172.27.0.0/16 [IPv4: Enabled, IPv6: Disabled]
Controller Status:      55/55 healthy
Proxy Status:           OK, ip 172.27.2.122, 0 redirects active on ports 10000-20000
Hubble:                 Ok   Current/Max Flows: 4095/4095 (100.00%), Flows/s: 67.08   Metrics: Disabled
# 流量加密已禁用
Encryption:             Disabled
# 集群节点/端点状态。如果存在不健康的对象，这里可以看到
Cluster health:         3/3 reachable   (2021-07-02T07:09:23Z)</pre>
<div class="blog_h1"><span class="graybg">开发</span></div>
<div class="blog_h2"><span class="graybg">构建</span></div>
<p>迁出项目后，在项目根目录下执行命令进行开发环境检查：<pre class="crayon-plain-tag">make dev-doctor</pre> 。</p>
<p>为了运行单元测试，需要docker；为了在虚拟机中运行Cilium，需要Vagrant和VirtualBox。建议在虚拟机中进行开发、构建、运行。</p>
<div class="blog_h3"><span class="graybg">Vagrant配置</span></div>
<p>通过下面的命令启动包含Cilium依赖的Vagrant虚拟机：</p>
<pre class="crayon-plain-tag"># 基于base系统cilium/ubuntu
contrib/vagrant/start.sh [vm_name]</pre>
<p>可选的vm_name用于添加新的虚拟机到现有集群中：</p>
<pre class="crayon-plain-tag"># 在节点上构建并安装K8S，主节点k8s1
#     创建一个从节点k8s2
#                使用net-next内核
K8S=1 NWORKERS=1 NETNEXT=1 ./contrib/vagrant/start.sh k8s2+

# 其它环境变量

# 执行vagrant reload而非vagrant up，用于恢复挂起的虚拟机
RELOAD=1
# 不在虚拟机中构建Cilium，用于快速重启（不去完全重新构建Cilium）
NO_PROVISION=1
# 启用Cilium的IPv4支持
IPV4=1
# 选择容器运行时：docker, containerd, crio
RUNTIME=docker
# 设置代理
VM_SET_PROXY=http://10.0.0.1:8088 
# 重新安装Cilium、K8S等，如果安装过程被打断有用
INSTALL=1
# 在虚拟机中构建Cilium前执行make clean
MAKECLEAN=1
# 不在虚拟机中进行构建，假设开发者先前已经在虚拟机中执行过make build
NO_BUILD=1
# 定义额外的挂载点
# USER_MOUNTS=foo 将宿主机的~/foo挂载为虚拟机的/home/vagrant/foo
# USER_MOUNTS=foo,/tmp/bar=/tmp/bar 额外挂载宿主机的/tmp/bar为虚拟机的/tmp/bar
USER_MOUNTS=
# 设置虚拟机内存,单位MB
VM_MEMORY=4096
# 设置虚拟机CPU数量
VM_CPUS=2</pre>
<p>Vagrantfile会在项目根目录寻找文件 <pre class="crayon-plain-tag">.devvmrc</pre>，如果文件存在且可执行，则VM启动时会执行它。你可以用该文件定制VM。</p>
<p>宿主机上的Cilium代码树不需要手工同步到虚拟机中，该目录默认已经通过VirtualBox NFS共享给虚拟机。</p>
<p>你也可以不使用start.sh脚本，手工启动虚拟机并构建Cilium：</p>
<pre class="crayon-plain-tag">vagrant init cilium/ubuntu
vagrant up
vagrant ssh [...]

go get github.com/cilium/cilium
cd go/src/github.com/cilium/cilium/
# 修改代码后，构建Cilium
make
# 重新安装Cilium
make install

mkdir -p /etc/sysconfig/
cp contrib/systemd/cilium.service /etc/systemd/system/
cp contrib/systemd/cilium-docker.service /etc/systemd/system/
cp contrib/systemd/cilium-consul.service /etc/systemd/system/
cp contrib/systemd/cilium  /etc/sysconfig/cilium
usermod -a -G cilium vagrant
systemctl enable cilium-docker
systemctl restart cilium-docker
systemctl enable cilium-consul
systemctl restart cilium-consul
systemctl enable cilium
# 重新启动新安装的Cilium
systemctl restart cilium

# 冒烟测试，确保Cilium正确启动，和Envoy的集成正常工作
tests/envoy-smoke-test.sh</pre>
<div class="blog_h3"><span class="graybg">构建开发者镜像</span></div>
<p>使用下面的命令，依据本地修改，构建cilium-agnet的镜像：</p>
<pre class="crayon-plain-tag">ARCH=amd64 DOCKER_REGISTRY=docker.gmem.cc DOCKER_DEV_ACCOUNT=cilium DOCKER_IMAGE_TAG=1.10.1 make dev-docker-image</pre>
<p>使用下面的命令，依据本地修改，构建cilium-operator的镜像：</p>
<pre class="crayon-plain-tag">make docker-operator-generic-image
# 类似，针对特定云平台的Operator镜像
make docker-operator-aws-image
make docker-operator-azure-image</pre>
<div class="blog_h2"><span class="graybg">调试</span></div>
<div class="blog_h3"><span class="graybg"> 数据路径代码</span></div>
<p>无法单步跟踪，主要依靠<pre class="crayon-plain-tag">cilium monitor</pre>。当cilium-agent或者某个特定的端点在debug模式下运行的话，Cilium会发送调试信息。</p>
<p>要让cilium-agent运行在debug模式，使用<pre class="crayon-plain-tag">--debug</pre>选项或在在运行时执行<pre class="crayon-plain-tag">cilium config debug=true</pre>。</p>
<p>要让特定端点进入debug模式，执行命令<pre class="crayon-plain-tag">cilium endpoint config ID debug=true</pre>。</p>
<p>使用<pre class="crayon-plain-tag">cilium monitor -v -v</pre>可以显示更多调试信息。</p>
<p>开发eBPF时，常遇到的问题是代码无法载入内核，此时，通过<pre class="crayon-plain-tag">cilium endpoint list</pre>会看到<pre class="crayon-plain-tag">not-ready</pre>状态的端点。你可以利用命令<pre class="crayon-plain-tag">cilium endpoint get</pre>来获取端点的eBPF校验日志。</p>
<p>目录<pre class="crayon-plain-tag">/var/run/cilium/state</pre>下的文件说明Cilium如何建立和管理BPF数据路径。.h文件包含了用于BPF程序编译的头文件配置，以数字为名的目录对应特定端点的状态，包括头文件和BPF二进制文件。</p>
<div class="blog_h3"><span class="graybg">查看eBPF Map</span></div>
<p>eBPF Map状态存放在/sys/fs/bpf/下，工具bpf-map可以用于查看其中的内容。</p>
<div class="blog_h1"><span class="graybg">源码分析(cilium-agent)</span></div>
<div class="blog_h2"><span class="graybg">环境准备</span></div>
<p>K8S中cilium-agent启动时的命令行为：</p>
<pre class="crayon-plain-tag">/usr/bin/cilium-agent --config-dir=/tmp/cilium/config-map</pre>
<p>/tmp/cilium/config-map是一个目录， 每个文件对应ConfigMap cilium-config中的一项。我们在本地调试cilium-agent时，可以将配置项写在YAML中：</p>
<pre class="crayon-plain-tag">auto-direct-node-routes: "true"
bpf-lb-map-max: "65536"
bpf-map-dynamic-size-ratio: "0.0025"
bpf-policy-map-max: "16384"
cilium-endpoint-gc-interval: 5m0s
cluster-id: "27"
cluster-name: k8s
cluster-pool-ipv4-cidr: 172.27.0.0/16
cluster-pool-ipv4-mask-size: "24"
custom-cni-conf: "false"
debug: "true"
disable-cnp-status-updates: "true"
enable-auto-protect-node-port-range: "true"
enable-bandwidth-manager: "true"
enable-bpf-clock-probe: "true"
enable-bpf-masquerade: "true"
enable-endpoint-health-checking: "true"
enable-health-check-nodeport: "true"
enable-health-checking: "true"
enable-host-legacy-routing: "false"
enable-host-reachable-services: "true"
enable-hubble: "true"
enable-ipv4: "true"
enable-ipv4-fragment-tracking: "true"
enable-ipv4-masquerade: "true"
enable-ipv6: "false"
enable-ipv6-masquerade: "true"
enable-l7-proxy: "true"
enable-local-redirect-policy: "false"
enable-policy: default
enable-remote-node-identity: "true"
enable-session-affinity: "true"
enable-well-known-identities: "false"
enable-xt-socket-fallback: "true"
hubble-disable-tls: "false"
hubble-listen-address: :4244
hubble-socket-path: /var/run/cilium/hubble.sock
hubble-tls-cert-file: /var/lib/cilium/tls/hubble/server.crt
hubble-tls-client-ca-files: /var/lib/cilium/tls/hubble/client-ca.crt
hubble-tls-key-file: /var/lib/cilium/tls/hubble/server.key
identity-allocation-mode: crd
install-iptables-rules: "true"
install-no-conntrack-iptables-rules: "false"
ipam: cluster-pool
kube-proxy-replacement: strict
kube-proxy-replacement-healthz-bind-address: ""
monitor-aggregation: medium
monitor-aggregation-flags: all
monitor-aggregation-interval: 5s
native-routing-cidr: 172.27.0.0/16
node-port-bind-protection: "true"
operator-api-serve-addr: 127.0.0.1:9234
preallocate-bpf-maps: "false"
sidecar-istio-proxy-image: cilium/istio_proxy
tunnel: disabled
wait-bpf-mount: "false"</pre>
<p>然后使用<pre class="crayon-plain-tag">--config=ciliumd.yaml</pre>启动cilium-agent。</p>
<div class="blog_h2"><span class="graybg">核心数据结构</span></div>
<div class="blog_h3"><span class="graybg">Endpoint</span></div>
<p>端点表示一个容器或者类似的，能够在L3独立寻址（具有独立IP地址）的网络实体。端点由端点管理器管理。</p>
<p>Cilium中的端点，仅仅<span style="background-color: #c0c0c0;">在当前节点的视角下</span>考虑。</p>
<pre class="crayon-plain-tag">type Endpoint struct {
	owner regeneration.Owner

	// 节点范围内唯一的ID
	ID uint16

	// 端点创建时间
	createdAt time.Time

	// 保护端点状态写入操作
	mutex lock.RWMutex

	// 端点对应容器的名字
	containerName string

	// 端点对应容器的ID
	containerID string

	// 如果端点由Docker管理，该字段填写libnetwork网络ID
	dockerNetworkID string

	// 如果端点由Docker管理，该字段填写libnetwork端点ID
	dockerEndpointID string

	// IPVLAN数据路径下，对应的用于尾调用的BPF Map标识符
	datapathMapID int

	// 宿主机端的，连接到端点的网络接口名。通常是veth
	ifName string

	// 宿主机段网络接口索引
	ifIndex int

	// 端点标签配置
	// FIXME: 该字段应该命名为Label
	OpLabels labels.OpLabels

	// 身份标识版本号，该版本号在端点的身份标识标签变化后会递增
	identityRevision int

	// 端点的出口速率
	bps uint64

	// 端点的MAC地址
	mac mac.MAC

	// 端点的IPv6地址
	IPv6 addressing.CiliumIPv6

	// 端点的IPv4地址
	IPv4 addressing.CiliumIPv4

	// 宿主节点的MAC地址，对于每个端点不一样
	nodeMAC mac.MAC

	// 根据端点标签计算的安全标识
	SecurityIdentity *identity.Identity `json:"SecLabel"`

	// 提示端点是否被Istio注入了Cilium兼容的sidecar proxy：
	// 1. 如果是，该sidecar用于应用L7策略规则
	// 2. 如果否，则节点级别的Cilium Envoy用于应用L7策略规则
	// 
	// 目前仅针对HTTP L7规则，Kafka只能在节点级别Enovy中应用
	hasSidecarProxy bool

	// 数据路径的策略相关的Map，包含对所有策略相关的BPF的引用
	policyMap *policymap.PolicyMap

	// 跟踪policyMap压力的指标
	policyMapPressureGauge *metrics.GaugeWithThreshold

	// 端点的数据路径配置
	Options *option.IntOptions

	// 最后N次端点的状态转换信息
	status *EndpointStatus

	// 当前端点特定的DNS代理规则的集合。cilium-agent重启时能够恢复
	DNSRules restore.DNSRules

	// 为此端点拦截的，依然有效的DNS响应缓存
	DNSHistory *fqdn.DNSCache

	// 已经过期或者从DNSHistory中驱除的DNS IPs，在确认没有连接时用这些IP后会自动删除
	DNSZombies *fqdn.DNSZombieMappings

	// dnsHistoryTrigger is the trigger to write down the ep_config.h to make
	// sure that restores when DNS policy is in there are correct
	dnsHistoryTrigger *trigger.Trigger

	// 端点状态
	state State

	// 最后一次编译和安装的BPF头文件的哈希
	bpfHeaderfileHash string

	// 端点对应的K8S Pod名
	K8sPodName string

	// 端点对应的K8S Namespace
	K8sNamespace string

	// 端点对应的Pod
	pod *slim_corev1.Pod

	// 关联到Pod 的容器端口。基于端口名应用K8S网络策略时需要
	k8sPorts policy.NamedPortMap

	// 限制重复的警告日志
	logLimiter logging.Limiter

	// 跟踪k8sPorts被设置至少一次的情况
	hasK8sMetadata bool

	// 端点当前使用的策略的版本
	policyRevision uint64

	// policyRevisionSignals contains a map of PolicyRevision signals that
	// should be triggered once the policyRevision reaches the wanted wantedRev.
	policyRevisionSignals map[*policySignal]bool

	// 应用到代理的策略修订版
	proxyPolicyRevision uint64

	// 写proxyStatistics用的锁
	proxyStatisticsMutex lock.RWMutex

	proxy EndpointProxy

	// 代理重定向的统计信息，键是 policy.ProxyIDs
	proxyStatistics map[string]*models.ProxyStatistics

	// 端点已经更新的、下一次regenerate时使用的策略修订版
	nextPolicyRevision uint64

	// 当端点选项变化后，是否强制重新计算端点的策略
	forcePolicyCompute bool

	// buildMutex synchronizes builds of individual endpoints and locks out
	// deletion during builds
	buildMutex lock.Mutex

	// logger is a logrus object with fields set to report an endpoints information.
	// This must only be accessed with atomic.LoadPointer/StorePointer.
	// 'mutex' must be Lock()ed to synchronize stores. No lock needs to be held
	// when loading this pointer.
	logger unsafe.Pointer

	// policyLogger is a logrus object with fields set to report an endpoints information.
	// This must only be accessed with atomic LoadPointer/StorePointer.
	// 'mutex' must be Lock()ed to synchronize stores. No lock needs to be held
	// when loading this pointer.
	policyLogger unsafe.Pointer

	// controllers is the list of async controllers syncing the endpoint to
	// other resources
	controllers *controller.Manager

	// realizedRedirects maps the ID of each proxy redirect that has been
	// successfully added into a proxy for this endpoint, to the redirect's
	// proxy port number.
	// You must hold Endpoint.mutex to read or write it.
	realizedRedirects map[string]uint16

	// ctCleaned indicates whether the conntrack table has already been
	// cleaned when this endpoint was first created
	ctCleaned bool

	hasBPFProgram chan struct{}

	// selectorPolicy represents a reference to the shared SelectorPolicy
	// for all endpoints that have the same Identity.
	selectorPolicy policy.SelectorPolicy

	desiredPolicy *policy.EndpointPolicy

	realizedPolicy *policy.EndpointPolicy

	visibilityPolicy *policy.VisibilityPolicy

	eventQueue *eventqueue.EventQueue

	// skippedRegenerationLevel is the DatapathRegenerationLevel of the regeneration event that
	// was skipped due to another regeneration event already being queued, as indicated by
	// state. A lower-level current regeneration is bumped to this level to cover for the
	// skipped regeneration levels.
	skippedRegenerationLevel regeneration.DatapathRegenerationLevel

	// DatapathConfiguration is the endpoint's datapath configuration as
	// passed in via the plugin that created the endpoint, e.g. the CNI
	// plugin which performed the plumbing will enable certain datapath
	// features according to the mode selected.
	DatapathConfiguration models.EndpointDatapathConfiguration

	aliveCtx        context.Context
	aliveCancel     context.CancelFunc
	regenFailedChan chan struct{}

	allocator cache.IdentityAllocator

	isHost bool

	noTrackPort uint16
}</pre>
<p>&nbsp;</p>
<div class="blog_h2"><span class="graybg">入口点</span></div>
<p>程序入口点位于daemon/cmd/daemon_main.go文件的RootCmd中： </p>
<pre class="crayon-plain-tag">var (
	log = logging.DefaultLogger.WithField(logfields.LogSubsys, daemonSubsys)

	bootstrapTimestamp = time.Now()

	// RootCmd represents the base command when called without any subcommands
	RootCmd = &amp;cobra.Command{
		Use:   "cilium-agent",
		Short: "Run the cilium agent",
		Run: func(cmd *cobra.Command, args []string) {
			cmdRefDir := viper.GetString(option.CMDRef)
			if cmdRefDir != "" {
				genMarkdown(cmd, cmdRefDir)
				os.Exit(0)
			}

			// gops监听套接字，gops能够获取Go进程的网络连接、调用栈等诊断信息
			addr := fmt.Sprintf("127.0.0.1:%d", viper.GetInt(option.GopsPort))
			addrField := logrus.Fields{"address": addr}
			if err := gops.Listen(gops.Options{
				Addr:                   addr,
				ReuseSocketAddrAndPort: true,
			}); err != nil {
				log.WithError(err).WithFields(addrField).Fatal("Cannot start gops server")
			}
			log.WithFields(addrField).Info("Started gops server")

			bootstrapStats.earlyInit.Start()
			// 环境初始化
			initEnv(cmd)
			bootstrapStats.earlyInit.End(true)
			// 运行cilium-agent
			runDaemon()
		},
	}

	bootstrapStats = bootstrapStatistics{}
)</pre>
<div class="blog_h2"><span class="graybg">环境初始化</span></div>
<p>该部分的整体逻辑包括：</p>
<ol>
<li>初始化配置option.Config
<ol>
<li>初始化Map尺寸（sizeof***Element）相关配置</li>
<li>利用viper读取命令行选项、配置文件中的的选项</li>
<li>配置K8S API Server客户端参数</li>
<li>各种调试选项：flow/envoy/datapath/policy</li>
<li>其它选项处理</li>
</ol>
</li>
<li>打印Logo和版本信息</li>
<li>确然当前用户具有root权限</li>
<li>检查PATH下cilium-envoy文件的版本</li>
<li>如果identity-allocation-mode=crd，断言启用了K8S集成</li>
<li>如果必要，启用PProf端口</li>
<li>如果必要，打开自动BPF Map分配开关</li>
<li>检查目录：
<ol>
<li>LibDir: "/var/lib/cilium"</li>
<li>RunDir: "/var/run/cilium"</li>
<li>BpfDir: "/var/lib/cilium/bpf"</li>
<li>StateDir: "/var/run/cilium/state"</li>
</ol>
</li>
<li>检查数据路径最小要求
<ol>
<li>Linux版本最低4.8.0</li>
<li>需要启用策略路由支持，检查内核配置CONFIG_IP_MULTIPLE_TABLES</li>
<li>如果Cilium启用了IPv6，断言路径/proc/net/if_inet6存在</li>
<li>断言clang、llc存在且版本满足需求。需要在运行时编译BPF源码</li>
<li>如果BpfDir不存在，提示make install-bpf拷贝BPF源码</li>
<li>启动probes.NewProbeManager()探测系统的BPF特性，检查内核参数。断言包linux-tools-generic已经安装</li>
</ol>
</li>
<li>检查或挂载BPF文件系统</li>
<li>检查或挂载Cgroups2文件系统</li>
</ol>
<div class="blog_h3"><span class="graybg">检查用户权限</span></div>
<p>直接要求当前用户为root：</p>
<pre class="crayon-plain-tag">func RequireRootPrivilege(cmd string) {
	if os.Getuid() != 0 {
		fmt.Fprintf(os.Stderr, "Please run %q command(s) with root privileges.\n", cmd)
		os.Exit(1)
	}
}</pre>
<div class="blog_h3"><span class="graybg">检查数据路径最小要求</span></div>
<p>解析内核版本major.minor.patch的代码：</p>
<pre class="crayon-plain-tag">func parseKernelVersion(ver string) (semver.Version, error) {
	verStrs := strings.Split(ver, ".")
	switch {
	case len(verStrs) &lt; 2:
		return semver.Version{}, fmt.Errorf("unable to get kernel version from %q", ver)
	case len(verStrs) &lt; 3:
		verStrs = append(verStrs, "0")
	}
	// We are assuming the kernel version will be something as:
	// 4.9.17-040917-generic

	// If verStrs is []string{ "4", "9", "17-040917-generic" }
	// then we need to retrieve patch number.
	patch := regexp.MustCompilePOSIX(`^[0-9]+`).FindString(verStrs[2])
	if patch == "" {
		verStrs[2] = "0"
	} else {
		verStrs[2] = patch
	}
	return versioncheck.Version(strings.Join(verStrs[:3], "."))
}

// GetKernelVersion returns the version of the Linux kernel running on this host.
func GetKernelVersion() (semver.Version, error) {
	var unameBuf unix.Utsname
	if err := unix.Uname(&amp;unameBuf); err != nil {
		return semver.Version{}, err
	}
	return parseKernelVersion(string(unameBuf.Release[:]))
}</pre>
<p>比较版本时使用库<pre class="crayon-plain-tag">github.com/blang/semver/v4</pre>：</p>
<pre class="crayon-plain-tag">type Range func(Version) bool;  // 这是一个函数，调用后直接判断版本是否符合要求
func MustCompile(constraint string) semver.Range {
	verCheck, err := Compile(constraint)
	if err != nil {
		panic(fmt.Errorf("cannot compile go-version constraint '%s' %s", constraint, err))
	}
	return verCheck
}

minKernelVer = "4.8.0"
isMinKernelVer = versioncheck.MustCompile("&gt;=" + minKernelVer)
if !isMinKernelVer(kernelVersion) {

}</pre>
<p>检查是否支持策略路由：</p>
<pre class="crayon-plain-tag">_, err = netlink.RuleList(netlink.FAMILY_V4)
//                该errno表示地址族不支持？  https://man7.org/linux/man-pages/man3/errno.3.html
if errors.Is(err, unix.EAFNOSUPPORT) {
	log.WithError(err).Error("Policy routing:NOT OK. " +
		"Please enable kernel configuration item CONFIG_IP_MULTIPLE_TABLES")
}</pre>
<p>检查是否支持IPv6：</p>
<pre class="crayon-plain-tag">if option.Config.EnableIPv6 {
	if _, err := os.Stat("/proc/net/if_inet6"); os.IsNotExist(err) {
		log.Fatalf("kernel: ipv6 is enabled in agent but ipv6 is either disabled or not compiled in the kernel")
	}
}</pre>
<p>查找clang二进制文件：</p>
<pre class="crayon-plain-tag">if filePath, err := exec.LookPath("clang"); err != nil {}</pre>
<p>调用bpftool进行特性探测：</p>
<pre class="crayon-plain-tag">probeManager := probes.NewProbeManager()
func NewProbeManager() *ProbeManager {
	newProbeManager := func() {
		probeManager = &amp;ProbeManager{}
		// 调用bpftool -j feature probe探测内核配置
		probeManager.features = probeManager.Probe()
	}
	// Do只会调用一次，这保证了全局变量probeManager不会被重复初始化
	once.Do(newProbeManager)
	return probeManager
}

//  判断Cilium必须的、可选的内核配置是否满足
if err := probeManager.SystemConfigProbes(); err != nil {
	errMsg := "BPF system config check: NOT OK."
	// TODO(brb) warn after GH#14314 has been resolved
	if !errors.Is(err, probes.ErrKernelConfigNotFound) {
		log.WithError(err).Warn(errMsg)
	}
}
func (p *ProbeManager) SystemConfigProbes() error {
	if !p.KernelConfigAvailable() {
		return ErrKernelConfigNotFound
	}
	requiredParams := p.GetRequiredConfig()
	for param, kernelOption := range requiredParams {
		if !kernelOption.Enabled {
			// err
		}
	}
	optionalParams := p.GetOptionalConfig()
	for param, kernelOption := range optionalParams {
		if !kernelOption.Enabled {
			// warn
		}
	}
	return nil
}
// 必须内核配置列表，大部分取决于cilium-agent的选项
func (p *ProbeManager) GetRequiredConfig() map[KernelParam]kernelOption {
	config := p.features.SystemConfig
	coreInfraDescription := "Essential eBPF infrastructure"
	kernelParams := make(map[KernelParam]kernelOption)

	kernelParams["CONFIG_BPF"] = kernelOption{
		Enabled:     config.ConfigBpf.Enabled(),
		Description: coreInfraDescription,
		CanBeModule: false,
	}
	kernelParams["CONFIG_BPF_SYSCALL"] = kernelOption{
		Enabled:     config.ConfigBpfSyscall.Enabled(),
		Description: coreInfraDescription,
		CanBeModule: false,
	}
	kernelParams["CONFIG_NET_SCH_INGRESS"] = kernelOption{
		Enabled:     config.ConfigNetSchIngress.Enabled() || config.ConfigNetSchIngress.Module(),
		Description: coreInfraDescription,
		CanBeModule: true,
	}
	kernelParams["CONFIG_NET_CLS_BPF"] = kernelOption{
		Enabled:     config.ConfigNetClsBpf.Enabled() || config.ConfigNetClsBpf.Module(),
		Description: coreInfraDescription,
		CanBeModule: true,
	}
	kernelParams["CONFIG_NET_CLS_ACT"] = kernelOption{
		Enabled:     config.ConfigNetClsAct.Enabled(),
		Description: coreInfraDescription,
		CanBeModule: false,
	}
	kernelParams["CONFIG_BPF_JIT"] = kernelOption{
		Enabled:     config.ConfigBpfJit.Enabled(),
		Description: coreInfraDescription,
		CanBeModule: false,
	}
	kernelParams["CONFIG_HAVE_EBPF_JIT"] = kernelOption{
		Enabled:     config.ConfigHaveEbpfJit.Enabled(),
		Description: coreInfraDescription,
		CanBeModule: false,
	}

	return kernelParams
}
// 可选内核配置列表
func (p *ProbeManager) GetOptionalConfig() map[KernelParam]kernelOption {
	config := p.features.SystemConfig
	kernelParams := make(map[KernelParam]kernelOption)

	kernelParams["CONFIG_CGROUP_BPF"] = kernelOption{
		Enabled:     config.ConfigCgroupBpf.Enabled(),
		Description: "Host Reachable Services and Sockmap optimization",
		CanBeModule: false,
	}
	kernelParams["CONFIG_LWTUNNEL_BPF"] = kernelOption{
		Enabled:     config.ConfigLwtunnelBpf.Enabled(),
		Description: "Lightweight Tunnel hook for IP-in-IP encapsulation",
		CanBeModule: false,
	}
	kernelParams["CONFIG_BPF_EVENTS"] = kernelOption{
		Enabled:     config.ConfigBpfEvents.Enabled(),
		Description: "Visibility and congestion management with datapath",
		CanBeModule: false,
	}

	return kernelParams
}

// 创建一个头文件 /var/run/cilium/state/globals/bpf_features.h 包含描述内核特性的宏
if err := probeManager.CreateHeadersFile(); err != nil {
	log.WithError(err).Fatal("BPF check: NOT OK.")
}
func (p *ProbeManager) CreateHeadersFile() error {
	// ...
	return p.writeHeaders(featuresFile);
}</pre>
<div class="blog_h3"><span class="graybg">挂载文件系统 </span></div>
<p>Cilium可能不在初始命名空间下运行，并且初始命名空间下的/sys/fs/bpf已经被挂载到命名空间的特定位置（option.Config.BPFRoot）。下面的调用检查BPF文件系统是否已经挂载，如果没有则挂载之：</p>
<pre class="crayon-plain-tag">func checkOrMountFS(bpfRoot string, printWarning bool) error {
	// 如果必要，进行挂载
	if bpfRoot == "" {
		checkOrMountDefaultLocations(printWarning)
	} else {
		checkOrMountCustomLocation(bpfRoot, printWarning)
	}
	// 确保没有重复挂载
	multipleMounts, err := hasMultipleMounts()
	if multipleMounts {
		return fmt.Errorf("multiple mount points detected at %s", mapRoot)
	}
	return nil
}

func checkOrMountDefaultLocations(printWarning bool) error {
	// 首先检查 /sys/fs/bpf 是否挂载了BPFFS
	mounted, bpffsInstance, err := mountinfo.IsMountFS(mountinfo.FilesystemTypeBPFFS, mapRoot)

	// 不是挂载点，则这里进行挂载
	if !mounted {
		mountFS(printWarning)
		return nil
	}
	// 挂载了，但是不是BPFFS。这意味着Cilium在容器中运行且宿主机/sys/fs/bpf没有挂载 （要避免这种情况！）
	// 这种情况下使用备用挂载点  /run/cilium/bpffs。此备用挂载点能够被Cilium使用
	// 但是会在Pod重启的时候导致umount，进而导致BPF Map（例如ct表）不可用，后果是
	// 所有到本地容器的连接被丢弃
	//
	//
	if !bpffsInstance {
		setMapRoot(defaults.DefaultMapRootFallback)

		cMounted, cBpffsInstance, err := mountinfo.IsMountFS(mountinfo.FilesystemTypeBPFFS, mapRoot)
		if !cMounted {
			if err := mountFS(printWarning); err != nil {
				return err
			}
		} else if !cBpffsInstance {
			log.Fatalf("%s is mounted but has a different filesystem than BPFFS", defaults.DefaultMapRootFallback)
		}
	}
	log.Infof("Detected mounted BPF filesystem at %s", mapRoot)
	return nil
}
func IsMountFS(mntType int64, path string) (bool, bool, error) {
	var st, pst unix.Stat_t
	// 类似于stat，但当path是符号连接的时候，查看的是链接自身（而非目标）的信息
	err := unix.Lstat(path, &amp;st)
	if err != nil {
		if errors.Is(err, unix.ENOENT) {
			// non-existent path can't be a mount point
			return false, false, nil
		}
		return false, false, &amp;os.PathError{Op: "lstat", Path: path, Err: err}
	}

	parent := filepath.Dir(path)
	err = unix.Lstat(parent, &amp;pst)
	if err != nil {
		return false, false, &amp;os.PathError{Op: "lstat", Path: parent, Err: err}
	}
	if st.Dev == pst.Dev {
		// 如果路径和父目录的设备一样，意味着它不是挂载点
		return false, false, nil
	}

	// 否则，获取文件系统信息
	fst := unix.Statfs_t{}
	err = unix.Statfs(path, &amp;fst)
	if err != nil {
		return true, false, &amp;os.PathError{Op: "statfs", Path: path, Err: err}
	}
	//           文件系统类型
	return true, fst.Type == mntType, nil

}
func mountFS(printWarning bool) error {
	// ...
	if err := unix.Mount(mapRoot, mapRoot, "bpf", 0, ""); err != nil {
		return fmt.Errorf("failed to mount %s: %s", mapRoot, err)
	}
	return nil
}

func hasMultipleMounts() (bool, error) {
	num := 0
	mountInfos, err := mountinfo.GetMountInfo()
	for _, mountInfo := range mountInfos {
		// 什么时候两个条目具有相同挂载点
		if mountInfo.Root == "/" &amp;&amp; mountInfo.MountPoint == mapRoot {
			num++
		}
	}
	return num &gt; 1, nil
}
// 读取/proc/self/mountinfo获取所有挂载信息
func GetMountInfo() ([]*MountInfo, error) {
	fMounts, err := os.Open(mountInfoFilepath)
	defer fMounts.Close()
	return parseMountInfoFile(fMounts)
}</pre>
<p>除了bpf，还需要检查/挂载cgroup2。和bpf不一样的是，存在多个cgroupv2的root mount是无害的，因此不会作重复挂载检查。</p>
<pre class="crayon-plain-tag">func CheckOrMountCgrpFS(mapRoot string) {
	cgrpMountOnce.Do(func() {
		if mapRoot == "" {
			mapRoot = cgroupRoot
		}
		cgrpCheckOrMountLocation(mapRoot)
	})
}
func cgrpCheckOrMountLocation(cgroupRoot string) error {
	setCgroupRoot(cgroupRoot)
	mounted, cgroupInstance, err := mountinfo.IsMountFS(mountinfo.FilesystemTypeCgroup2, cgroupRoot)
	if !mounted {
		return mountCgroup()
	} else if !cgroupInstance {
		return fmt.Errorf("Mount in the custom directory %s has a different filesystem than cgroup2", cgroupRoot)
	}
	return nil
}
func mountCgroup() error {
	unix.Mount("none", cgroupRoot, "cgroup2", 0, "") //...
}</pre>
<div class="blog_h3"><span class="graybg">其它选项处理</span></div>
<p>initEnv阶段会进行大量的选项校验、处理。这里列出一些比较重要的。</p>
<p>至少启用IPv4/IPv6之一：</p>
<pre class="crayon-plain-tag">if !option.Config.EnableIPv4 &amp;&amp; !option.Config.EnableIPv6 {
		log.Fatal("Either IPv4 or IPv6 addressing must be enabled")
	}</pre>
<p>和数据路径模式相关的判断逻辑：</p>
<pre class="crayon-plain-tag">switch option.Config.DatapathMode {
	case datapathOption.DatapathModeVeth:
		// 使用VETH数据路径时，不能配置IPVLAN master设备名，默认使用隧道模式，隧道默认使用VXLAN技术
		if name := viper.GetString(option.IpvlanMasterDevice); name != "undefined" {
			log.WithField(logfields.IpvlanMasterDevice, name).
				Fatal("ipvlan master device cannot be set in the 'veth' datapath mode")
		}
		if option.Config.Tunnel == "" {
			option.Config.Tunnel = option.TunnelVXLAN
		}
	case datapathOption.DatapathModeIpvlan:
		// 使用IPVLAN数据路径时，必须禁用隧道，不支持IPSec
		if option.Config.Tunnel != "" &amp;&amp; option.Config.Tunnel != option.TunnelDisabled {
			log.WithField(logfields.Tunnel, option.Config.Tunnel).
				Fatal("tunnel cannot be set in the 'ipvlan' datapath mode")
		}
		if len(option.Config.Devices) != 0 {
			log.WithField(logfields.Devices, option.Config.Devices).
				Fatal("device cannot be set in the 'ipvlan' datapath mode")
		}
		if option.Config.EnableIPSec {
			log.Fatal("Currently ipsec cannot be used in the 'ipvlan' datapath mode.")
		}

		option.Config.Tunnel = option.TunnelDisabled
		// 尽管IPVLAN模式不允许指定--device，但是后续逻辑都是通过option.Config.Devices保存设备名的，因此这里
		// 读取--ipvlan-master-device并存放在option.Config.Devices
		iface := viper.GetString(option.IpvlanMasterDevice)
		if iface == "undefined" {
			// 必须指定Master设备名
			log.WithField(logfields.IpvlanMasterDevice, option.Config.Devices[0]).
				Fatal("ipvlan master device must be specified in the 'ipvlan' datapath mode")
		}
		option.Config.Devices = []string{iface}
		link, err := netlink.LinkByName(option.Config.Devices[0])
		if err != nil {
			log.WithError(err).WithField(logfields.IpvlanMasterDevice, option.Config.Devices[0]).
				Fatal("Cannot find device interface")
		}
		option.Config.Ipvlan.MasterDeviceIndex = link.Attrs().Index
		option.Config.Ipvlan.OperationMode = connector.OperationModeL3
		if option.Config.InstallIptRules {
			option.Config.Ipvlan.OperationMode = connector.OperationModeL3S
		} else {
			log.WithFields(logrus.Fields{
				logfields.URL: "https://github.com/cilium/cilium/issues/12879",
			}).Warn("IPtables rule configuration has been disabled. This may affect policy and forwarding, see the URL for more details.")
		}
	case datapathOption.DatapathModeLBOnly:
		// 仅LB模式
		log.Info("Running in LB-only mode")
		option.Config.LoadBalancerPMTUDiscovery =
			option.Config.NodePortAcceleration != option.NodePortAccelerationDisabled
		option.Config.KubeProxyReplacement = option.KubeProxyReplacementPartial
		option.Config.EnableHostReachableServices = true
		option.Config.EnableHostPort = false
		option.Config.EnableNodePort = true
		option.Config.EnableExternalIPs = true
		option.Config.Tunnel = option.TunnelDisabled
		option.Config.EnableHealthChecking = false
		option.Config.EnableIPv4Masquerade = false
		option.Config.EnableIPv6Masquerade = false
		option.Config.InstallIptRules = false
		option.Config.EnableL7Proxy = false
	default:
		log.WithField(logfields.DatapathMode, option.Config.DatapathMode).Fatal("Invalid datapath mode")
	}</pre>
<p> 要支持L7策略，必须允许安装iptables规则：</p>
<pre class="crayon-plain-tag">if option.Config.EnableL7Proxy &amp;&amp; !option.Config.InstallIptRules {
		log.Fatal("L7 proxy requires iptables rules (--install-iptables-rules=\"true\")")
	}</pre>
<p>IPSec + 隧道组合，需要4.19+内核：</p>
<pre class="crayon-plain-tag">if option.Config.EnableIPSec &amp;&amp; option.Config.Tunnel != option.TunnelDisabled {
		if err := ipsec.ProbeXfrmStateOutputMask(); err != nil {
			log.WithError(err).Fatal("IPSec with tunneling requires support for xfrm state output masks (Linux 4.19 or later).")
		}
	}</pre>
<p>如果要求Cilium安装iptables规则install-no-conntrack-iptables-rules，让所有Pod流量跳过netfilter的conntrack，则必须使用直接路由模式。因为隧道模式下，外层封包已经自动跳过conntrack：</p>
<pre class="crayon-plain-tag">if option.Config.InstallNoConntrackIptRules {
		if option.Config.Tunnel != option.TunnelDisabled {
			log.Fatalf("%s requires the agent to run in direct routing mode.", option.InstallNoConntrackIptRules)
		}

		// 此外，跳过conntrack必须和IPv4一起使用，原因是用于匹配PodCIDR的是native routing CIDR
		// 此CIDR目前仅支持IPv4
		if !option.Config.EnableIPv4 {
			log.Fatalf("%s requires IPv4 support.", option.InstallNoConntrackIptRules)
		}
	}</pre>
<p>使用隧道时，不能使用直接路由相关的选项。</p>
<p>auto-direct-node-routes：（L2模式下）将直接路由通过给其它节点</p>
<pre class="crayon-plain-tag">if option.Config.Tunnel != option.TunnelDisabled &amp;&amp; option.Config.EnableAutoDirectRouting {
		log.Fatalf("%s cannot be used with tunneling. Packets must be routed through the tunnel device.", option.EnableAutoDirectRoutingName)
	}</pre>
<div class="blog_h2"><span class="graybg">运行cilium-agent</span></div>
<div class="blog_h3"><span class="graybg">整体逻辑</span></div>
<p>runDaemon</p>
<p style="padding-left: 30px;">enableIPForwarding  启用IPv4/IPv6转发</p>
<p style="padding-left: 30px;">iptablesManager.Init  初始化iptables管理器，检查相关内核模块是否可用</p>
<p style="padding-left: 30px;">k8s.Init  初始化各种K8S客户端对象</p>
<p style="padding-left: 30px;">NewDaemon 创建新的cilium-agent守护进程实例</p>
<p style="padding-left: 60px;">WithDefaultEndpointManager 创建端点管理器，负责从底层数据源同步端点信息</p>
<div class="blog_h3"><span class="graybg"> 设置宿主机设备</span></div>
<pre class="crayon-plain-tag">func runDaemon() {
	datapathConfig := linuxdatapath.DatapathConfiguration{
		HostDevice: option.Config.HostDevice,
}</pre>
<p>使用veth数据路径时，默认使用的宿主机设备是cilium_host，它是一个veth，对端也在初始命名空间中，是cilium_net。 </p>
<p>直接路由模式下，从路由上看，发往PodCIDR的流量都是从cilium_host出去：</p>
<pre class="crayon-plain-tag">172.27.2.0      172.27.2.122    255.255.255.0   UG    0      0        0 cilium_host
172.27.2.122    0.0.0.0         255.255.255.255 UH    0      0        0 cilium_host</pre>
<p>但是却找不到什么规则将其发送给Pod的veth（位于初始命名空间的一端）也就是lxc***网卡。</p>
<p>实际上，转发操作是通过挂钩在cilium_host中的BPF程序进行redirect实现的。类似的难以用传统Linux网络拓扑思维难以理解的地方会有很多。</p>
<div class="blog_h3"><span class="graybg">启用IP转发</span></div>
<pre class="crayon-plain-tag">if err := enableIPForwarding(); err != nil {
		log.WithError(err).Fatal("Error when enabling sysctl parameters")
	}

// ...
func enableIPForwarding() error {
	if err := sysctl.Enable("net.ipv4.ip_forward"); err != nil {
		return err
	}
	if err := sysctl.Enable("net.ipv4.conf.all.forwarding"); err != nil {
		return err
	}
	if option.Config.EnableIPv6 {
		if err := sysctl.Enable("net.ipv6.conf.all.forwarding"); err != nil {
			return err
		}
	}
	return nil
}</pre>
<div class="blog_h3"><span class="graybg">初始化iptables管理器</span></div>
<pre class="crayon-plain-tag">iptablesManager := &amp;iptables.IptablesManager{}
	iptablesManager.Init()

// ...

unc (m *IptablesManager) Init() {
	modulesManager := &amp;modules.ModulesManager{}
	haveIp6tables := true
	// 内核模块管理器，读取/proc/modules中的模块信息
	modulesManager.Init()
	// 确保模块加载
	modulesManager.FindOrLoadModules(
		"ip_tables", "iptable_nat", "iptable_mangle", "iptable_raw",
		"iptable_filter");
	modulesManager.FindOrLoadModules(
		"ip6_tables", "ip6table_mangle", "ip6table_raw", "ip6table_filter")

	if err := modulesManager.FindOrLoadModules("xt_socket"); err != nil {
		if option.Config.Tunnel == option.TunnelDisabled {
			// xt_socket执行一个local socket match(根据封包进行套接字查找，匹配应该本地处理的封包)，并且
			// 设置skb mark，这样封包将被Cilium的策略路由导向本地网络栈，不会被ip_forward()处理
			//
			// 如果xt_socket模块不存在，那么可以禁用ip_early_demux，避免在ip_forward()中明确的drop
			// 在隧道模式下可以不需要ip_early_demux，因为我们可以在BPF逻辑中设置skb mark，这个BPF也是
			// 早于策略路由阶段的，可以保证封包被导向本地网络栈，不会ip_forward()转发
			// 
			// 如果对于任何场景，我们都能保证在封包到达策略路由之前，被设置好“to proxy” 这个skb mark
			// 则可以不需要xt_socket模块。目前对于endpoint routing mode，无法满足这一点
			log.WithError(err).Warning("xt_socket kernel module could not be loaded")

			if option.Config.EnableXTSocketFallback {
				v4disabled := true
				v6disabled := true
				if option.Config.EnableIPv4 {
					v4disabled = sysctl.Disable("net.ipv4.ip_early_demux") == nil
				}
				if option.Config.EnableIPv6 {
					v6disabled = sysctl.Disable("net.ipv6.ip_early_demux") == nil
				}
				if v4disabled &amp;&amp; v6disabled {
					m.ipEarlyDemuxDisabled = true
					log.Warning("Disabled ip_early_demux to allow proxy redirection with 
                                               original source/destination address without xt_socket support 
                                               also in non-tunneled datapath modes.")
				} else {
					log.WithError(err).Warning("Could not disable ip_early_demux, traffic
                                           redirected due to an HTTP policy or visibility may be dropped unexpectedly")
				}
			}
		}
	} else {
		m.haveSocketMatch = true
	}
	m.haveBPFSocketAssign = option.Config.EnableBPFTProxy

	v, err := ip4tables.getVersion()
	if err == nil {
		switch {
		case isWaitSecondsMinVersion(v):
			m.waitArgs = []string{waitString, fmt.Sprintf("%d", option.Config.IPTablesLockTimeout/time.Second)}
		case isWaitMinVersion(v):
			m.waitArgs = []string{waitString}
		}
	}
}</pre>
<p>封包到达内核之后，内核需要知道如何路由它。为了避免反复查找路由表，Linux出现过很多路由缓存机制。从3.6开始，Linux废弃了全局路由缓存，由各子系统（例如TCP协议栈）负责维护路由缓存。</p>
<p>在套接字级别，有一个dst字段作为路由缓存。当一个TCP连接最初建立时，此字段为空，随后dst被填充，该套接字的生命周期内，后续到来的skb都不再需要查找路由。</p>
<p>ip_early_demux是一个内核特性。它向（网络栈的）下优化某些类型的本地套接字（目前是已建立的TCP套接字）输入封包的处理，它的工作原理是：</p>
<ol>
<li>内核接收到封包后，需要查找skb对应的路由，然后查找skb对应的socket</li>
<li>这里存在一种浪费，对应已建立的连接，属于同一socket的skb的路由是一致的</li>
<li>如果能将路由信息缓存到套接字上，那么就可以避免为每个skb查找路由</li>
</ol>
<p>问题在于，对于主要（60%+流量）作为路由器使用的Linux系统，它会引入不必要的吞吐量下降，可以禁用。</p>
<p>回到Cilium的场景，使用HTTP策略时，需要将流量重定向给Envoy，同时保持源、目的IP地址不变。这个重定向是依赖于xt_socket模块的。如果xt_socket不可用则需要禁用ip_early_demux，否则会导致原本应该发给Envoy的流量被转发走。</p>
<div class="blog_h3"><span class="graybg">初始化K8S</span></div>
<pre class="crayon-plain-tag">func Init(conf k8sconfig.Configuration) error {
	// 创建K8S核心API客户端
	k8sRestClient, closeAllDefaultClientConns, err := createDefaultClient()
	if err != nil {
		return fmt.Errorf("unable to create k8s client: %s", err)
	}
	// 创建Cilium客户端
	closeAllCiliumClientConns, err := createDefaultCiliumClient()
	if err != nil {
		return fmt.Errorf("unable to create cilium k8s client: %s", err)
	}
	// 创建API Extensions客户端
	if err := createAPIExtensionsClient(); err != nil {
		return fmt.Errorf("unable to create k8s apiextensions client: %s", err)
	}

	// 心跳函数
	heartBeat := func(ctx context.Context) error {
		// Kubernetes默认的心跳是获取所在节点的信息
		// [0] https://github.com/kubernetes/kubernetes/blob/v1.17.3/pkg/kubelet/kubelet_node_status.go#L423
		// 这对于Cilium来说太重，因此这里的心跳采用检查/healthz端点的方式
		res := k8sRestClient.Get().Resource("healthz").Do(ctx)
		return res.Error()
	}
	
	// 对K8S进行心跳检测
	if option.Config.K8sHeartbeatTimeout != 0 {
		controller.NewManager().UpdateController("k8s-heartbeat",
			controller.ControllerParams{
				DoFunc: func(context.Context) error {
					runHeartbeat(
						heartBeat, // 心跳函数
						option.Config.K8sHeartbeatTimeout, // 超时
						// 后面的是超时后的回调，这里是关闭客户端连接
						closeAllDefaultClientConns,
						closeAllCiliumClientConns,
					)
					return nil
				},
				// 心跳运行间隔
				RunInterval: option.Config.K8sHeartbeatTimeout,
			},
		)
	}
	// 获取K8S版本进而推导具有哪些特性
	if err := k8sversion.Update(Client(), conf); err != nil {
		return err
	}
	if !k8sversion.Capabilities().MinimalVersionMet {
		return fmt.Errorf("k8s version (%v) is not meeting the minimal requirement (%v)",
			k8sversion.Version(), k8sversion.MinimalVersionConstraint)
	}

	return nil
}

// 创建Cilium客户端
import 	clientset "github.com/cilium/cilium/pkg/k8s/client/clientset/versioned"
func createDefaultCiliumClient() (func(), error) {
	restConfig, err := CreateConfig()
	closeAllConns := setDialer(restConfig)
	createdCiliumK8sClient, err := clientset.NewForConfig(restConfig)
	k8sCiliumCLI.Interface = createdCiliumK8sClient
	return closeAllConns, nil
}
// 为 rest.Config设置Dial，一个负责拨号（创建TCP连接）的函数
func setDialer(config *rest.Config) func() {
	if option.Config.K8sHeartbeatTimeout == 0 {
		return func() {}
	}
	ctx := (&amp;net.Dialer{
		Timeout:   option.Config.K8sHeartbeatTimeout,
		KeepAlive: option.Config.K8sHeartbeatTimeout,
	}).DialContext
	dialer := connrotation.NewDialer(ctx)
	// 拨号器的DialContext用于创建连接，CloseAll用于关闭它创建的所有连接
	// 这个关闭函数返回，并在心跳失败时调用以关闭连接
	config.Dial = dialer.DialContext
	return dialer.CloseAll
}</pre>
<p>&nbsp;</p>
<div class="blog_h1"><span class="graybg">常见问题</span></div>
<div class="blog_h2"><span class="graybg">零散问题</span></div>
<div class="blog_h3"><span class="graybg">native routing cidr must be configured with option --native-routing-cidr in combination with --masquerade --tunnel=disabled</span></div>
<p>禁用隧道，使用直接路由时，必须指定native-routing-cidr选项（对应Helm值nativeRoutingCIDR）。该选项必须设置为PodCIDR，也就是Kubernetes的--cluster-cidr。该选项提示Cilium直接路由的目的地址范围。</p>
<div class="blog_h3"><span class="graybg">Failed to compile XDP program" error="Failed to load prog with ip cilium</span></div>
<p>可能提示不支持XDP，禁用<pre class="crayon-plain-tag">--set loadBalancer.acceleration=disabled</pre>。</p>
<div class="blog_h2"><span class="graybg">cilium status提示端点不通</span></div>
<p>异常时输出示例：</p>
<pre class="crayon-plain-tag">kubectl -n kube-system exec cilium-4bsb6 -- cilium status
# ...
# Cluster health:           0/3 reachable   (2021-07-02T04:06:04Z)
#   Name                    IP              Node          Endpoints
#   k8s/k8s-3 (localhost)   10.0.3.3        reachable     unreachable
#   k8s/k8s-1               10.0.3.1        reachable     unreachable
#   k8s/k8s-2               10.0.3.2        unreachable   unreachable</pre>
<p>正常时输出示例：</p>
<pre class="crayon-plain-tag"># Cluster health:         3/3 reachable   (2021-07-02T04:09:19Z)</pre>
<p>任何导致网络不通的故障，都会出现类似报错。</p>
<div class="blog_h3"><span class="graybg">禁用隧道但未同步路由</span></div>
<p>配置<pre class="crayon-plain-tag">--set tunnel=disabled</pre>以使用直接路由模式后，容器网络封包不再使用VXLAN包装，而是直接在集群节点之间路由。</p>
<p>这需要集群节点之间有正确的路由规则：</p>
<ol>
<li>如果节点是L2直连的，应当设置<pre class="crayon-plain-tag">--set autoDirectNodeRoutes=true</pre>。这样，路由规则会直接安装到节点路由表中，例如：<br />
<pre class="crayon-plain-tag">route -n
Kernel IP routing table
Destination     Gateway         Genmask         Flags Metric Ref    Use Iface
# 下面的两个PodCIDR的节点CIDR，通过以太网路由给其它节点
172.27.0.0      10.0.3.2        255.255.255.0   UG    0      0        0 eth0
172.27.1.0      10.0.3.3        255.255.255.0   UG    0      0        0 eth0
# 下面是本节点的CIDR，交给cilium_host虚拟设备处理
172.27.2.0      172.27.2.122    255.255.255.0   UG    0      0        0 cilium_host
172.27.2.122    0.0.0.0         255.255.255.255 UH    0      0        0 cilium_host</pre>
</li>
<li>如果节点是L3互联的，则路由器需要和Cilium进行某种形式的集成。例如通过BGP协议，或者使用crd这个IPAM，静态规定每个节点的CIDR，然后更新路由器的路由规则</li>
</ol>
<div class="blog_h3"><span class="graybg">IaaS层配置异常</span></div>
<p>如果节点是虚拟机，则要考虑IaaS层的网络设置。例如OpenStack可能需要为Port配置Allowed Address Pairs，将PodCIDR加入其中。</p>
<div class="blog_h2"><span class="graybg">DSR模式下无法从集群外部访问LB服务</span></div>
<p>LoadBalancer服务的IP地址为：10.0.11.20，服务端点IP地址为：172.28.1.76。流量入口节点为m1，服务端点所在节点为m3。</p>
<p>在m1上通过tcpdump抓包：</p>
<pre class="crayon-plain-tag"># tcpdump -i any -nnn -vvv 'dst 172.28.1.76'
15:21:02.310338 IP (tos 0x10, ttl 64, id 58773, offset 0, flags [DF], proto TCP (6), length 68, options (unknown 154))
    10.2.0.1.36568 &gt; 172.28.1.76.2379: Flags [S], cksum 0xb799 (incorrect -&gt; 0x65e9), seq 2591998745, win 64240, options [mss 1460,sackOK,TS val 899286115 ecr 0,nop,wscale 9], length 0</pre>
<p>可以看到，在入口节点m1，已经尝试将请求直接转发给位于m3的172.28.1.76.2379。携带了IP选项154。但是，在m3上抓包，没有任何相关信息。</p>
<p>禁用DSR后，两个节点抓包可以发现通信正常：</p>
<pre class="crayon-plain-tag"># m1
15:36:13.301471 IP (tos 0x10, ttl 64, id 47272, offset 0, flags [DF], proto TCP (6), length 57)
    10.2.0.61.38732 &gt; 172.28.1.76.2379: Flags [P.], cksum 0xb7d2 (incorrect -&gt; 0x5309), seq 1086727409:1086727414, ack 3915217720, win 126, options [nop,nop,TS val 900197106 ecr 488619248], length 5

# m3
15:36:13.301825 IP (tos 0x10, ttl 64, id 47272, offset 0, flags [DF], proto TCP (6), length 57)
    10.2.0.61.38732 &gt; 172.28.1.76.2379: Flags [P.], cksum 0xb7d2 (incorrect -&gt; 0x5309), seq 1086727409:1086727414, ack 3915217720, win 126, options [nop,nop,TS val 900197106 ecr 488619248], length 5</pre>
<p>差别在于源地址、IP选项不同。考虑是OpenStack源地址检查的原因。增加Allowed Address Pairs：10.0.0.0/8，问题解决。</p>
</div><p>The post <a rel="nofollow" href="https://blog.gmem.cc/cilium">Cilium学习笔记</a> appeared first on <a rel="nofollow" href="https://blog.gmem.cc">绿色记忆</a>.</p>
]]></content:encoded>
			<wfw:commentRss>https://blog.gmem.cc/cilium/feed</wfw:commentRss>
		<slash:comments>2</slash:comments>
		</item>
		<item>
		<title>基于BCC进行性能追踪</title>
		<link>https://blog.gmem.cc/bcc</link>
		<comments>https://blog.gmem.cc/bcc#comments</comments>
		<pubDate>Fri, 07 Jun 2019 07:33:11 +0000</pubDate>
		<dc:creator><![CDATA[Alex]]></dc:creator>
				<category><![CDATA[Linux]]></category>
		<category><![CDATA[eBPF]]></category>

		<guid isPermaLink="false">https://blog.gmem.cc/?p=37315</guid>
		<description><![CDATA[<p>简介 BCC是一个工具包，用来创建高效的、在内核中运行的性能追踪程序。它首先是一个编译器，能够将你写的代码编译为eBPF程序，此外，它还提供了大量有用的工具和样例 BCC的基础是eBPF，因此其大部分功能需要内核版本在4.1+才能使用，其提供的某些根据甚至需要4.4或4.6版本的内核。要编译和运行BCC程序，你除了需要安装bcc之外，还需要llvm/clang，以及内核头文件。 BCC的支持内核中多种追踪机制，包括kprobe、uprobe、tracepoint、USDT（User-level statically defined tracing probes ），能够监控块设备IO、TCP函数、文件系统操作、任何syscall、Node.js/Java/MySQL/libc等多种常用软件/平台定义的USDT。 在网络监控方面，BCC提供了分布式网桥、HTTP过滤器、快速封包丢弃、隧道监控等样例代码。 BCC通过以下途径让eBPF开发变得简单： 在C中使用kernel instrumentation，提供了LLVM的一个C wrapper 提供Python和Lua的前端 安装 内核配置 要使用BCC，你通常需要4.1或者更新版本的内核，并且启用以下内核配置： [crayon-69d5411550d48871516707/] 安装软件包 [crayon-69d5411550d4f673517505/] 从源码构建 <a class="read-more" href="https://blog.gmem.cc/bcc">[...]</a></p>
<p>The post <a rel="nofollow" href="https://blog.gmem.cc/bcc">基于BCC进行性能追踪</a> appeared first on <a rel="nofollow" href="https://blog.gmem.cc">绿色记忆</a>.</p>
]]></description>
				<content:encoded><![CDATA[<div class="wri_content_clear_both"><div class="blog_h1"><span class="graybg">简介</span></div>
<p>BCC是一个工具包，用来创建高效的、在内核中运行的性能追踪程序。它首先是一个编译器，能够将你写的代码编译为eBPF程序，此外，它还提供了大量有用的工具和样例</p>
<p>BCC的基础是eBPF，因此其大部分功能需要内核版本在4.1+才能使用，其提供的某些根据甚至需要4.4或4.6版本的内核。要编译和运行BCC程序，你除了需要安装bcc之外，还需要llvm/clang，以及内核头文件。</p>
<p>BCC的支持内核中多种追踪机制，包括kprobe、uprobe、tracepoint、USDT（User-level statically defined tracing probes ），能够监控块设备IO、TCP函数、文件系统操作、任何syscall、Node.js/Java/MySQL/libc等多种常用软件/平台定义的USDT。</p>
<p>在网络监控方面，BCC提供了分布式网桥、HTTP过滤器、快速封包丢弃、隧道监控等样例代码。</p>
<p>BCC通过以下途径让eBPF开发变得简单：</p>
<ol>
<li>在C中使用kernel instrumentation，提供了LLVM的一个C wrapper</li>
<li>提供Python和Lua的前端</li>
</ol>
<div class="blog_h1"><span class="graybg">安装</span></div>
<div class="blog_h2"><span class="graybg">内核配置</span></div>
<p>要使用BCC，你通常需要4.1或者更新版本的内核，并且启用以下内核配置：</p>
<pre class="crayon-plain-tag">CONFIG_BPF=y
CONFIG_BPF_SYSCALL=y
# [optional, for tc filters]
CONFIG_NET_CLS_BPF=m
# [optional, for tc actions]
CONFIG_NET_ACT_BPF=m
CONFIG_BPF_JIT=y
# [for Linux kernel versions 4.1 through 4.6]
CONFIG_HAVE_BPF_JIT=y
# [for Linux kernel versions 4.7 and later]
CONFIG_HAVE_EBPF_JIT=y
# [optional, for kprobes]
CONFIG_BPF_EVENTS=y
# Need kernel headers through /sys/kernel/kheaders.tar.xz
CONFIG_IKHEADERS=y</pre>
<div class="blog_h2"><span class="graybg">安装软件包</span></div>
<pre class="crayon-plain-tag"># Ubuntu
sudo apt-get install bpfcc-tools linux-headers-$(uname -r)
# CentOS
yum install bcc-tools</pre>
<div class="blog_h2"><span class="graybg">从源码构建</span></div>
<p>从0.10.0开始，BCC依赖libbpf，后者提供对BPF系统调用的包装函数，提供bpf.h / btf.h的UAPI头文件。</p>
<p>安装构建BCC需要的依赖，以及相关工具：</p>
<pre class="crayon-plain-tag">apt install -y bison build-essential cmake flex git libedit-dev \
    python3-distutils  llvm clang llvm-dev libclang-dev python zlib1g-dev libelf-dev libfl-dev

apt install arping netperf iperf3
ln -s `which python3` /usr/bin/python</pre>
<p>编译并安装BCC：</p>
<pre class="crayon-plain-tag">git clone https://github.com/iovisor/bcc.git
mkdir bcc/build; cd bcc/build
cmake ..
make
sudo make install


cmake -DPYTHON_CMD=python3 .. # build python3 binding
pushd src/python/
make
sudo make install</pre>
<div class="blog_h3"><span class="graybg">环境变量</span></div>
<p>BCC自带的工具位于<pre class="crayon-plain-tag">/usr/share/bcc/tools</pre>下，需要将其加入到PATH环境变量。</p>
<div class="blog_h1"><span class="graybg">工具</span></div>
<p>我们可以先了解以下BCC提供的各种工具，这些工具可以直接用来解决性能追踪等问题，也是发明新轮子的重要参考。</p>
<p>下图显示了工作在系统各层次的工具列表：</p>
<p><a href="https://blog.gmem.cc/wp-content/uploads/2019/06/bcc_tracing_tools_2019.png"><img class="size-large wp-image-37455 aligncenter" src="https://blog.gmem.cc/wp-content/uploads/2019/06/bcc_tracing_tools_2019-1024x613.png" alt="bcc_tracing_tools_2019" width="710" height="425" /></a></p>
<div class="blog_h2"><span class="graybg">非BCC工具</span></div>
<p>在使用BCC之前，我们有必要回顾以下常用的Linux性能分析工具。</p>
<div class="blog_h3"><span class="graybg">uptime</span></div>
<pre class="crayon-plain-tag">uptime 
23:51:26 up 21:31, 1 user, load average: 30.02, 26.43, 19.02</pre>
<p>这个命令可以查看系统启动了多久，还可以快速查看工作负载情况。最后的三个数字分别是最近1/5/15分钟的、基于指数衰减移动和平均（exponentially damped moving sum averages）算法的工作负载信息。如果你发现第一个数字比第二个小很多，可能提示你错过了高负载时刻。</p>
<p>工作负载提示了有多少进程想要运行（去获得CPU）：包括积极等待被调度的进程，以及被不可中断睡眠（通常是块IO）阻塞的进程。</p>
<div class="blog_h3"><span class="graybg">dmesg</span></div>
<p>内核在重大事件发生后，都会输出信息到日志环：</p>
<pre class="crayon-plain-tag">dmesg | tail
# [1880957.563150] perl invoked oom-killer: gfp_mask=0x280da, order=0, oom_score_adj=0
# [1880957.563400] Out of memory: Kill process 18694 (perl) score 246 or sacrifice child
# [1880957.563408] Killed process 18694 (perl) total-vm:1972392kB, anon-rss:1953348kB, file-rss:0kB
# [2320864.954447] TCP: Possible SYN flooding on port 7001. Dropping request.  Check SNMP counters.</pre>
<p>系统工作不正常是，有必要观察这些日志。</p>
<div class="blog_h3"><span class="graybg">vmstat</span></div>
<pre class="crayon-plain-tag">vmstat 1
# procs -----------memory---------- ---swap-- -----io---- -system-- ------cpu-----
#  r  b   swpd   free   buff  cache   si   so    bi    bo   in   cs us sy id wa st
#  1  0      0 35469380 1681776 43056984    0    0   673   552  174  706 17  8 74  1  0
#  4  0      0 35468148 1681776 43059452    0    0    12  3480 21357 44556  8  3 88  0  0
# 10  0      0 35467088 1681784 43059824    0    0    20  3212 23257 45228 14  5 81  0  0</pre>
<p>这个工具给出系统的各种宏观统计信息：</p>
<ol>
<li><pre class="crayon-plain-tag">r</pre>等待获得运行机会的CPU数量，比起load average，更明确的显示了CPU饱和度，因为它没有包含I/O，如果数量大于CPU核心，说明饱和了</li>
<li><pre class="crayon-plain-tag">free</pre> 空闲内存KB</li>
<li><pre class="crayon-plain-tag">si</pre>/<pre class="crayon-plain-tag">so</pre> 换入/换出的页数量，如果非零提示内存不足</li>
<li><pre class="crayon-plain-tag">us</pre>, <pre class="crayon-plain-tag">sy</pre>, <pre class="crayon-plain-tag">id</pre>, <pre class="crayon-plain-tag">wa</pre>, <pre class="crayon-plain-tag">st</pre> 处理器时间的分解：用户空间耗时，内核空间耗时，空闲时间，等待IO时间，被偷走的时间（虚拟化环境下被其它客户机偷走使用的）
<ol>
<li>如果us+sy高提示CPU繁忙、wa高则提示IO瓶颈</li>
<li>系统需要消耗sy时间来处理IO，如果sy时间总是很高，例如占比20%+，可能提示内核处理IO效率较差</li>
<li>CPU利用90%+不一定说明CPU饱和，查看r列更靠谱</li>
</ol>
</li>
</ol>
<div class="blog_h3"><span class="graybg">mpstat</span></div>
<p>这个命令可以分别查看每个CPU核心的使用情况：</p>
<pre class="crayon-plain-tag">mpstat -P ALL 1                                                                                                                                                                          
# Linux 5.8.0-55-generic (zircon)         06/11/2021      _x86_64_        (16 CPU)
# 09:14:03 PM  CPU    %usr   %nice    %sys %iowait    %irq   %soft  %steal  %guest  %gnice   %idle
# 09:14:04 PM  all    2.95    0.00   10.39    0.80    0.00    0.00    0.00    0.68    0.00   85.18
# 09:14:04 PM    0    5.00    0.00    5.00    0.00    0.00    0.00    0.00    3.00    0.00   87.00
# 09:14:04 PM    1    6.06    0.00    5.05    0.00    0.00    0.00    0.00    0.00    0.00   88.89</pre>
<p>能够用来发现CPU负载不均衡的情况。单个热点CPU，提示存在一个忙的单线程应用程序。</p>
<div class="blog_h3"><span class="graybg">pidstat</span></div>
<pre class="crayon-plain-tag">pidstat 1
# Linux 5.8.0-55-generic (zircon)         06/11/2021      _x86_64_        (16 CPU)

# 09:17:58 PM   UID       PID    %usr %system  %guest   %wait    %CPU   CPU  Command
# 09:17:59 PM  1000      2736    4.95    1.98    0.00    0.00    6.93     6  gnome-shell
# 09:17:59 PM  1000      6916    0.99    0.00    0.00    0.00    0.99    11  gnome-terminal-
# 09:17:59 PM  1000      7380    8.91   51.49    0.00    0.00   60.40     4  vmware-vmx</pre>
<p>类似于top，但是滚动更新，有助于录制和事后分析或者发现特定的pattern。</p>
<div class="blog_h3"><span class="graybg"> iostat</span></div>
<pre class="crayon-plain-tag">iostat -x 1</pre>
<p>用于了解块设备的工作情况：</p>
<ol>
<li> <pre class="crayon-plain-tag">r/s</pre>, <pre class="crayon-plain-tag">w/s</pre>, <pre class="crayon-plain-tag">rkB/s</pre>, <pre class="crayon-plain-tag">wkB/s</pre>：读写的次数、读写的字节数</li>
<li><pre class="crayon-plain-tag">await</pre>：等待IO完成的平均ms</li>
<li><pre class="crayon-plain-tag">avgqu-sz</pre>：平均发送给目标设备的请求数量，数量大于1提示饱和（尽管设备通常能够并行处理请求，特别是由多个后端磁盘组成的虚拟设备）</li>
<li><pre class="crayon-plain-tag">%util</pre>：提示设备有多少时间百分比是忙着的。尽管取决于设备，大于60%通常会导致低性能。接近100%通常提示设备饱和。但是，对于多个后端磁盘组成的虚拟设备，100%仅仅提示所有时间都有后端磁盘在工作，但是可能远远没有饱和。</li>
</ol>
<p>需要注意，很多应用程序都使用异步IO技术，这样即使设备饱和，应用程序也不会出现直接的（可被预读、缓冲写缓和）延迟。</p>
<div class="blog_h3"><span class="graybg">free</span></div>
<pre class="crayon-plain-tag">free -m
#               total        used        free      shared  buff/cache   available
# Mem:         128677        6601       77687       12528       44389      108334
# Swap:         62499           0       62499</pre>
<div class="blog_h3"><span class="graybg">sar</span></div>
<p>需要安装下面的软件：</p>
<pre class="crayon-plain-tag">apt install sysstat</pre>
<p>修改配置文件：</p>
<pre class="crayon-plain-tag">ENABLED="true"</pre>
<p>并重启服务：</p>
<pre class="crayon-plain-tag">systemctl restart sysstat.service</pre>
<p>查看网络接口的吞吐情况：</p>
<pre class="crayon-plain-tag">sar -n DEV 1

# 接口     收包数    发包数      收KB      发KB                                   网卡利用率
  IFACE   rxpck/s   txpck/s    rxkB/s    txkB/s   rxcmp/s   txcmp/s  rxmcst/s   %ifutil
  lo      0.00      0.00      0.00      0.00      0.00      0.00      0.00      0.00</pre>
<p>查看TCP统计信息：</p>
<pre class="crayon-plain-tag">sar -n TCP,ETCP 1

#            本地主动发起连接数量
#                     本地被动accept连接数量
#                                  
09:38:04 PM  active/s passive/s    iseg/s    oseg/s
09:38:05 PM      0.00      0.00      5.00      5.00

#                               重传次数
09:39:41 PM  atmptf/s  estres/s retrans/s isegerr/s   orsts/s
09:39:42 PM      0.00      0.00      0.00      0.00      0.00</pre>
<div class="blog_h2"><span class="graybg">可观察性工具</span></div>
<div class="blog_h3"><span class="graybg">execsnoop</span></div>
<p>这个工具可以探测新<pre class="crayon-plain-tag">exec()</pre>的进程，注意它不会追踪<pre class="crayon-plain-tag">fork()</pre>因而不能看到所有的进程。</p>
<p>该工具可用于发现短暂存在的、消耗CPU的进程，这些进程可能难以在周期性的、快照式的监控工具（例如top）中出现。</p>
<div class="blog_h3"><span class="graybg">opensnoop</span></div>
<p>这个工具可以探测<pre class="crayon-plain-tag">open()</pre>系统调用，从而推测应用程序的工作方式（会使用哪些数据文件、配置文件、日志文件）。某些时候，应用程序因为反复尝试打开不存在的文件，或者反复写入大量数据到文件，而性能低下，该工具也可以发现这类场景。</p>
<div class="blog_h3"><span class="graybg">ext4slower</span></div>
<p>类似的工具还包括<pre class="crayon-plain-tag">btrfsslower</pre>, <pre class="crayon-plain-tag">xfsslower</pre>, <pre class="crayon-plain-tag">zfsslower</pre>。</p>
<p>这些工具可以探测单个的、缓慢的文件系统操作。由于磁盘是异步处理IO的，难以将块设备层的延迟和应用程序的延迟关联起来。在VFS/文件系统接口层进行探测，更加接近应用程序，延迟相关的可能性也更大。</p>
<div class="blog_h3"><span class="graybg">biolatency</span></div>
<p>跟踪此片IO延迟（从向设备发起请求到处理完毕），退出此工具时会绘制直方图。</p>
<p>iostat之类工具能给出平均延迟，此工具则通过直方图显示延迟的分布情况。</p>
<div class="blog_h3"><span class="graybg">biosnoop</span></div>
<p>按进程和延迟探测块IO操作。</p>
<div class="blog_h3"><span class="graybg">biotop</span></div>
<p>按进程显示块IO的汇总信息。</p>
<div class="blog_h3"><span class="graybg">cachestat</span></div>
<p>每行打印统计周期内（默认1s）文件系统缓存命中、丢失、读/写操作缓存命中率。</p>
<div class="blog_h3"><span class="graybg">bindsnoop</span></div>
<p>追踪IPv4/IPv6的<pre class="crayon-plain-tag">bind()</pre>系统调用。</p>
<div class="blog_h3"><span class="graybg">tcpconnect</span></div>
<p>该工具监控每一个新发起的主动TCP连接（通过<pre class="crayon-plain-tag">connect()</pre>系统调用）。</p>
<div class="blog_h3"><span class="graybg">tcpaccept</span></div>
<p>该工具监控每一个新发起的被动TCP连接（通过<pre class="crayon-plain-tag">accept()</pre>系统调用）。</p>
<div class="blog_h3"><span class="graybg">tcpretrans</span></div>
<p>每当出现一个TCP重传包时，该工具打印一行内容。</p>
<p>TCP重传会导致延迟和吞吐量问题。</p>
<p>对于ESTABLISHED状态连接的重传，需要检查网络patterns；对于SYN_SENT状态连接的重传，可能提示目标内核CPU饱和以及内核丢包。</p>
<div class="blog_h3"><span class="graybg">runqlat</span></div>
<p>显示线程在CPU运行队列上等待的时间的直方图。</p>
<p>在CPU饱和的情况下，该工具可以度量等待CPU消耗的时间。</p>
<div class="blog_h3"><span class="graybg">profile</span></div>
<p>以特定的周期，获取栈追踪采样，并且在结束时，打印每个独特的栈追踪的数量。</p>
<p>该工具可以辅助理解哪些代码路径消耗CPU资源，因为消耗资源越多的代码路径，被采样到的概率越大。</p>
<div class="blog_h3"><span class="graybg">trace</span></div>
<p>能够探测你指定的函数，并且在满足条件的时候打印指定格式的追踪消息。</p>
<p>命令格式：</p>
<pre class="crayon-plain-tag">trace [-h] [-b BUFFER_PAGES] [-p PID] [-L TID] [-v] [-Z STRING_SIZE] [-S] [-M MAX_EVENTS] [-t] 
      [-u] [-T] [-C] [-c CGROUP_PATH] [-n NAME] [-f MSG_FILTER] [-B] [-s SYM_FILE_LIST] [-K] [-U] [-a]
             [-I header]
             # 探针规格定义
             probe [probe ...]</pre>
<p>选项：</p>
<p style="padding-left: 30px;">perf_events 环缓冲占用的页数量，默认64<br />-p PID  追踪特定的进程<br />-L TID 追踪特定的线程<br />-v 执行前打印生成的BPF代码<br />-S 包含trace程序自身的栈追踪，默认排除<br />-M 退出前允许的最大事件数量<br />-t 打印时间戳信息<br />-t -u 打印UNIX时间戳<br />-T 打印时间<br />-c  cgroup路径<br />-n 仅仅打印进程名包含该选项指定字符串的进程<br />-f 仅仅打印消息包含该选项指定字符串的事件<br />-B 运行对二进制值调用STRCMP<br />-s 逗号分割的符号文件，用于栈的符号解析<br />-K 输出内核栈追踪<br />-U 输出用户栈追踪<br />-a 在栈中打印虚拟地址<br />-I 额外包含在BPF程序中的头文件地址</p>
<p>示例：</p>
<pre class="crayon-plain-tag"># 追踪内核的发生的do_sys_open函数调用
trace do_sys_open

# 追踪open系统调用，并打印打开的文件名
#     ::do_sys_open (没有前缀)表示你需要一个entry probe
#                  格式化为字符串
#                        打印第二个参数
trace 'do_sys_open "%s", arg2'

# 追踪进程名包含main的进程的do_sys_open调用
trace 'do_sys_open "%s", arg2' -n main

# 追踪打开文件名包含config的进程的do_sys_open调用
trace 'do_sys_open "%s", arg2' -f config

# 追踪do_sys_open调用，并打印返回值
# 前缀 r 用来定义一个return probe，关键字retval代表函数返回值
trace 'r::do_sys_open "%llx", retval'

# 追踪内核函数kfree_skb的0x12偏移量的指令
trace kfree_skb+0x12

# 追踪读取超过20000bytes的sys_read系统调用
# 注意：在4.17+，系统调用函数发生了重命名，例如 sys_read被重命名为 __x64_sys_read
trace 'sys_read (arg3 &gt; 20000) "read %d bytes", arg3'

#        中间这个bash表示包含目标函数的库或者二进制文件的名字。如果不指定二进制
#        文件的绝对路径，则它必须位于PATH中
trace 'r:bash:readline "%s", retval'

# 追踪共享库的导出函数，或者二进制文件导入的函数
trace'r:/usr/lib64/libtinfo.so:curses_version "Version=%s", retval'

# 追踪标记参数为42的、由libc发起的open调用
#      指定库名时不需要前缀lib
trace 'c:open (arg2 == 42) "%s %d", arg1, arg2'

# 追踪malloc调用，打印分配的内存大小
trace 'c:malloc "size = %d", arg1'

# 追踪通过write调用写到stdout的字节数
trace 'p:c:write (arg1 == 1) "writing %d bytes to STDOUT", arg3'

# 追踪失败的__kmalloc调用，也就是返回值为0的
trace 'r::__kmalloc (retval == 0) "kmalloc failed!"'

# 多个探针可以在单个命令中执行
#               返回值必须cast为int然后再和0比较，因为argN和retval的默认类型是64bit整数
#               可能溢出为负数了
trace 'r:c:read ((int)retval &lt; 0) "read failed: %d", retval' \
        'r:c:write ((int)retval &lt; 0) "write failed: %d", retval' -T

# 该工具也对内核tracepoint提供了基本的支持
# 前缀 t 用来定义一个tracepoint
trace 't:block:block_rq_complete "sectors=%d", args-&gt;nr_sector' -T
# 要想知道tracepoint的结构的格式，也就是       args 指针的格式，可以利用工具tplist
tplist -v block:block_rq_complete
#   block:block_rq_complete
#       dev_t dev;
#       sector_t sector;
#       unsigned int nr_sector;
#       int errors;
#       char rwbs[8];

# 越来越多的高层次的库，启用了基于USDT探针的内省支持。这些探针的用法就像内核的tracepoint
# 前缀 u 用来定义一个USDT
#                                %U表示将arg3解析为用户空间符号，类似的%K则解析为内核空间符号
trace 'u:pthread:pthread_create "%U", arg3' -T -C

# Ruby OpenJDK Node等平台，也都支持USDT
# 下面的例子追踪Ruby方法调用，需要Ruby构建时启用配置--enable-dtrace
#                                    类名   方法名
trace 'u:ruby:method__entry "%s.%s", arg1, arg2' -p $(pidof irb) -T

# 根据字符串来过滤
#                打开文件名中包含test.txt
trace 'p:c:open (STRCMP("test.txt", arg1)) "opening %s", arg1' -T

# 通过提供函数签名，来提升可读性
trace 'p:c:open(char *filename) "opening %s", filename'</pre>
<div class="blog_h3"><span class="graybg">argdist</span></div>
<p>追踪一个函数，并且打印它的参数汇总信息。</p>
<div class="blog_h3"><span class="graybg">funccount</span></div>
<p>对函数、追踪点、USDT探针进行计数。</p>
<div class="blog_h3"><span class="graybg">bpflist</span></div>
<p>显示使用了BPF程序和Map的进程。</p>
<div class="blog_h3"><span class="graybg">bashreadline</span></div>
<p>打印全局范围内，在bash中执行的所有命令。</p>
<div class="blog_h3"><span class="graybg">capable</span></div>
<p>追踪对内核函数<pre class="crayon-plain-tag">cap_capable()</pre>的调用，此函数负责进行security capability的检查。</p>
<div class="blog_h1"><span class="graybg">开发</span></div>
<p>完整的开发文档参考：<a href="https://github.com/iovisor/bcc/blob/master/docs/reference_guide.md">https://github.com/iovisor/bcc/blob/master/docs/reference_guide.md</a></p>
<div class="blog_h2"><span class="graybg">HelloWorld</span></div>
<p>要基于BCC开发BPF程序，你可以使用多种编程语言，例如Python、Lua或者CPP。不过，你通常都要在这些编程语言代码中，用字符串嵌入C语言的BPF程序片段。</p>
<div class="blog_h3"><span class="graybg">C++</span></div>
<pre class="crayon-plain-tag">#include &lt;unistd.h&gt;
#include &lt;fstream&gt;
#include &lt;iostream&gt;
#include &lt;string&gt;

#include "bcc_version.h"
#include "BPF.h"

const std::string BPF_PROGRAM = R"(
int on_sys_clone(void *ctx) {
  bpf_trace_printk("Hello, World! Here I did a sys_clone call!\n");
  return 0;
}
)";

int main() {
  // 此类提供BPF各种操作接口
  ebpf::BPF bpf;
  // 初始化C编写的BPF程序片段
  auto init_res = bpf.init(BPF_PROGRAM);
  if (init_res.code() != 0) {
    std::cerr &lt;&lt; init_res.msg() &lt;&lt; std::endl;
    return 1;
  }
  // 打开此输入流用于查看BPF程序打印的日志
  std::ifstream pipe("/sys/kernel/debug/tracing/trace_pipe");
  std::string line;
  // 获取系统调用的函数名，4.17+给系统调用函数名加了前缀__x64_，这个函数会自动添加前缀
  std::string clone_fnname = bpf.get_syscall_fnname("clone");

  //                挂钩探针            到此函数        探针的BPF函数名
  auto attach_res = bpf.attach_kprobe(clone_fnname, "on_sys_clone");
  if (attach_res.code() != 0) {
    std::cerr &lt;&lt; attach_res.msg() &lt;&lt; std::endl;
    return 1;
  }

  std::cout &lt;&lt; "Starting HelloWorld with BCC " &lt;&lt; LIBBCC_VERSION &lt;&lt; std::endl;

  while (true) {
    if (std::getline(pipe, line)) {
      std::cout &lt;&lt; line &lt;&lt; std::endl;
      // 当pipe中没有更多数据可读，卸载探针
      auto detach_res = bpf.detach_kprobe(clone_fnname);
      if (detach_res.code() != 0) {
        std::cerr &lt;&lt; detach_res.msg() &lt;&lt; std::endl;
        return 1;
      }
      break;
    } else {
      std::cout &lt;&lt; "Waiting for a sys_clone event" &lt;&lt; std::endl;
      sleep(1);
    }
  }

  return 0;
}</pre>
<div class="blog_h3"><span class="graybg">python</span></div>
<pre class="crayon-plain-tag">from bcc import BPF

# This may not work for 4.17 on x64, you need replace kprobe__sys_clone with kprobe____x64_sys_clone
BPF(text='int kprobe__sys_clone(void *ctx) { bpf_trace_printk("Hello, World!\\n"); return 0; }').trace_print()</pre>
<p>类似C++的ebpf::BPF类，Python中的BPF类提供了BPF操作的各种接口。</p>
<p>text参数是需要执行的C语言编写的BPF程序。</p>
<p><pre class="crayon-plain-tag">kprobe__sys_clone()</pre>这种命名约定，用于快捷的定义Kprobe，前缀<pre class="crayon-plain-tag">kprobe__</pre>表示这是一个Kprobe，后面的则是需要instrument的内核函数的名字。</p>
<p>Kprobe接收一个<pre class="crayon-plain-tag">ctx</pre>参数，这个例子中访问该参数，因此将其转型为<pre class="crayon-plain-tag">void*</pre>。</p>
<p>函数bpf_trace_printk()打开公共的内核调试追踪管道（kernel debug trace pipe）并写入消息，也就是/sys/kernel/debug/tracing/trace_pipe。此管道是公共的，也就是所有BPF程序都可能写入消息，导致输出混乱。此外此管道性能较差，因此可以考虑BPF_PERF_OUTPUT()。</p>
<p><pre class="crayon-plain-tag">trace_print()</pre>函数读取trace_pipe中的消息并打印。</p>
<div class="blog_h2"><span class="graybg">常用编程接口</span></div>
<div class="blog_h3"><span class="graybg">读取追踪管道字段</span></div>
<pre class="crayon-plain-tag">b = BPF(text=prog)
# ...
# 获得字段元组，而非字符串
(task, pid, cpu, flags, ts, msg) = b.trace_fields()</pre>
<div class="blog_h3"><span class="graybg">使用Map</span></div>
<pre class="crayon-plain-tag">// b = BPF(text="""
#include &lt;uapi/linux/ptrace.h&gt;

// 定义一个哈希表，使用默认的键值类型u64
BPF_HASH(last);

int do_trace(struct pt_regs *ctx) {
    u64 ts, *tsp, delta, key = 0;

    // 查找键值
    tsp = last.lookup(&amp;key);
    if (tsp != NULL) {
        //      此助手函数返回当前纳秒数
        delta = bpf_ktime_get_ns() - *tsp;
        if (delta &lt; 1000000000) {
            // output if time is less than 1 second
            bpf_trace_printk("%d\\n", delta / 1000000);
        }
        // 删除键值
        last.delete(&amp;key);
    }

    ts = bpf_ktime_get_ns();
    // 更新键值
    last.update(&amp;key, &amp;ts);
    return 0;
}
// """)
// b.attach_kprobe(event=b.get_syscall_fnname("sync"), fn_name="do_trace")</pre>
<div class="blog_h3"><span class="graybg">使用perfbuf</span></div>
<p>由于bpf_trace_printk()性能较差，不适合在生产环境中使用。perfbuf可以作为其代替品。perfbuf本质上是BPF_MAP_TYPE_PERF_EVENT_ARRAY类型的Map。</p>
<p>使用BCC时，可以直接调用<pre class="crayon-plain-tag">BPF_PERF_OUTPUT()</pre>以使用perfbuf。</p>
<pre class="crayon-plain-tag">from bcc import BPF
# prog = """</pre><br />
<pre class="crayon-plain-tag">#include &lt;linux/sched.h&gt;

// 定义一个结构，作为输出的“事件”
struct data_t {
    u32 pid;
    u64 ts;
    char comm[TASK_COMM_LEN];
};
// 定义名为events的perfbuf
BPF_PERF_OUTPUT(events);

int hello(struct pt_regs *ctx) {
    struct data_t data = {};
    // 当前进程ID（内核角度的PID，对应用户空间的Thread ID），此ID存放在低32bit
    // 当前线程组ID（内核角度的Thread Group ID，对应用户空间的PID），此ID存放在高32bit
    // 这里直接赋值给u32，导致高32bit丢弃，存储线程ID
    // 对于多线程程序，它的所有线程共享TGID，如果需要区分不同线程，应当使用PID
    data.pid = bpf_get_current_pid_tgid();
    // 时间戳
    data.ts = bpf_ktime_get_ns();
    // 当前进程名
    bpf_get_current_comm(&amp;data.comm, sizeof(data.comm));

    // 提交事件到用户空间
    events.perf_submit(ctx, &amp;data, sizeof(data));

    return 0;
} </pre><br />
<pre class="crayon-plain-tag"># """

# 加载并挂钩BPF程序
b = BPF(text=prog)
b.attach_kprobe(event=b.get_syscall_fnname("clone"), fn_name="hello")

# header
print("%-18s %-16s %-6s %s" % ("TIME(s)", "COMM", "PID", "MESSAGE"))

start = 0
# 处理事件的回调函数
def print_event(cpu, data, size):
    global start
    #       访问perfbuf 转换事件（data）为Python对象
    event = b["events"].event(data)
    if start == 0:
            start = event.ts
    time_s = (float(event.ts - start)) / 1000000000
    print("%-18.9f %-16s %-6d %s" % (time_s, event.comm, event.pid,
        "Hello, perf_output!"))

# 打开perfbuf，设置回调函数
b["events"].open_perf_buffer(print_event)
while 1:
    # 阻塞式的拉取事件
    b.perf_buffer_poll()</pre>
<div class="blog_h3"><span class="graybg">生成直方图</span></div>
<pre class="crayon-plain-tag">from __future__ import print_function
from bcc import BPF
from time import sleep

# b = BPF(text="""</pre><br />
<pre class="crayon-plain-tag">#include &lt;uapi/linux/ptrace.h&gt;
#include &lt;linux/blkdev.h&gt;

// 声明一个直方图Map
BPF_HISTOGRAM(dist);
//  kprobe__开头的，表示是Kprobe，后面的blk_account_io_done就是目标函数
int kprobe__blk_account_io_done(struct pt_regs *ctx, struct request *req)
{
	// 让直方图的指定索引上的计数增加
	// 将块IO数据长度取2为底的对数，作为索引值。这表示进行2^的桶分布
	// 打个比方，当前操作字节数为64KB，取对数得到6，因此增加下面的第6个桶的计数

	//      kbytes          : count     distribution
	//        0 -&gt; 1        : 3        |                                      |
	//        2 -&gt; 3        : 0        |                                      |
	//        4 -&gt; 7        : 211      |**********                            |
	//        8 -&gt; 15       : 0        |                                      |
	//       16 -&gt; 31       : 0        |                                      |
	//       32 -&gt; 63       : 0        |                                      |
	//       64 -&gt; 127      : 1        |                                      |
	//      128 -&gt; 255      : 800      |**************************************|
	dist.increment(bpf_log2l(req-&gt;__data_len / 1024));
	return 0;
}</pre><br />
<pre class="crayon-plain-tag"># """)

# header
print("Tracing... Hit Ctrl-C to end.")

# trace until Ctrl-C
try:
	sleep(99999999)
except KeyboardInterrupt:
	print()

# output
b["dist"].print_log2_hist("kbytes")</pre>
<div class="blog_h3"><span class="graybg">从文件加载BPF程序</span></div>
<pre class="crayon-plain-tag">b = BPF(src_file = "vfsreadlat.c")</pre>
<div class="blog_h3"><span class="graybg">tracepoint</span></div>
<p>要运行tracepoint BPF程序，你需要Linux内核4.7+版本。</p>
<pre class="crayon-plain-tag">// instrument内核中追踪带年random:urandom_read。追踪点具有稳定的API，应当尽可能使用追踪点
// 而不是kprobes
TRACEPOINT_PROBE(random, urandom_read) {
    // args is from /sys/kernel/debug/tracing/events/random/urandom_read/format
    bpf_trace_printk("%d\\n", args-&gt;got_bits);
    return 0;
}</pre>
<p>所有可用的追踪点可以通过下面的命令获得：</p>
<pre class="crayon-plain-tag">tplist
# 或者
perf list</pre>
<p>追踪点可用参数可以通过下面的命令获得：</p>
<pre class="crayon-plain-tag">tplist -v random:urandom_read
# random:urandom_read
#     int got_bits;
#     int pool_left;
#     int input_left;</pre>
<div class="blog_h3"><span class="graybg">kprobe</span></div>
<p>下面的例子，追踪内核函数：</p>
<pre class="crayon-plain-tag">#include &lt;uapi/linux/ptrace.h&gt;
#include &lt;linux/sched.h&gt;

struct key_t {
    u32 prev_pid;
    u32 curr_pid;
};

BPF_HASH(stats, struct key_t, u64, 1024);
//                                   第二个参数开始，对应被探测的内核函数的参数
int count_sched(struct pt_regs *ctx, struct task_struct *prev) {
    struct key_t key = {};
    u64 zero = 0, *val;

    key.curr_pid = bpf_get_current_pid_tgid();
    key.prev_pid = prev-&gt;pid;

    // 等价于 stats.increment(key)
    val = stats.lookup_or_try_init(&amp;key, &amp;zero);
    if (val) {
      (*val)++;
    }
    return 0;
}</pre>
<p>注册探针：</p>
<pre class="crayon-plain-tag">b.attach_kprobe(event="finish_task_switch", fn_name="count_sched")</pre>
<p>需要注意的一点是，你可以在探针的参数列表中，将被探测函数的参数列出来，并访问。 finish_task_switch的签名：</p>
<pre class="crayon-plain-tag">static struct rq *finish_task_switch(struct task_struct *prev)
	__releases(rq-&gt;lock);</pre>
<p>因此在探针中我们可以访问prev参数，代表被切换出去的进程。</p>
<div class="blog_h3"><span class="graybg">uprobe</span></div>
<p>下面的例子追踪用户空间函数strlen()。</p>
<pre class="crayon-plain-tag">#include &lt;uapi/linux/ptrace.h&gt;

struct key_t {
    char c[80];
};
BPF_HASH(counts, struct key_t);

int count(struct pt_regs *ctx) {
    // 从上下文中读取strlen()的第一个参数
    if (!PT_REGS_PARM1(ctx))
        return 0;

    struct key_t key = {};
    u64 zero = 0, *val;

    // 将参数存入结构
    bpf_probe_read_user(&amp;key.c, sizeof(key.c), (void *)PT_REGS_PARM1(ctx));
    // 将结构存入Map，如果没有对应的key则初始化
    val = counts.lookup_or_try_init(&amp;key, &amp;zero);
    // 增加计数
    if (val) {
      (*val)++;
    }
    return 0;
};</pre><br />
<pre class="crayon-plain-tag"># 挂钩到         库c      的strlen函数   钩子函数为count
b.attach_uprobe(name="c", sym="strlen", fn_name="count")</pre>
<div class="blog_h3"><span class="graybg">USDT </span></div>
<p>USDT即用户静态定义的追踪点（user statically-defined tracing point），相当于tracepoint的用户态版本。很多流行的软件、框架都预置了USDT。</p>
<pre class="crayon-plain-tag">#include &lt;uapi/linux/ptrace.h&gt;
int do_trace(struct pt_regs *ctx) {
    uint64_t addr;
    char path[128]={0};
    // 读取USDT探针的第6个参数，到addr变量
    bpf_usdt_readarg(6, ctx, &amp;addr);
    // 将addr作为指针看待，并且从它所在位置读取最多128字节的字符串
    bpf_probe_read_user(&amp;path, sizeof(path), (void *)addr);
    bpf_trace_printk("path:%s\\n", path);
    return 0;
};</pre><br />
<pre class="crayon-plain-tag"># 为指定的PID初始化USDT追踪
u = USDT(pid=int(pid))
# 挂钩do_trace到追踪点http__server__request
u.enable_probe(probe="http__server__request", fn_name="do_trace")

# 初始化BPF
b = BPF(text=bpf_text, usdt_contexts=[u])</pre>
<div class="blog_h2"><span class="graybg">工具源码分析</span></div>
<div class="blog_h3"><span class="graybg">disksnoop</span></div>
<pre class="crayon-plain-tag"># 在Python程序中定义一个内核常量，不需要给出头文件
REQ_WRITE = 1		# from include/linux/blk_types.h

# 加载BPF程序
# b = BPF(text=""" </pre><br />
<pre class="crayon-plain-tag">#include &lt;uapi/linux/ptrace.h&gt;
#include &lt;linux/blkdev.h&gt;

// 哈希表        键的类型（值类型是默认的u64）
BPF_HASH(start, struct request *);

// KProbe的参数pt_regs，包含registers、BPF上下文
//                                    struct request *req是目标函数blk_start_request的第一个参数
void trace_start(struct pt_regs *ctx, struct request *req) {
	u64 ts = bpf_ktime_get_ns();
        // 直接使用请求参数的指针作为键，在追踪程序中这很常见。因为任何两个结构都不会有相同的地址
        // 可以安全的作为唯一键（注意内存被释放并重用的情况）
	start.update(&amp;req, &amp;ts);
}

void trace_completion(struct pt_regs *ctx, struct request *req) {
	u64 *tsp, delta;
        // 查找先前记录的时间戳
	tsp = start.lookup(&amp;req);
	if (tsp != 0) {
		// 获取消耗的时间
		delta = bpf_ktime_get_ns() - *tsp;
		// 打印本次块IO操作的信息
		//                           直接访问结构字段，BCC会自动将其转换为bpf_probe_read_kernel()调用
		//                           某些情况下BCC无法处理复杂的解引用操作，你可以手工调用该助手函数
		bpf_trace_printk("%d %x %d\\n", req-&gt;__data_len,
		    req-&gt;cmd_flags, delta / 1000);
		start.delete(&amp;req);
	}
}
 </pre><br />
<pre class="crayon-plain-tag"># """)
# 函数 void blk_start_request ( struct request * req); 开始请求驱动来处理块IO请求
b.attach_kprobe(event="blk_start_request", fn_name="trace_start")
# 函数 void blk_mq_start_request(struct request *rq); 高IOPS下Linux的块设备层形成了瓶颈
# 因此多核心系统下，3.13+添加的Multi queue特性可以减少块设备层的性能影响。传统块设备层有能力
# 处理万级别的IOPS，但是随着SSD这样的高性能设备的出现，其出现能力瓶颈。Multi queue的主要思想
# 是，为每个CPU核心设置一个请求队列，均衡核之间的负载，减少对请求队列的锁竞争
b.attach_kprobe(event="blk_mq_start_request", fn_name="trace_start")
# 函数 void blk_account_io_done(struct request *req, u64 now); 在块设备IO完成后调用
# 此函数优于blk_account_io_completion()，后者可能在请求部分完成的情况下调用，调用可能发生多次，
# 这种现象会发生在loop设备/老的SCSI上
b.attach_kprobe(event="blk_account_io_done", fn_name="trace_completion")</pre>
<p>可以使用具有稳定接口的追踪点block:block_rq_issue、block:block_rq_complete代替上述Kprobe。</p>
<div class="blog_h3"><span class="graybg">vfsreadlat</span></div>
<p>所有统计延迟的监控程序，都是选择适当的键，然后在entry/return钩子中进行计时：</p>
<pre class="crayon-plain-tag">#include &lt;uapi/linux/ptrace.h&gt;

BPF_HASH(start, u32);
BPF_HISTOGRAM(dist);

int do_entry(struct pt_regs *ctx)
{
	u32 pid;
	u64 ts, *val;

	pid = bpf_get_current_pid_tgid();
	ts = bpf_ktime_get_ns();
	// 记录开始时间
	start.update(&amp;pid, &amp;ts);
	return 0;
}

int do_return(struct pt_regs *ctx)
{
	u32 pid;
	u64 *tsp, delta;

	pid = bpf_get_current_pid_tgid();
	tsp = start.lookup(&amp;pid);

	if (tsp != 0) {
		// 计算耗时
		delta = bpf_ktime_get_ns() - *tsp;
		// 更新桶计数
		dist.increment(bpf_log2l(delta / 1000));
		start.delete(&amp;pid);
	}

	return 0;
}</pre><br />
<pre class="crayon-plain-tag"># 从文件中加载BPF程序
b = BPF(src_file = "vfsreadlat.c")
# 挂钩的内核函数是vfs_read
b.attach_kprobe(event="vfs_read", fn_name="do_entry")
b.attach_kretprobe(event="vfs_read", fn_name="do_return")</pre>
</div><p>The post <a rel="nofollow" href="https://blog.gmem.cc/bcc">基于BCC进行性能追踪</a> appeared first on <a rel="nofollow" href="https://blog.gmem.cc">绿色记忆</a>.</p>
]]></content:encoded>
			<wfw:commentRss>https://blog.gmem.cc/bcc/feed</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
	</channel>
</rss>
