mirror of
https://github.com/eunomia-bpf/bpf-developer-tutorial.git
synced 2026-10-29 03:34:44 +08:00
docs: remove dash punctuation from new tutorials
This commit is contained in:
@@ -385,7 +385,7 @@ int enforce_dns_policy(struct bpf_sock_addr *ctx)
|
||||
|
||||
The program uses three BPF maps, each serving a distinct role in the trust chain.
|
||||
|
||||
**`pending_queries`** is an LRU hash that holds correlation state for DNS queries. The key combines four fields—server IP, client IP, client port, and transaction ID—that together uniquely identify a query-response pair. The value stores only the expiration timestamp, which is all we need for timing validation. LRU eviction keeps memory bounded even under heavy DNS traffic.
|
||||
**`pending_queries`** is an LRU hash that holds correlation state for DNS queries. The key combines four fields: server IP, client IP, client port, and transaction ID. Together, they uniquely identify a query-response pair. The value stores only the expiration timestamp, which is all we need for timing validation. LRU eviction keeps memory bounded even under heavy DNS traffic.
|
||||
|
||||
**`allowed_ips`** is also an LRU hash, keyed simply by IPv4 address. The value contains the expiration timestamp, the original TTL in seconds (for logging), padding for alignment, and a flag indicating whether we have already reported expiration. This flag prevents duplicate "expired" events when multiple threads race on a stale entry.
|
||||
|
||||
@@ -403,7 +403,7 @@ Note that `record_dns_query` always returns 1, telling the kernel to continue pr
|
||||
|
||||
The ingress program `learn_dns_answer` reverses the perspective. Now we examine packets arriving from the resolver, so `parse_response_transport` checks that the *source* (not destination) matches the resolver IP and port. The correlation key gets populated with the same fields, but from the response's viewpoint.
|
||||
|
||||
The critical security check happens in `pending_query_is_live`. This function looks up the correlation key in `pending_queries`. If no entry exists—meaning we never saw a matching query—the response is rejected. If an entry exists but has expired, we delete it and reject the response. Only responses that match a live pending query proceed.
|
||||
The critical security check happens in `pending_query_is_live`. This function looks up the correlation key in `pending_queries`. If no entry exists, meaning we never saw a matching query, the response is rejected. If an entry exists but has expired, we delete it and reject the response. Only responses that match a live pending query proceed.
|
||||
|
||||
After confirming we have a legitimate response, `parse_response_question` validates the DNS header. It checks that flags indicate a successful response with no errors, that exactly one question matches our domain, and that at least one answer exists. Then `parse_direct_a_answer` extracts the first A record, requiring the common `0xc00c` compressed name format, correct type and class, and a TTL between 1 and 86400 seconds.
|
||||
|
||||
@@ -1077,7 +1077,7 @@ In normal mode, `poll_policy_events` loops on the ring buffer until the duration
|
||||
|
||||
Demo mode functions as both an integration test and a demonstration of the security model. It runs entirely on loopback using non-standard ports (15353 for DNS, 19090 for TCP) to avoid interfering with real services.
|
||||
|
||||
The test sequence begins by verifying that connections are blocked before any DNS traffic occurs. It then sends an unsolicited DNS response—one that arrives without a preceding query. The BPF program rejects this because `pending_queries` contains no matching entry. The test confirms the connection remains blocked.
|
||||
The test sequence begins by verifying that connections are blocked before any DNS traffic occurs. It then sends an unsolicited DNS response, one that arrives without a preceding query. The BPF program rejects this because `pending_queries` contains no matching entry. The test confirms the connection remains blocked.
|
||||
|
||||
Next, it sends a legitimate DNS query but responds with the wrong transaction ID. The BPF program rejects this too, because the transaction ID is part of the correlation key. Again, the test confirms the connection stays blocked.
|
||||
|
||||
@@ -1158,4 +1158,4 @@ This example transforms observed DNS results into a time-bounded connection poli
|
||||
- [BPF LRU hash maps](https://docs.kernel.org/bpf/map_hash.html)
|
||||
- [BPF atomic compare-and-exchange commit](https://github.com/torvalds/linux/commit/5ffa25502b5ab3d639829a2d1e316cff7f59a41e)
|
||||
- [Control Group v2](https://docs.kernel.org/admin-guide/cgroup-v2.html)
|
||||
- [RFC 1035: Domain Names — Implementation and Specification](https://www.rfc-editor.org/rfc/rfc1035.html)
|
||||
- [RFC 1035: Domain Names: Implementation and Specification](https://www.rfc-editor.org/rfc/rfc1035.html)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# eBPF 实战教程:用 cgroup BPF 构建基于 DNS 的 IP 允许列表
|
||||
|
||||
假设你希望一个容器化服务只能访问 `api.example.com`,拒绝所有其他连接。最直接的方案是 IP 白名单,但 DNS 应答会变、TTL 会过期。到了 `connect()` 阶段,内核只看到一个裸 IP,完全不知道它源自哪个域名。缺少的是一条短期关联——把 DNS 查询和随后的 TCP 连接串起来。
|
||||
假设你希望一个容器化服务只能访问 `api.example.com`,拒绝所有其他连接。最直接的方案是 IP 白名单,但 DNS 应答会变、TTL 会过期。到了 `connect()` 阶段,内核只看到一个裸 IP,完全不知道它源自哪个域名。缺少的是一条短期关联:把 DNS 查询和随后的 TCP 连接串起来。
|
||||
|
||||
本教程就来构建这条关联。我们监控特定域名的 DNS 流量,只从有效的查询与响应对中提取 IP 地址,并在 TTL 有效期内允许连接。最终是一个简洁但完整的策略工具,展示 cgroup BPF 程序如何跨多个内核 hook 共享状态。
|
||||
|
||||
@@ -22,9 +22,9 @@ eBPF 允许在多个内核 hook 上运行经过验证的程序,并通过 map
|
||||
|
||||
1. **`cgroup_skb/egress`**:监控出站 DNS 查询,记录哪些响应是我们期待的
|
||||
2. **`cgroup_skb/ingress`**:验证入站 DNS 响应,只从匹配待处理查询的回复中提取 IP 地址
|
||||
3. **`cgroup/connect4`**:做最终裁决——只允许连接到从有效 DNS 响应中学到的 IP,且仅在 TTL 有效期内
|
||||
3. **`cgroup/connect4`**:做最终裁决,只允许连接到从有效 DNS 响应中学到的 IP,且仅在 TTL 有效期内
|
||||
|
||||
下面跟着一次成功的解析走完整个流程。应用发送 `lab.test` 的 A 查询,egress hook 保存一个关联 key,包含四个字段:解析器地址、客户端地址、客户端 UDP 端口和 DNS 事务 ID。这条记录 5 秒后过期。响应到达后,ingress hook 用相同字段重建 key,验证响应内容,并提取 IP 地址和 TTL。之后发往这个地址的 `connect()` 可以成功——但只在 TTL 有效期内。
|
||||
下面跟着一次成功的解析走完整个流程。应用发送 `lab.test` 的 A 查询,egress hook 保存一个关联 key,包含四个字段:解析器地址、客户端地址、客户端 UDP 端口和 DNS 事务 ID。这条记录 5 秒后过期。响应到达后,ingress hook 用相同字段重建 key,验证响应内容,并提取 IP 地址和 TTL。之后发往这个地址的 `connect()` 可以成功,但只在 TTL 有效期内。
|
||||
|
||||
待处理查询记录就是信任边界。主动推送的伪造响应在 map 中找不到匹配的键;事务 ID 错误的响应会查找另一个键而落空。两者都无法污染允许列表。即使条目还留在 LRU map 中,connect hook 也会重新检查过期时间戳,所以地址会随 DNS TTL 自然失效。
|
||||
|
||||
@@ -83,7 +83,7 @@ struct dns_egress_event {
|
||||
|
||||
协议结构使用 `packed` 属性,因为它们直接映射网络报文字节,不允许填充。`dns_header` 对应每个查询和响应开头的 12 字节 DNS 头。`dns_question` 跟在问题名(使用长度前缀标签)后面。`dns_a_answer` 期望常见的压缩格式,其中 name 指针是 `0xc00c`,指回问题部分。
|
||||
|
||||
发给用户空间的每个事件同时携带以秒为单位的 DNS TTL 和以纳秒为单位的绝对过期时间戳。用户空间打印人类可读的 TTL 用于日志,而 BPF 程序用单调时间戳做决策。这种分离让内核逻辑保持简洁——BPF 代码中无需时间格式转换。
|
||||
发给用户空间的每个事件同时携带以秒为单位的 DNS TTL 和以纳秒为单位的绝对过期时间戳。用户空间打印人类可读的 TTL 用于日志,而 BPF 程序用单调时间戳做决策。这种分离让内核逻辑保持简洁,BPF 代码中无需时间格式转换。
|
||||
|
||||
## BPF 程序:完整实现
|
||||
|
||||
@@ -385,7 +385,7 @@ int enforce_dns_policy(struct bpf_sock_addr *ctx)
|
||||
|
||||
程序使用三个 BPF map,各自在信任链中承担不同角色。
|
||||
|
||||
**`pending_queries`** 是一个 LRU hash,保存 DNS 查询的关联状态。键由四个字段组成——服务器 IP、客户端 IP、客户端端口和事务 ID——它们共同唯一标识一个查询与响应对。值只存储过期时间戳,这就是时序验证所需的全部信息。LRU 淘汰机制确保即使 DNS 流量很大,内存占用也保持有界。
|
||||
**`pending_queries`** 是一个 LRU hash,保存 DNS 查询的关联状态。键由四个字段组成:服务器 IP、客户端 IP、客户端端口和事务 ID。这些字段共同唯一标识一个查询与响应对。值只存储过期时间戳,这就是时序验证所需的全部信息。LRU 淘汰机制确保即使 DNS 流量很大,内存占用也保持有界。
|
||||
|
||||
**`allowed_ips`** 也是 LRU hash,但只用 IPv4 地址作为键。值包含过期时间戳、以秒为单位的原始 TTL(用于日志)、对齐填充,以及一个标记是否已上报过期的标志位。这个标志位可以防止多线程竞争访问过期条目时产生重复的 `expired` 事件。
|
||||
|
||||
@@ -403,7 +403,7 @@ int enforce_dns_policy(struct bpf_sock_addr *ctx)
|
||||
|
||||
入站程序 `learn_dns_answer` 反转了视角。现在我们检查的是从解析器到达的报文,所以 `parse_response_transport` 检查*源*(而非目标)是否匹配解析器 IP 和端口。关联键使用相同的字段填充,但改从响应的视角取值。
|
||||
|
||||
关键的安全检查发生在 `pending_query_is_live`。这个函数在 `pending_queries` 中查找关联键。如果没有条目——说明我们从未看到匹配的查询——响应被拒绝。如果条目存在但已过期,删除它并拒绝响应。只有匹配活跃待处理查询的响应才能继续。
|
||||
关键的安全检查发生在 `pending_query_is_live`。这个函数在 `pending_queries` 中查找关联键。如果没有条目,说明我们从未看到匹配的查询,响应会被拒绝。如果条目存在但已过期,删除它并拒绝响应。只有匹配活跃待处理查询的响应才能继续。
|
||||
|
||||
确认响应合法后,`parse_response_question` 验证 DNS 头。它检查 flags 是否表示成功响应且无错误、是否恰好包含一个匹配我们域名的问题、以及是否至少有一个应答。然后 `parse_direct_a_answer` 提取第一个 A 记录,要求常见的 `0xc00c` 压缩 name 格式、正确的 type 和 class,以及 1 到 86400 秒之间的 TTL。
|
||||
|
||||
@@ -1077,7 +1077,7 @@ cleanup:
|
||||
|
||||
Demo 模式既是集成测试,也是安全模型的演示。它完全在 loopback 上运行,使用非标准端口(DNS 15353,TCP 19090)以避免干扰真实服务。
|
||||
|
||||
测试序列首先验证在任何 DNS 流量之前连接被阻止。然后发送一个未经请求的 DNS 响应——一个没有前置查询就到达的响应。BPF 程序拒绝它,因为 `pending_queries` 中没有匹配的条目。测试确认连接仍然被阻止。
|
||||
测试序列首先验证在任何 DNS 流量之前连接被阻止。然后发送一个未经请求的 DNS 响应,也就是一个没有前置查询就到达的响应。BPF 程序拒绝它,因为 `pending_queries` 中没有匹配的条目。测试确认连接仍然被阻止。
|
||||
|
||||
接下来,发送一个合法的 DNS 查询,但响应使用错误的事务 ID。BPF 程序同样拒绝它,因为事务 ID 是关联键的一部分。测试再次确认连接仍处于阻断状态。
|
||||
|
||||
@@ -1158,4 +1158,4 @@ demo step=expired-answer result=blocked
|
||||
- [BPF LRU hash map](https://docs.kernel.org/bpf/map_hash.html)
|
||||
- [BPF atomic compare-and-exchange commit](https://github.com/torvalds/linux/commit/5ffa25502b5ab3d639829a2d1e316cff7f59a41e)
|
||||
- [Control Group v2](https://docs.kernel.org/admin-guide/cgroup-v2.html)
|
||||
- [RFC 1035:Domain Names — Implementation and Specification](https://www.rfc-editor.org/rfc/rfc1035.html)
|
||||
- [RFC 1035:Domain Names: Implementation and Specification](https://www.rfc-editor.org/rfc/rfc1035.html)
|
||||
|
||||
@@ -438,7 +438,7 @@ OOM 处理程序必须仔细解析进程身份。tracepoint 给我们一个线
|
||||
|
||||
用户空间代码负责几项工作:加载内核符号用于调用栈符号化,使用任何 cgroup 过滤器设置 BPF 程序,处理来自 ring buffer 的事件,以及按总耗时对调用栈进行排名。它还包含一个 demo 模式,可以创建一个内存受限的 cgroup 并触发 OOM 来验证工具是否正常工作。
|
||||
|
||||
完整的用户空间代码相当长,所以我们重点介绍关键部分。启动时,它读取 `/proc/kallsyms`,按地址排序符号,然后使用二分查找解析每个栈帧。当符号地址受限时(在生产系统上很常见),它仍然打印原始地址——测量结果保持准确。
|
||||
完整的用户空间代码相当长,所以我们重点介绍关键部分。启动时,它读取 `/proc/kallsyms`,按地址排序符号,然后使用二分查找解析每个栈帧。当符号地址受限时(在生产系统上很常见),它仍然打印原始地址,测量结果保持准确。
|
||||
|
||||
调用栈组按累计回收时间排名。这种排名可以展示频繁调用的路径和罕见的慢速间隔。在 OOM 事件之后,打印前五个调用栈及其时间统计。
|
||||
|
||||
@@ -506,7 +506,7 @@ dropped_victim_states=0 dropped_reclaim_states=0
|
||||
|
||||
## 实现范围
|
||||
|
||||
Profile 从程序附加开始累积直到退出,使用有界的 LRU map:4096 个 cgroup profile、4096 个活动间隔、8192 个调用栈聚合和 1024 个唯一调用栈。每个 `pid_tgid` 保留一个活动间隔,与 begin/end 跟踪模式相匹配。工具捕获内核调用栈而不是用户调用栈,并将符号化视为展示——受限的 `kallsyms` 会将名称变为地址,但不影响测量结果。
|
||||
Profile 从程序附加开始累积直到退出,使用有界的 LRU map:4096 个 cgroup profile、4096 个活动间隔、8192 个调用栈聚合和 1024 个唯一调用栈。每个 `pid_tgid` 保留一个活动间隔,与 begin/end 跟踪模式相匹配。工具捕获内核调用栈而不是用户调用栈,并将符号化视为展示环节:受限的 `kallsyms` 会将名称变为地址,但不影响测量结果。
|
||||
|
||||
## 总结
|
||||
|
||||
|
||||
@@ -28,7 +28,7 @@ AF_XDP 通过四个必须精确协调的组件工作:
|
||||
|
||||
流程是这样的:你向 Fill Ring 发布 64 个 frame 地址。一个 UDP 报文到达。你的 XDP 程序检查目的端口,在 XSKMAP 中查找 socket,然后重定向。内核把报文复制到你的某个 frame 中,并在 RX Ring 上发布一个 descriptor。你读取 descriptor、处理报文,再把这个 frame 地址放回 Fill Ring。循环继续。
|
||||
|
||||
这个所有权模型至关重要。一个 frame 开始时属于你。你通过 Fill Ring 借给内核。内核借用它来接收报文。你从 RX Ring 回收它。你必须把它归还到 Fill Ring,否则 64 个报文之后就会耗尽 frame。我们的工具通过成功捕获 65 个报文来证明这个机制有效——这需要至少一个 frame 完成完整的所有权周期。
|
||||
这个所有权模型至关重要。一个 frame 开始时属于你。你通过 Fill Ring 借给内核。内核借用它来接收报文。你从 RX Ring 回收它。你必须把它归还到 Fill Ring,否则 64 个报文之后就会耗尽 frame。我们的工具通过成功捕获 65 个报文来证明这个机制有效,这需要至少一个 frame 完成完整的所有权周期。
|
||||
|
||||
## XDP 程序:过滤和重定向
|
||||
|
||||
@@ -715,7 +715,7 @@ Packet 65 证明至少有一个 frame 完成了完整的所有权周期:发布
|
||||
|
||||
AF_XDP 给你提供了具有 eBPF 安全保证的内核旁路报文接收。XDP 程序在驱动边界选择流量,XSKMAP 把报文路由到你的 socket,无锁的 ring buffer 不用系统调用就能传输数据。这个例子展示了完整的接收契约:向 Fill 发布 frame,在 RX 上接收 descriptor,处理报文,把 frame 回收到 Fill。
|
||||
|
||||
理解这个流程的价值超越了抓包本身。这里的模式——内核与用户态之间的共享内存、显式的所有权转移、无锁同步——在从数据库到 GPU 驱动的各种高性能系统中反复出现。
|
||||
理解这个流程的价值超越了抓包本身。这里的模式包括内核与用户态之间的共享内存、显式的所有权转移和无锁同步,这些模式在从数据库到 GPU 驱动的各种高性能系统中反复出现。
|
||||
|
||||
> 如果你想深入了解 eBPF,请查看我们的教程代码仓库 <https://github.com/eunomia-bpf/bpf-developer-tutorial> 或访问我们的网站 <https://eunomia.dev/tutorials/>。
|
||||
|
||||
|
||||
Reference in New Issue
Block a user