+#include "accept.h"
+
+struct conn_id_t
+{
+ u32 pid;
+ int fd;
+ __u64 tsid;
+};
+
+struct conn_info_t
+{
+ struct conn_id_t conn_id;
+ __s64 wr_bytes;
+ __s64 rd_bytes;
+ bool is_http;
+};
+
+// A struct describing the event that we send to the user mode upon a new connection.
+struct socket_open_event_t
+{
+ // The time of the event.
+ u64 timestamp_ns;
+
+ // A unique ID for the connection.
+ struct conn_id_t conn_id;
+};
+
+struct
+{
+ __uint(type, BPF_MAP_TYPE_HASH);
+ __uint(max_entries, 131072);
+ __type(key, __u64);
+ __type(value, struct conn_info_t);
+} conn_info_map SEC(".maps");
+
+struct
+{
+ __uint(type, BPF_MAP_TYPE_PERF_EVENT_ARRAY);
+ __uint(key_size, sizeof(u32));
+ __uint(value_size, sizeof(u32));
+} events SEC(".maps");
+
+struct accept_args_t
+{
+ struct sockaddr_in *addr;
+};
+
+struct
+{
+ __uint(type, BPF_MAP_TYPE_HASH);
+ __uint(max_entries, 4096);
+ __type(key, u64);
+ __type(value, struct accept_args_t);
+} active_accept_args_map SEC(".maps");
+
+SEC("tracepoint/syscalls/sys_enter_accept")
+int sys_enter_accept(struct trace_event_raw_sys_enter *ctx)
+{
+ u64 id = bpf_get_current_pid_tgid();
+
+ struct accept_args_t accept_args = {};
+ accept_args.addr = (struct sockaddr_in *)BPF_CORE_READ(ctx, args[1]);
+ bpf_map_update_elem(&active_accept_args_map, &id, &accept_args, BPF_ANY);
+ bpf_printk("enter_accept accept_args.addr: %llx\n", accept_args.addr);
+ return 0;
+}
+
+SEC("tracepoint/syscalls/sys_exit_accept")
+int sys_exit_accept(struct trace_event_raw_sys_exit *ctx)
+{
+
+ u64 id = bpf_get_current_pid_tgid();
+
+ struct accept_args_t *args =
+ bpf_map_lookup_elem(&active_accept_args_map, &id);
+ if (args == NULL)
+ {
+ return 0;
+ }
+ bpf_printk("exit_accept accept_args.addr: %llx\n", args->addr);
+ int ret_fd = (int)BPF_CORE_READ(ctx, ret);
+ if (ret_fd <= 0)
+ {
+ return 0;
+ }
+
+ struct conn_info_t conn_info = {};
+
+ u32 pid = id >> 32;
+ conn_info.conn_id.pid = pid;
+ conn_info.conn_id.fd = ret_fd;
+ conn_info.conn_id.tsid = bpf_ktime_get_ns();
+
+ __u64 pid_fd = ((__u64)pid << 32) | (u32)ret_fd;
+ bpf_map_update_elem(&conn_info_map, &pid_fd, &conn_info, BPF_ANY);
+
+ struct socket_data_event_t open_event = {};
+ open_event.timestamp_ns = bpf_ktime_get_ns();
+ open_event.pid = conn_info.conn_id.pid;
+ open_event.fd = conn_info.conn_id.fd;
+ open_event.is_connection = true;
+
+ bpf_perf_event_output(ctx, &events, BPF_F_CURRENT_CPU,
+ &open_event, sizeof(struct socket_data_event_t));
+
+ bpf_map_delete_elem(&active_accept_args_map, &id);
+}
+
+struct data_args_t
+{
+ __s32 fd;
+ const char *buf;
+};
+
+struct
+{
+ __uint(type, BPF_MAP_TYPE_HASH);
+ __uint(max_entries, 4096);
+ __type(key, u64);
+ __type(value, struct data_args_t);
+} active_read_args_map SEC(".maps");
+
+SEC("tracepoint/syscalls/sys_enter_read")
+int sys_enter_read(struct trace_event_raw_sys_enter *ctx)
+{
+ u64 id = bpf_get_current_pid_tgid();
+
+ struct data_args_t read_args = {};
+ read_args.fd = (int)BPF_CORE_READ(ctx, args[0]);
+ read_args.buf = (char *)BPF_CORE_READ(ctx, args[1]);
+ bpf_map_update_elem(&active_read_args_map, &id, &read_args, BPF_ANY);
+
+ return 0;
+}
+
+static inline bool is_http_connection(const char *line_buffer, u64 bytes_count)
+{
+ if (bytes_count < 6)
+ {
+ return 0;
+ }
+ if (bpf_strncmp(line_buffer, 3, "GET") != 0 && bpf_strncmp(line_buffer, 4, "POST") != 0 && bpf_strncmp(line_buffer, 3, "PUT") != 0 && bpf_strncmp(line_buffer, 6, "DELETE") != 0 && bpf_strncmp(line_buffer, 4, "HTTP") != 0)
+ {
+ return 0;
+ }
+ return 1;
+}
+
+static inline void process_data(struct trace_event_raw_sys_exit *ctx,
+ u64 id, const struct data_args_t *args, u64 bytes_count)
+{
+ if (args->buf == NULL)
+ {
+ return;
+ }
+
+ u32 pid = id >> 32;
+ u64 pid_fd = ((u64)pid << 32) | (u64)args->fd;
+ struct conn_info_t *conn_info = bpf_map_lookup_elem(&conn_info_map, &pid_fd);
+ if (conn_info == NULL)
+ {
+ return;
+ }
+ if (args->buf == NULL)
+ {
+ return;
+ }
+ char line_buffer[7];
+ bpf_probe_read_kernel(line_buffer, 7, args->buf);
+ if (is_http_connection(line_buffer, bytes_count))
+ {
+ u32 kZero = 0;
+ struct socket_data_event_t event = {};
+
+ event.timestamp_ns = bpf_ktime_get_ns();
+ event.is_connection = false;
+ event.pid = conn_info->conn_id.pid;
+ event.fd = conn_info->conn_id.fd;
+ unsigned int read_size = bytes_count > MAX_MSG_SIZE ? MAX_MSG_SIZE : bytes_count;
+ bpf_probe_read_kernel(&event.msg, read_size, args->buf);
+
+ bpf_perf_event_output(ctx, &events, BPF_F_CURRENT_CPU,
+ &event, sizeof(struct socket_data_event_t));
+ }
+}
+
+SEC("tracepoint/syscalls/sys_exit_read")
+int sys_exit_read(struct trace_event_raw_sys_exit *ctx)
+{
+ u64 bytes_count = (u64)BPF_CORE_READ(ctx, ret);
+ if (bytes_count <= 0)
+ {
+ return 0;
+ }
+ u64 id = bpf_get_current_pid_tgid();
+ struct data_args_t *read_args = bpf_map_lookup_elem(&active_read_args_map, &id);
+ if (read_args != NULL)
+ {
+ process_data(ctx, id, read_args, bytes_count);
+ }
+
+ bpf_map_delete_elem(&active_read_args_map, &id);
+
+ return 0;
+}
+
+char _license[] SEC("license") = "GPL";
diff --git a/23-http/accept.h b/23-http/accept.h
new file mode 100644
index 0000000..4fc81de
--- /dev/null
+++ b/23-http/accept.h
@@ -0,0 +1,17 @@
+#ifndef BPF_HTTP_ACCEPT_TRACE_H
+#define BPF_HTTP_ACCEPT_TRACE_H
+
+#define MAX_MSG_SIZE 256
+
+struct socket_data_event_t
+{
+ unsigned long long timestamp_ns;
+ unsigned int pid;
+ int fd;
+ bool is_connection;
+ unsigned int msg_size;
+ unsigned long long pos;
+ char msg[MAX_MSG_SIZE];
+};
+
+#endif // BPF_HTTP_ACCEPT_TRACE_H
diff --git a/23-http/index.html b/23-http/index.html
index 17b2d0b..fd5461b 100644
--- a/23-http/index.html
+++ b/23-http/index.html
@@ -3,7 +3,7 @@
- 使用 eBPF 追踪 HTTP 请求或其他七层协议 - bpf-developer-tutorial
+ 使用 eBPF socket filter 或 syscall tracepoint 追踪 HTTP 请求等七层协议 - bpf-developer-tutorial
@@ -83,7 +83,7 @@
@@ -166,8 +166,581 @@
-
-TODO
+
+在当今的技术环境中,随着微服务、云原生应用和复杂的分布式系统的崛起,系统的可观测性已成为确保其健康、性能和安全的关键要素。特别是在微服务架构中,应用程序的组件可能分布在多个容器和服务器上,这使得传统的监控方法往往难以提供足够的深度和广度来全面了解系统的行为。这就是为什么观测七层协议,如 HTTP、gRPC、MQTT 等,变得尤为重要。
+七层协议为我们提供了关于应用程序如何与其他服务和组件交互的详细信息。在微服务环境中,了解这些交互是至关重要的,因为它们经常是性能瓶颈、故障和安全问题的根源。然而,监控这些协议并不简单。传统的网络监控工具,如 tcpdump,虽然在捕获网络流量方面非常有效,但在处理七层协议的复杂性和动态性时,它们往往显得力不从心。
+这正是 eBPF 技术发挥作用的地方。eBPF 允许开发者和运维人员深入到系统的内核层,实时观测和分析系统的行为,而无需对应用程序代码进行任何修改或插入埋点。这为我们提供了一个独特的机会,可以更简单、更高效地处理应用层流量,特别是在微服务环境中。
+在本教程中,我们将深入探讨以下内容:
+
+- 追踪七层协议,如 HTTP,以及与其相关的挑战。
+- eBPF 的 socket filter 和 syscall 追踪:这两种技术如何帮助我们在不同的内核层次追踪 HTTP 网络请求数据,以及这两种方法的优势和局限性。
+- eBPF 实践教程:如何开发一个 eBPF 程序,使用 eBPF socket filter 或 syscall 追踪来捕获和分析 HTTP 流量
+
+随着网络流量的增加和应用程序的复杂性增加,对七层协议的深入了解变得越来越重要。通过本教程,您将获得必要的知识和工具,以便更有效地监控和分析您的网络流量,从而为您的应用程序和服务器提供最佳的性能。
+本文是 eBPF 开发者教程的一部分,更详细的内容可以在这里找到:https://eunomia.dev/tutorials/ 源代码在 GitHub 仓库 中开源。
+
+在现代的网络环境中,七层协议不仅仅局限于 HTTP。实际上,有许多七层协议,如 HTTP/2, gRPC, MQTT, WebSocket, AMQP 和 SMTP,它们都在不同的应用场景中发挥着关键作用。这些协议为我们提供了关于应用程序如何与其他服务和组件交互的详细信息。但是,追踪这些协议并不是一个简单的任务,尤其是在复杂的分布式系统中。
+
+-
+
多样性和复杂性:每种七层协议都有其特定的设计和工作原理。例如,gRPC 使用了 HTTP/2 作为其传输协议,并支持多种语言。而 MQTT 是为低带宽和不可靠的网络设计的轻量级发布/订阅消息传输协议。
+
+-
+
动态性:许多七层协议都是动态的,这意味着它们的行为可能会根据网络条件、应用需求或其他因素而变化。
+
+-
+
加密和安全性:随着安全意识的增强,许多七层协议都采用了加密技术,如 TLS/SSL。这为追踪和分析带来了额外的挑战,因为需要解密流量才能进行深入的分析。
+
+-
+
高性能需求:在高流量的生产环境中,捕获和分析七层协议的流量可能会对系统性能产生影响。传统的网络监控工具可能无法处理大量的并发会话。
+
+-
+
数据的完整性和连续性:与 tcpdump 这样的工具只捕获单独的数据包不同,追踪七层协议需要捕获完整的会话,这可能涉及多个数据包。这要求工具能够正确地重组和解析这些数据包,以提供连续的会话视图。
+
+-
+
代码侵入性:为了深入了解七层协议的行为,开发人员可能需要修改应用程序代码以添加监控功能。这不仅增加了开发和维护的复杂性,而且可能会影响应用程序的性能。
+
+
+正如上文所述,eBPF 提供了一个强大的解决方案,允许我们在内核层面捕获和分析七层协议的流量,而无需对应用程序进行任何修改。这种方法为我们提供了一个独特的机会,可以更简单、更高效地处理应用层流量,特别是在微服务和分布式环境中。
+在处理网络流量和系统行为时,选择在内核态而非用户态进行处理有其独特的优势。首先,内核态处理可以直接访问系统资源和硬件,从而提供更高的性能和效率。其次,由于内核是操作系统的核心部分,它可以提供对系统行为的全面视图,而不受任何用户空间应用程序的限制。
+**无插桩追踪("zero-instrumentation observability")**的优势如下:
+
+- 性能开销小:由于不需要修改或添加额外的代码到应用程序中,所以对性能的影响最小化。
+- 透明性:开发者和运维人员不需要知道应用程序的内部工作原理,也不需要访问源代码。
+- 灵活性:可以轻松地在不同的环境和应用程序中部署和使用,无需进行任何特定的配置或修改。
+- 安全性:由于不需要修改应用程序代码,所以降低了引入潜在安全漏洞的风险。
+
+利用 eBPF 在内核态进行无插桩追踪,我们可以实时捕获和分析系统的行为,而不需要对应用程序进行任何修改。这种方法不仅提供了对系统深入的洞察力,而且确保了最佳的性能和效率。这是为什么 eBPF 成为现代可观测性工具的首选技术,特别是在需要高性能和低延迟的生产环境中。
+
+
+是什么?
+eBPF socket filter 是经典的 Berkeley Packet Filter (BPF) 的扩展,允许在内核中直接进行更高级的数据包过滤。它在套接字层操作,使得可以精细地控制哪些数据包被用户空间应用程序处理。
+主要特点:
+
+- 性能:通过在内核中直接处理数据包,eBPF socket filters 减少了用户和内核空间之间的上下文切换的开销。
+- 灵活性:eBPF socket filters 可以附加到任何套接字,为各种协议和套接字类型提供了通用的数据包过滤机制。
+- 可编程性:开发者可以编写自定义的 eBPF 程序来定义复杂的过滤逻辑,超越简单的数据包匹配。
+
+用途:
+
+- 流量控制:根据自定义条件限制或优先处理流量。
+- 安全性:在它们到达用户空间应用程序之前丢弃恶意数据包。
+- 监控:捕获特定数据包进行分析,而不影响其它流量。
+
+
+是什么?
+使用 eBPF 进行的系统调用跟踪允许监视和操作应用程序发出的系统调用。系统调用是用户空间应用程序与内核交互的主要机制,因此跟踪它们可以深入了解应用程序的行为。
+主要特点:
+
+- 粒度:eBPF 允许跟踪特定的系统调用,甚至是这些系统调用中的特定参数。
+- 低开销:与其他跟踪方法相比,eBPF 系统调用跟踪旨在具有最小的性能影响。
+- 安全性:内核验证 eBPF 程序,以确保它们不会损害系统稳定性。
+
+工作原理:
+eBPF 系统调用跟踪通常涉及将 eBPF 程序附加到与系统调用相关的 tracepoints 或 kprobes。当跟踪的系统调用被调用时,执行 eBPF 程序,允许收集数据或甚至修改系统调用参数。
+
+| 项目 | eBPF Socket Filter | eBPF Syscall Tracing |
+| 操作层 | 套接字层,主要处理从套接字接收或发送的网络数据包 | 系统调用层,监视和可能更改应用程序发出的系统调用的行为 |
+| 主要用途 | 主要用于网络数据包的过滤、监控和操作 | 用于性能分析、安全监控和系统调用交互的调试 |
+| 粒度 | 专注于单个网络数据包 | 可以监视与网络无关的广泛的系统活动 |
+| 追踪 HTTP 流量 | 可以用于过滤和捕获通过套接字传递的 HTTP 数据包 | 可以跟踪与网络操作相关的系统调用 |
+
+
+总之,eBPF 的 socket filter 和 syscall 追踪都可以用于追踪 HTTP 流量,但 socket filters 更直接且更适合此目的。然而,如果您对应用程序如何与系统交互的更广泛的上下文感兴趣(例如,哪些系统调用导致了 HTTP 流量),那么系统调用跟踪将是非常有价值的。在许多高级的可观察性设置中,这两种工具可能会同时使用,以提供系统和网络行为的全面视图。
+
+eBPF 代码由用户态和内核态组成,这里主要关注于内核态代码。这是使用 eBPF socket filter 技术来在内核中捕获HTTP流量的主要逻辑,完整代码如下:
+SEC("socket")
+int socket_handler(struct __sk_buff *skb)
+{
+ struct so_event *e;
+ __u8 verlen;
+ __u16 proto;
+ __u32 nhoff = ETH_HLEN;
+ __u32 ip_proto = 0;
+ __u32 tcp_hdr_len = 0;
+ __u16 tlen;
+ __u32 payload_offset = 0;
+ __u32 payload_length = 0;
+ __u8 hdr_len;
+
+ bpf_skb_load_bytes(skb, 12, &proto, 2);
+ proto = __bpf_ntohs(proto);
+ if (proto != ETH_P_IP)
+ return 0;
+
+ if (ip_is_fragment(skb, nhoff))
+ return 0;
+
+ // ip4 header lengths are variable
+ // access ihl as a u8 (linux/include/linux/skbuff.h)
+ bpf_skb_load_bytes(skb, ETH_HLEN, &hdr_len, sizeof(hdr_len));
+ hdr_len &= 0x0f;
+ hdr_len *= 4;
+
+ /* verify hlen meets minimum size requirements */
+ if (hdr_len < sizeof(struct iphdr))
+ {
+ return 0;
+ }
+
+ bpf_skb_load_bytes(skb, nhoff + offsetof(struct iphdr, protocol), &ip_proto, 1);
+
+ if (ip_proto != IPPROTO_TCP)
+ {
+ return 0;
+ }
+
+ tcp_hdr_len = nhoff + hdr_len;
+ bpf_skb_load_bytes(skb, nhoff + 0, &verlen, 1);
+ bpf_skb_load_bytes(skb, nhoff + offsetof(struct iphdr, tot_len), &tlen, sizeof(tlen));
+
+ __u8 doff;
+ bpf_skb_load_bytes(skb, tcp_hdr_len + offsetof(struct __tcphdr, ack_seq) + 4, &doff, sizeof(doff)); // read the first byte past __tcphdr->ack_seq, we can't do offsetof bit fields
+ doff &= 0xf0; // clean-up res1
+ doff >>= 4; // move the upper 4 bits to low
+ doff *= 4; // convert to bytes length
+
+ payload_offset = ETH_HLEN + hdr_len + doff;
+ payload_length = __bpf_ntohs(tlen) - hdr_len - doff;
+
+ char line_buffer[7];
+ if (payload_length < 7 || payload_offset < 0)
+ {
+ return 0;
+ }
+ bpf_skb_load_bytes(skb, payload_offset, line_buffer, 7);
+ bpf_printk("%d len %d buffer: %s", payload_offset, payload_length, line_buffer);
+ if (bpf_strncmp(line_buffer, 3, "GET") != 0 &&
+ bpf_strncmp(line_buffer, 4, "POST") != 0 &&
+ bpf_strncmp(line_buffer, 3, "PUT") != 0 &&
+ bpf_strncmp(line_buffer, 6, "DELETE") != 0 &&
+ bpf_strncmp(line_buffer, 4, "HTTP") != 0)
+ {
+ return 0;
+ }
+
+ /* reserve sample from BPF ringbuf */
+ e = bpf_ringbuf_reserve(&rb, sizeof(*e), 0);
+ if (!e)
+ return 0;
+
+ e->ip_proto = ip_proto;
+ bpf_skb_load_bytes(skb, nhoff + hdr_len, &(e->ports), 4);
+ e->pkt_type = skb->pkt_type;
+ e->ifindex = skb->ifindex;
+
+ e->payload_length = payload_length;
+ bpf_skb_load_bytes(skb, payload_offset, e->payload, MAX_BUF_SIZE);
+
+ bpf_skb_load_bytes(skb, nhoff + offsetof(struct iphdr, saddr), &(e->src_addr), 4);
+ bpf_skb_load_bytes(skb, nhoff + offsetof(struct iphdr, daddr), &(e->dst_addr), 4);
+ bpf_ringbuf_submit(e, 0);
+
+ return skb->len;
+}
+
+当分析这段eBPF程序时,我们将按照每个代码块的内容来详细解释,并提供相关的背景知识:
+SEC("socket")
+int socket_handler(struct __sk_buff *skb)
+{
+ // ...
+}
+
+这是eBPF程序的入口点,它定义了一个名为 socket_handler 的函数,它会被内核用于处理传入的网络数据包。这个函数位于一个名为 socket 的 eBPF 节(section)中,表明这个程序用于套接字处理。
+struct so_event *e;
+__u8 verlen;
+__u16 proto;
+__u32 nhoff = ETH_HLEN;
+__u32 ip_proto = 0;
+__u32 tcp_hdr_len = 0;
+__u16 tlen;
+__u32 payload_offset = 0;
+__u32 payload_length = 0;
+__u8 hdr_len;
+
+在这个代码块中,我们定义了一些变量来存储在处理数据包时需要的信息。这些变量包括了struct so_event *e用于存储事件信息,verlen、proto、nhoff、ip_proto、tcp_hdr_len、tlen、payload_offset、payload_length、hdr_len等用于存储数据包信息的变量。
+
+struct so_event *e;:这是一个指向so_event结构体的指针,用于存储捕获到的事件信息。该结构体的具体定义在程序的其他部分。
+__u8 verlen;、__u16 proto;、__u32 nhoff = ETH_HLEN;:这些变量用于存储各种信息,例如协议类型、数据包偏移量等。nhoff初始化为以太网帧头部的长度,通常为14字节,因为以太网帧头部包括目标MAC地址、源MAC地址和帧类型字段。
+__u32 ip_proto = 0;:这个变量用于存储IP协议的类型,初始化为0。
+__u32 tcp_hdr_len = 0;:这个变量用于存储TCP头部的长度,初始化为0。
+__u16 tlen;:这个变量用于存储IP数据包的总长度。
+__u32 payload_offset = 0;、__u32 payload_length = 0;:这两个变量用于存储HTTP请求的载荷(payload)的偏移量和长度。
+__u8 hdr_len;:这个变量用于存储IP头部的长度。
+
+bpf_skb_load_bytes(skb, 12, &proto, 2);
+proto = __bpf_ntohs(proto);
+if (proto != ETH_P_IP)
+ return 0;
+
+在这里,代码从数据包中加载了以太网帧的类型字段,这个字段告诉我们数据包使用的网络层协议。然后,使用__bpf_ntohs函数将网络字节序的类型字段转换为主机字节序。接下来,代码检查类型字段是否等于IPv4的以太网帧类型(0x0800)。如果不等于,说明这个数据包不是IPv4数据包,直接返回0,放弃处理。
+这里需要了解以下几个概念:
+
+- 以太网帧(Ethernet Frame):是数据链路层(第二层)的协议,用于在局域网中传输数据帧。以太网帧通常包括目标MAC地址、源MAC地址和帧类型字段。
+- 网络字节序(Network Byte Order):网络协议通常使用大端字节序(Big-Endian)来表示数据。因此,需要将从网络中接收到的数据转换为主机字节序,以便在主机上正确解释数据。
+- IPv4帧类型(ETH_P_IP):表示以太网帧中包含的协议类型字段,0x0800表示IPv4。
+
+if (ip_is_fragment(skb, nhoff))
+ return 0;
+
+这一部分的代码检查是否处理IP分片。IP分片是将较大的IP数据包分割成多个小片段以进行传输的机制。在这里,如果数据包是IP分片,则直接返回0,表示不处理分片,只处理完整的数据包。
+static inline int ip_is_fragment(struct __sk_buff *skb, __u32 nhoff)
+{
+ __u16 frag_off;
+
+ bpf_skb_load_bytes(skb, nhoff + offsetof(struct iphdr, frag_off), &frag_off, 2);
+ frag_off = __bpf_ntohs(frag_off);
+ return frag_off & (IP_MF | IP_OFFSET);
+}
+
+上述代码是一个辅助函数,用于检查传入的IPv4数据包是否为IP分片。IP分片是一种机制,当IP数据包的大小超过了网络的最大传输单元(MTU),路由器会将其分割成多个较小的片段,以便在网络上进行传输。这个函数的目的是检查数据包的分片标志(Fragmentation Flag)以及片偏移(Fragment Offset)字段,以确定是否为分片。
+下面是代码的逐行解释:
+
+__u16 frag_off;:定义一个16位无符号整数变量frag_off,用于存储片偏移字段的值。
+bpf_skb_load_bytes(skb, nhoff + offsetof(struct iphdr, frag_off), &frag_off, 2);:这行代码使用bpf_skb_load_bytes函数从数据包中加载IPv4头部的片偏移字段(frag_off),并加载2个字节。nhoff是IPv4头部在数据包中的偏移量,offsetof(struct iphdr, frag_off)用于计算片偏移字段在IPv4头部中的偏移量。
+frag_off = __bpf_ntohs(frag_off);:将加载的片偏移字段从网络字节序(Big-Endian)转换为主机字节序。网络协议通常使用大端字节序表示数据,而主机可能使用大端或小端字节序。这里将片偏移字段转换为主机字节序,以便进一步处理。
+return frag_off & (IP_MF | IP_OFFSET);:这行代码通过使用位运算检查片偏移字段的值,以确定是否为IP分片。具体来说,它使用位与运算符&将片偏移字段与两个标志位进行位与运算:
+
+IP_MF:表示"更多分片"标志(More Fragments)。如果这个标志位被设置为1,表示数据包是分片的一部分,还有更多分片。
+IP_OFFSET:表示片偏移字段。如果片偏移字段不为0,表示数据包是分片的一部分,且具有片偏移值。
+如果这两个标志位中的任何一个被设置为1,那么结果就不为零,说明数据包是IP分片。如果都为零,说明数据包不是分片。
+
+
+
+需要注意的是,IP头部的片偏移字段以8字节为单位,所以实际的片偏移值需要左移3位来得到字节偏移。此外,IP头部的"更多分片"标志(IP_MF)表示数据包是否有更多的分片,通常与片偏移字段一起使用来指示整个数据包的分片情况。这个函数只关心这两个标志位,如果其中一个标志被设置,就认为是IP分片。
+bpf_skb_load_bytes(skb, ETH_HLEN, &hdr_len, sizeof(hdr_len));
+hdr_len &= 0x0f;
+hdr_len *= 4;
+
+这一部分的代码从数据包中加载IP头部的长度字段。IP头部长度字段包含了IP头部的长度信息,以4字节为单位,需要将其转换为字节数。这里通过按位与和乘以4来进行转换。
+需要了解:
+
+- IP头部(IP Header):IP头部包含了关于数据包的基本信息,如源IP地址、目标IP地址、协议类型和头部校验和等。头部长度字段(IHL,Header Length)表示IP头部的长度,以4字节为单位,通常为20字节(5个4字节的字)。
+
+if (hdr_len < sizeof(struct iphdr))
+{
+ return 0;
+}
+
+这段代码检查IP头部的长度是否满足最小长度要求,通常IP头部的最小长度是20字节。如果IP头部的长度小于20字节,说明数据包不完整或损坏,直接返回0,放弃处理。
+需要了解:
+
+struct iphdr:这是Linux内核中定义的结构体,表示IPv4头部的格式。它包括了版本、头部长度、服务类型、总长度、
+
+标识符、标志位、片偏移、生存时间、协议、头部校验和、源IP地址和目标IP地址等字段。
+bpf_skb_load_bytes(skb, nhoff + offsetof(struct iphdr, protocol), &ip_proto, 1);
+if (ip_proto != IPPROTO_TCP)
+{
+ return 0;
+}
+
+在这里,代码从数据包中加载IP头部中的协议字段,以确定数据包使用的传输层协议。然后,它检查协议字段是否为TCP协议(IPPROTO_TCP)。如果不是TCP协议,说明不是HTTP请求或响应,直接返回0。
+需要了解:
+
+- 传输层协议:IP头部中的协议字段指示了数据包所使用的传输层协议,例如TCP、UDP或ICMP。
+
+tcp_hdr_len = nhoff + hdr_len;
+
+这行代码计算了TCP头部的偏移量。它将以太网帧头部的长度(nhoff)与IP头部的长度(hdr_len)相加,得到TCP头部的起始位置。
+bpf_skb_load_bytes(skb, nhoff + 0, &verlen, 1);
+
+这行代码从数据包中加载TCP头部的第一个字节,该字节包含了TCP头部长度信息。这个长度字段以4字节为单位,需要进行后续的转换。
+bpf_skb_load_bytes(skb, nhoff + offsetof(struct iphdr, tot_len), &tlen, sizeof(tlen));
+
+这行代码从数据包中加载IP头部的总长度字段。IP头部总长度字段表示整个IP数据包的长度,包括IP头部和数据部分。
+__u8 doff;
+bpf_skb_load_bytes(skb, tcp_hdr_len + offsetof(struct __tcphdr, ack_seq) + 4, &doff, sizeof(doff));
+doff &= 0xf0;
+doff >>= 4;
+doff *= 4;
+
+这段代码用于计算TCP头部的长度。它加载TCP头部中的数据偏移字段(Data Offset,也称为头部长度字段),该字段表示TCP头部的长度以4字节为单位。代码将偏移字段的高四位清零,然后将其右移4位,最后乘以4,得到TCP头部的实际长度。
+需要了解:
+
+- TCP头部(TCP Header):TCP头部包含了TCP协议相关的信息,如源端口、目标端口、序列号、确认号、标志位(如SYN、ACK、FIN等)、窗口大小和校验和等。
+
+payload_offset = ETH_HLEN + hdr_len + doff;
+payload_length = __bpf_ntohs(tlen) - hdr_len - doff;
+
+这两行代码计算HTTP请求的载荷(payload)的偏移量和长度。它们将以太网帧头部长度、IP头部长度和TCP头部长度相加,得到HTTP请求的数据部分的偏移量,然后通过减去总长度、IP头部长度和TCP头部长度,计算出HTTP请求数据的长度。
+需要了解:
+
+- HTTP请求载荷(Payload):HTTP请求中包含的实际数据部分,通常是HTTP请求头和请求体。
+
+char line_buffer[7];
+if (payload_length < 7 || payload_offset < 0)
+{
+ return 0;
+}
+bpf_skb_load_bytes(skb, payload_offset, line_buffer, 7);
+bpf_printk("%d len %d buffer: %s", payload_offset, payload_length, line_buffer);
+
+这部分代码用于加载HTTP请求行的前7个字节,存储在名为line_buffer的字符数组中。然后,它检查HTTP请求数据的长度是否小于7字节或偏移量是否为负数,如果满足这些条件,说明HTTP请求不完整,直接返回0。最后,它使用bpf_printk函数将HTTP请求行的内容打印到内核日志中,以供调试和分析。
+if (bpf_strncmp(line_buffer, 3, "GET") != 0 &&
+ bpf_strncmp(line_buffer, 4, "POST") != 0 &&
+ bpf_strncmp(line_buffer, 3, "PUT") != 0 &&
+ bpf_strncmp(line_buffer, 6, "DELETE") != 0 &&
+ bpf_strncmp(line_buffer, 4, "HTTP") != 0)
+{
+ return 0;
+}
+
+这段代码使用bpf_strncmp函数比较line_buffer中的数据与HTTP请求方法(GET、POST、PUT、DELETE、HTTP)是否匹配。如果不匹配,说明不是HTTP请求,直接返回0,放弃处理。
+e = bpf_ringbuf_reserve(&rb, sizeof(*e), 0);
+if (!e)
+ return 0;
+
+这部分代码尝试从BPF环形缓冲区中保留一块内存以存储事件信息。如果无法保留内存块,返回0。BPF环形缓冲区用于在eBPF程序和用户空间之间传递事件数据。
+需要了解:
+
+- BPF环形缓冲区:BPF环形缓冲区是一种在eBPF程序和用户空间之间传递数据的机制。它可以用来存储事件信息,以便用户空间应用程序进行进一步处理或分析。
+
+e->ip_proto = ip_proto;
+bpf_skb_load_bytes(skb, nhoff + hdr_len, &(e->ports), 4);
+e->pkt_type = skb->pkt_type;
+e->ifindex = skb->ifindex;
+
+e->payload_length = payload_length;
+bpf_skb_load_bytes(skb, payload_offset, e->payload, MAX_BUF_SIZE);
+
+bpf_skb_load_bytes(skb, nhoff + offsetof(struct iphdr, saddr), &(e->src_addr), 4);
+bpf_skb_load_bytes(skb, nhoff + offsetof(struct iphdr, daddr), &(e->dst_addr), 4);
+bpf_ringbuf_submit(e, 0);
+
+return skb->len;
+
+最后,这段代码将捕获到的事件信息存储在e结构体中,并将
+其提交到BPF环形缓冲区。它包括了捕获的IP协议、源端口和目标端口、数据包类型、接口索引、载荷长度、源IP地址和目标IP地址等信息。最后,它返回数据包的长度,表示成功处理了数据包。
+这段代码主要用于将捕获的事件信息存储起来,以便后续的处理和分析。 BPF环形缓冲区用于将这些信息传递到用户空间,供用户空间应用程序进一步处理或记录。
+总结:这段eBPF程序的主要任务是捕获HTTP请求,它通过解析数据包的以太网帧、IP头部和TCP头部来确定数据包是否包含HTTP请求,并将有关请求的信息存储在so_event结构体中,然后提交到BPF环形缓冲区。这是一种高效的方法,可以在内核层面捕获HTTP流量,适用于网络监控和安全分析等应用。
+
+上述代码也存在一些潜在的缺陷,其中一个主要缺陷是它无法处理跨多个数据包的URL。
+
+- 跨包URL:代码中通过解析单个数据包来检查HTTP请求中的URL,如果HTTP请求的URL跨足够多的数据包,那么只会检查第一个数据包中的URL部分。这会导致丢失或部分记录那些跨多个数据包的长URL。
+
+解决这个问题的方法通常需要对多个数据包进行重新组装,以还原完整的HTTP请求。这可能需要在eBPF程序中实现数据包的缓存和组装逻辑,并在检测到HTTP请求结束之前等待并收集所有相关数据包。这需要更复杂的逻辑和额外的内存来处理跨多个数据包的情况。
+
+用户态代码的主要目的是创建一个原始套接字(raw socket),然后将先前在内核中定义的eBPF程序附加到该套接字上,从而允许eBPF程序捕获和处理从该套接字接收到的网络数据包,例如:
+ /* Create raw socket for localhost interface */
+ sock = open_raw_sock(interface);
+ if (sock < 0) {
+ err = -2;
+ fprintf(stderr, "Failed to open raw socket\n");
+ goto cleanup;
+ }
+
+ /* Attach BPF program to raw socket */
+ prog_fd = bpf_program__fd(skel->progs.socket_handler);
+ if (setsockopt(sock, SOL_SOCKET, SO_ATTACH_BPF, &prog_fd, sizeof(prog_fd))) {
+ err = -3;
+ fprintf(stderr, "Failed to attach to raw socket\n");
+ goto cleanup;
+ }
+
+
+sock = open_raw_sock(interface);:这行代码调用了一个自定义的函数open_raw_sock,该函数用于创建一个原始套接字。原始套接字允许用户态应用程序直接处理网络数据包,而不经过协议栈的处理。函数open_raw_sock可能需要一个参数 interface,用于指定网络接口,以便确定从哪个接口接收数据包。如果创建套接字失败,它将返回一个负数,否则返回套接字的文件描述符sock。
+- 如果
sock的值小于0,表示打开原始套接字失败,那么将err设置为-2,并在标准错误流上输出一条错误信息。
+prog_fd = bpf_program__fd(skel->progs.socket_handler);:这行代码获取之前在eBPF程序定义中的套接字过滤器程序(socket_handler)的文件描述符,以便后续将它附加到套接字上。skel是一个eBPF程序对象的指针,可以通过它来访问程序集合。
+setsockopt(sock, SOL_SOCKET, SO_ATTACH_BPF, &prog_fd, sizeof(prog_fd)):这行代码使用setsockopt系统调用将eBPF程序附加到原始套接字。它设置了SO_ATTACH_BPF选项,将eBPF程序的文件描述符传递给该选项,以便内核知道要将哪个eBPF程序应用于这个套接字。如果附加成功,套接字将开始捕获和处理从中接收到的网络数据包。
+- 如果
setsockopt失败,它将err设置为-3,并在标准错误流上输出一条错误信息。
+
+
+完整的源代码可以在 https://github.com/eunomia-bpf/bpf-developer-tutorial/tree/main/src/23-http 中找到。编译运行上述代码:
+$ git submodule update --init --recursive
+$ make
+ BPF .output/sockfilter.bpf.o
+ GEN-SKEL .output/sockfilter.skel.h
+ CC .output/sockfilter.o
+ BINARY sockfilter
+$ sudo ./sockfilter
+...
+
+在另外一个窗口中,使用 python 启动一个简单的 web server:
+python3 -m http.server
+Serving HTTP on 0.0.0.0 port 8000 (http://0.0.0.0:8000/) ...
+127.0.0.1 - - [18/Sep/2023 01:05:52] "GET / HTTP/1.1" 200 -
+
+可以使用 curl 发起请求:
+$ curl http://0.0.0.0:8000/
+<!DOCTYPE HTML>
+<html lang="en">
+<head>
+<meta charset="utf-8">
+<title>Directory listing for /</title>
+....
+
+在 eBPF 程序中,可以看到打印出了 HTTP 请求的内容:
+127.0.0.1:34552(src) -> 127.0.0.1:8000(dst)
+payload: GET / HTTP/1.1
+Host: 0.0.0.0:8000
+User-Agent: curl/7.88.1
+...
+127.0.0.1:8000(src) -> 127.0.0.1:34552(dst)
+payload: HTTP/1.0 200 OK
+Server: SimpleHTTP/0.6 Python/3.11.4
+...
+
+分别包含了请求和响应的内容。
+
+eBPF 提供了一种强大的机制,允许我们在内核级别追踪系统调用。在这个示例中,我们将使用 eBPF 追踪 accept 和 read 系统调用,以捕获 HTTP 流量。由于篇幅有限,这里我们仅仅对代码框架做简要的介绍。
+struct
+{
+ __uint(type, BPF_MAP_TYPE_HASH);
+ __uint(max_entries, 4096);
+ __type(key, u64);
+ __type(value, struct accept_args_t);
+} active_accept_args_map SEC(".maps");
+
+// 定义在 accept 系统调用入口的追踪点
+SEC("tracepoint/syscalls/sys_enter_accept")
+int sys_enter_accept(struct trace_event_raw_sys_enter *ctx)
+{
+ u64 id = bpf_get_current_pid_tgid();
+ // ... 获取和存储 accept 调用的参数
+ bpf_map_update_elem(&active_accept_args_map, &id, &accept_args, BPF_ANY);
+ return 0;
+}
+
+// 定义在 accept 系统调用退出的追踪点
+SEC("tracepoint/syscalls/sys_exit_accept")
+int sys_exit_accept(struct trace_event_raw_sys_exit *ctx)
+{
+ // ... 处理 accept 调用的结果
+ struct accept_args_t *args =
+ bpf_map_lookup_elem(&active_accept_args_map, &id);
+ // ... 获取和存储 accept 调用获得的 socket 文件描述符
+ __u64 pid_fd = ((__u64)pid << 32) | (u32)ret_fd;
+ bpf_map_update_elem(&conn_info_map, &pid_fd, &conn_info, BPF_ANY);
+ // ...
+}
+
+struct
+{
+ __uint(type, BPF_MAP_TYPE_HASH);
+ __uint(max_entries, 4096);
+ __type(key, u64);
+ __type(value, struct data_args_t);
+} active_read_args_map SEC(".maps");
+
+// 定义在 read 系统调用入口的追踪点
+SEC("tracepoint/syscalls/sys_enter_read")
+int sys_enter_read(struct trace_event_raw_sys_enter *ctx)
+{
+ // ... 获取和存储 read 调用的参数
+ bpf_map_update_elem(&active_read_args_map, &id, &read_args, BPF_ANY);
+ return 0;
+}
+
+// 辅助函数,检查是否为 HTTP 连接
+static inline bool is_http_connection(const char *line_buffer, u64 bytes_count)
+{
+ // ... 检查数据是否为 HTTP 请求或响应
+}
+
+// 辅助函数,处理读取的数据
+static inline void process_data(struct trace_event_raw_sys_exit *ctx,
+ u64 id, const struct data_args_t *args, u64 bytes_count)
+{
+ // ... 处理读取的数据,检查是否为 HTTP 流量,并发送事件
+ if (is_http_connection(line_buffer, bytes_count))
+ {
+ // ...
+ bpf_probe_read_kernel(&event.msg, read_size, args->buf);
+ // ...
+ bpf_perf_event_output(ctx, &events, BPF_F_CURRENT_CPU,
+ &event, sizeof(struct socket_data_event_t));
+ }
+}
+
+// 定义在 read 系统调用退出的追踪点
+SEC("tracepoint/syscalls/sys_exit_read")
+int sys_exit_read(struct trace_event_raw_sys_exit *ctx)
+{
+ // ... 处理 read 调用的结果
+ struct data_args_t *read_args = bpf_map_lookup_elem(&active_read_args_map, &id);
+ if (read_args != NULL)
+ {
+ process_data(ctx, id, read_args, bytes_count);
+ }
+ // ...
+ return 0;
+}
+
+char _license[] SEC("license") = "GPL";
+
+这段代码简要展示了如何使用eBPF追踪Linux内核中的系统调用来捕获HTTP流量。以下是对代码的hook位置和流程的详细解释,以及需要hook哪些系统调用来实现完整的请求追踪:
+
+
+-
+
该代码使用了eBPF的Tracepoint功能,具体来说,它定义了一系列的eBPF程序,并将它们绑定到了特定的系统调用的Tracepoint上,以捕获这些系统调用的入口和退出事件。
+
+-
+
首先,它定义了两个eBPF哈希映射(active_accept_args_map和active_read_args_map)来存储系统调用参数。这些映射用于跟踪accept和read系统调用。
+
+-
+
接着,它定义了多个Tracepoint追踪程序,其中包括:
+
+sys_enter_accept:定义在accept系统调用的入口处,用于捕获accept系统调用的参数,并将它们存储在哈希映射中。
+sys_exit_accept:定义在accept系统调用的退出处,用于处理accept系统调用的结果,包括获取和存储新的套接字文件描述符以及建立连接的相关信息。
+sys_enter_read:定义在read系统调用的入口处,用于捕获read系统调用的参数,并将它们存储在哈希映射中。
+sys_exit_read:定义在read系统调用的退出处,用于处理read系统调用的结果,包括检查读取的数据是否为HTTP流量,如果是,则发送事件。
+
+
+-
+
在sys_exit_accept和sys_exit_read中,还涉及一些数据处理和事件发送的逻辑,例如检查数据是否为HTTP连接,组装事件数据,并使用bpf_perf_event_output将事件发送到用户空间供进一步处理。
+
+
+
+要实现完整的HTTP请求追踪,通常需要hook的系统调用包括:
+
+socket:用于捕获套接字创建,以追踪新的连接。
+bind:用于获取绑定的端口信息。
+listen:用于开始监听连接请求。
+accept:用于接受连接请求,获取新的套接字文件描述符。
+read:用于捕获接收到的数据,以检查其中是否包含 HTTP 请求。
+write:用于捕获发送的数据,以检查其中是否包含 HTTP 响应。
+
+上述代码已经涵盖了accept和read系统调用的追踪。要完整实现HTTP请求的追踪,还需要hook其他系统调用,并实现相应的逻辑来处理这些系统调用的参数和结果。
+完整的源代码可以在 https://github.com/eunomia-bpf/bpf-developer-tutorial/tree/main/src/23-http 中找到。
+
+在当今复杂的技术环境中,系统的可观测性变得至关重要,特别是在微服务和云原生应用程序的背景下。本文探讨了如何利用eBPF技术来追踪七层协议,以及在这个过程中可能面临的挑战和解决方案。以下是对本文内容的总结:
+
+-
+
背景介绍:
+
+- 现代应用程序通常由多个微服务和分布式组件组成,因此观测整个系统的行为至关重要。
+- 七层协议(如HTTP、gRPC、MQTT等)提供了深入了解应用程序交互的详细信息,但监控这些协议通常具有挑战性。
+
+
+-
+
eBPF技术的作用:
+
+- eBPF允许开发者在不修改或插入应用程序代码的情况下,深入内核层来实时观测和分析系统行为。
+- eBPF技术为监控七层协议提供了一个强大的工具,特别适用于微服务环境。
+
+
+-
+
追踪七层协议:
+
+- 本文介绍了如何追踪HTTP等七层协议的挑战,包括协议的复杂性和动态性。
+- 传统的网络监控工具难以应对七层协议的复杂性。
+
+
+-
+
eBPF的应用:
+
+- eBPF提供两种主要方法来追踪七层协议:socket filter和syscall trace。
+- 这两种方法可以帮助捕获HTTP等协议的网络请求数据,并分析它们。
+
+
+-
+
eBPF实践教程:
+
+- 本文提供了一个实际的eBPF教程,演示如何使用eBPF socket filter或syscall trace来捕获和分析HTTP流量。
+- 教程内容包括开发eBPF程序、使用eBPF工具链和实施HTTP请求的追踪。
+
+
+
+通过这篇文章,读者可以获得深入了解如何使用eBPF技术来追踪七层协议,尤其是HTTP流量的知识。这将有助于更好地监控和分析网络流量,从而提高应用程序性能和安全性。如果您希望学习更多关于 eBPF 的知识和实践,可以访问我们的教程代码仓库 https://github.com/eunomia-bpf/bpf-developer-tutorial 或网站 https://eunomia.dev/zh/tutorials/ 以获取更多示例和完整的教程。
diff --git a/23-http/main.go b/23-http/main.go
deleted file mode 100644
index 608e85d..0000000
--- a/23-http/main.go
+++ /dev/null
@@ -1,103 +0,0 @@
-/*
- * Copyright 2018- The Pixie Authors.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- *
- * SPDX-License-Identifier: Apache-2.0
- */
-
- package main
-
- import (
- "fmt"
- bpfwrapper2 "github.com/seek-ret/ebpf-training/workshop1/internal/bpfwrapper"
- "github.com/seek-ret/ebpf-training/workshop1/internal/connections"
- "github.com/seek-ret/ebpf-training/workshop1/internal/settings"
- "io/ioutil"
- "log"
- "os"
- "os/signal"
- "os/user"
- "runtime/debug"
- "syscall"
- "time"
-
- "github.com/iovisor/gobpf/bcc"
- )
-
- // abortIfNotRoot checks the current user permissions, if the permissions are not elevated, we abort.
- func abortIfNotRoot() {
- current, err := user.Current()
- if err != nil {
- log.Panic(err)
- }
-
- if current.Uid != "0" {
- log.Panic("sniffer must run under superuser privileges")
- }
- }
-
- // recoverFromCrashes is a defer function that caches all panics being thrown from the application.
- func recoverFromCrashes() {
- if err := recover(); err != nil {
- log.Printf("Application crashed: %v\nstack: %s\n", err, string(debug.Stack()))
- }
- }
-
- func main() {
- if len(os.Args) != 2 {
- fmt.Println("Usage: go run main.go
")
- os.Exit(1)
- }
- bpfSourceCodeFile := os.Args[1]
- bpfSourceCodeContent, err := ioutil.ReadFile(bpfSourceCodeFile)
- if err != nil {
- log.Panic(err)
- }
-
- defer recoverFromCrashes()
- abortIfNotRoot()
-
- if err := settings.InitRealTimeOffset(); err != nil {
- log.Printf("Failed fixing BPF clock, timings will be offseted: %v", err)
- }
-
- // Catching all termination signals to perform a cleanup when being stopped.
- sig := make(chan os.Signal, 1)
- signal.Notify(sig, syscall.SIGHUP, syscall.SIGINT, syscall.SIGQUIT, syscall.SIGTERM)
-
- bpfModule := bcc.NewModule(string(bpfSourceCodeContent), nil)
- if bpfModule == nil {
- log.Panic("bpf is nil")
- }
- defer bpfModule.Close()
-
- connectionFactory := connections.NewFactory(time.Minute)
- go func() {
- for {
- connectionFactory.HandleReadyConnections()
- time.Sleep(10 * time.Second)
- }
- }()
- if err := bpfwrapper2.LaunchPerfBufferConsumers(bpfModule, connectionFactory); err != nil {
- log.Panic(err)
- }
-
- // Lastly, after everything is ready and configured, attach the kprobes and start capturing traffic.
- if err := bpfwrapper2.AttachKprobes(bpfModule); err != nil {
- log.Panic(err)
- }
- log.Println("Sniffer is ready")
- <-sig
- log.Println("Signaled to terminate")
- }
diff --git a/23-http/sockfilter.bpf.c b/23-http/sockfilter.bpf.c
new file mode 100644
index 0000000..629076a
--- /dev/null
+++ b/23-http/sockfilter.bpf.c
@@ -0,0 +1,136 @@
+// SPDX-License-Identifier: GPL-2.0 OR BSD-3-Clause
+/* Copyright (c) 2022 Jacky Yin */
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include "sockfilter.h"
+
+#define IP_MF 0x2000
+#define IP_OFFSET 0x1FFF
+#define IP_TCP 6
+#define ETH_HLEN 14
+
+char LICENSE[] SEC("license") = "Dual BSD/GPL";
+
+struct
+{
+ __uint(type, BPF_MAP_TYPE_RINGBUF);
+ __uint(max_entries, 256 * 1024);
+} rb SEC(".maps");
+
+// Taken from uapi/linux/tcp.h
+struct __tcphdr
+{
+ __be16 source;
+ __be16 dest;
+ __be32 seq;
+ __be32 ack_seq;
+ __u16 res1 : 4, doff : 4, fin : 1, syn : 1, rst : 1, psh : 1, ack : 1, urg : 1, ece : 1, cwr : 1;
+ __be16 window;
+ __sum16 check;
+ __be16 urg_ptr;
+};
+
+static inline int ip_is_fragment(struct __sk_buff *skb, __u32 nhoff)
+{
+ __u16 frag_off;
+
+ bpf_skb_load_bytes(skb, nhoff + offsetof(struct iphdr, frag_off), &frag_off, 2);
+ frag_off = __bpf_ntohs(frag_off);
+ return frag_off & (IP_MF | IP_OFFSET);
+}
+
+SEC("socket")
+int socket_handler(struct __sk_buff *skb)
+{
+ struct so_event *e;
+ __u8 verlen;
+ __u16 proto;
+ __u32 nhoff = ETH_HLEN;
+ __u32 ip_proto = 0;
+ __u32 tcp_hdr_len = 0;
+ __u16 tlen;
+ __u32 payload_offset = 0;
+ __u32 payload_length = 0;
+ __u8 hdr_len;
+
+ bpf_skb_load_bytes(skb, 12, &proto, 2);
+ proto = __bpf_ntohs(proto);
+ if (proto != ETH_P_IP)
+ return 0;
+
+ if (ip_is_fragment(skb, nhoff))
+ return 0;
+
+ // ip4 header lengths are variable
+ // access ihl as a u8 (linux/include/linux/skbuff.h)
+ bpf_skb_load_bytes(skb, ETH_HLEN, &hdr_len, sizeof(hdr_len));
+ hdr_len &= 0x0f;
+ hdr_len *= 4;
+
+ /* verify hlen meets minimum size requirements */
+ if (hdr_len < sizeof(struct iphdr))
+ {
+ return 0;
+ }
+
+ bpf_skb_load_bytes(skb, nhoff + offsetof(struct iphdr, protocol), &ip_proto, 1);
+
+ if (ip_proto != IPPROTO_TCP)
+ {
+ return 0;
+ }
+
+ tcp_hdr_len = nhoff + hdr_len;
+ bpf_skb_load_bytes(skb, nhoff + 0, &verlen, 1);
+ bpf_skb_load_bytes(skb, nhoff + offsetof(struct iphdr, tot_len), &tlen, sizeof(tlen));
+
+ __u8 doff;
+ bpf_skb_load_bytes(skb, tcp_hdr_len + offsetof(struct __tcphdr, ack_seq) + 4, &doff, sizeof(doff)); // read the first byte past __tcphdr->ack_seq, we can't do offsetof bit fields
+ doff &= 0xf0; // clean-up res1
+ doff >>= 4; // move the upper 4 bits to low
+ doff *= 4; // convert to bytes length
+
+ payload_offset = ETH_HLEN + hdr_len + doff;
+ payload_length = __bpf_ntohs(tlen) - hdr_len - doff;
+
+ char line_buffer[7];
+ if (payload_length < 7 || payload_offset < 0)
+ {
+ return 0;
+ }
+ bpf_skb_load_bytes(skb, payload_offset, line_buffer, 7);
+ bpf_printk("%d len %d buffer: %s", payload_offset, payload_length, line_buffer);
+ if (bpf_strncmp(line_buffer, 3, "GET") != 0 &&
+ bpf_strncmp(line_buffer, 4, "POST") != 0 &&
+ bpf_strncmp(line_buffer, 3, "PUT") != 0 &&
+ bpf_strncmp(line_buffer, 6, "DELETE") != 0 &&
+ bpf_strncmp(line_buffer, 4, "HTTP") != 0)
+ {
+ return 0;
+ }
+
+ /* reserve sample from BPF ringbuf */
+ e = bpf_ringbuf_reserve(&rb, sizeof(*e), 0);
+ if (!e)
+ return 0;
+
+ e->ip_proto = ip_proto;
+ bpf_skb_load_bytes(skb, nhoff + hdr_len, &(e->ports), 4);
+ e->pkt_type = skb->pkt_type;
+ e->ifindex = skb->ifindex;
+
+ e->payload_length = payload_length;
+ bpf_skb_load_bytes(skb, payload_offset, e->payload, MAX_BUF_SIZE);
+
+ bpf_skb_load_bytes(skb, nhoff + offsetof(struct iphdr, saddr), &(e->src_addr), 4);
+ bpf_skb_load_bytes(skb, nhoff + offsetof(struct iphdr, daddr), &(e->dst_addr), 4);
+ bpf_ringbuf_submit(e, 0);
+
+ return skb->len;
+}
diff --git a/23-http/sockfilter.c b/23-http/sockfilter.c
new file mode 100644
index 0000000..0f913f2
--- /dev/null
+++ b/23-http/sockfilter.c
@@ -0,0 +1,149 @@
+// SPDX-License-Identifier: GPL-2.0 OR BSD-3-Clause
+/* Copyright (c) 2022 Jacky Yin */
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include "sockfilter.h"
+#include "sockfilter.skel.h"
+
+static int open_raw_sock(const char *name)
+{
+ struct sockaddr_ll sll;
+ int sock;
+
+ sock = socket(PF_PACKET, SOCK_RAW | SOCK_NONBLOCK | SOCK_CLOEXEC, htons(ETH_P_ALL));
+ if (sock < 0) {
+ fprintf(stderr, "Failed to create raw socket\n");
+ return -1;
+ }
+
+ memset(&sll, 0, sizeof(sll));
+ sll.sll_family = AF_PACKET;
+ sll.sll_ifindex = if_nametoindex(name);
+ sll.sll_protocol = htons(ETH_P_ALL);
+ if (bind(sock, (struct sockaddr *)&sll, sizeof(sll)) < 0) {
+ fprintf(stderr, "Failed to bind to %s: %s\n", name, strerror(errno));
+ close(sock);
+ return -1;
+ }
+
+ return sock;
+}
+
+static int libbpf_print_fn(enum libbpf_print_level level, const char *format, va_list args)
+{
+ return vfprintf(stderr, format, args);
+}
+
+static inline void ltoa(uint32_t addr, char *dst)
+{
+ snprintf(dst, 16, "%u.%u.%u.%u", (addr >> 24) & 0xFF, (addr >> 16) & 0xFF,
+ (addr >> 8) & 0xFF, (addr & 0xFF));
+}
+
+static int handle_event(void *ctx, void *data, size_t data_sz)
+{
+ const struct so_event *e = data;
+ char ifname[IF_NAMESIZE];
+ char sstr[16] = {}, dstr[16] = {};
+
+ if (e->pkt_type != PACKET_HOST)
+ return 0;
+
+ if (e->ip_proto < 0 || e->ip_proto >= IPPROTO_MAX)
+ return 0;
+
+ if (!if_indextoname(e->ifindex, ifname))
+ return 0;
+
+ ltoa(ntohl(e->src_addr), sstr);
+ ltoa(ntohl(e->dst_addr), dstr);
+
+ printf("%s:%d(src) -> %s:%d(dst)\n", sstr, ntohs(e->port16[0]), dstr, ntohs(e->port16[1]));
+ printf("payload: %s\n", e->payload);
+ return 0;
+}
+
+static volatile bool exiting = false;
+
+static void sig_handler(int sig)
+{
+ exiting = true;
+}
+
+int main(int argc, char **argv)
+{
+ struct ring_buffer *rb = NULL;
+ struct sockfilter_bpf *skel;
+ int err, prog_fd, sock;
+
+ const char* interface = "lo";
+
+ /* Set up libbpf errors and debug info callback */
+ libbpf_set_print(libbpf_print_fn);
+
+ /* Cleaner handling of Ctrl-C */
+ signal(SIGINT, sig_handler);
+ signal(SIGTERM, sig_handler);
+
+ /* Load and verify BPF programs*/
+ skel = sockfilter_bpf__open_and_load();
+ if (!skel) {
+ fprintf(stderr, "Failed to open and load BPF skeleton\n");
+ return 1;
+ }
+
+ /* Set up ring buffer polling */
+ rb = ring_buffer__new(bpf_map__fd(skel->maps.rb), handle_event, NULL, NULL);
+ if (!rb) {
+ err = -1;
+ fprintf(stderr, "Failed to create ring buffer\n");
+ goto cleanup;
+ }
+
+ /* Create raw socket for localhost interface */
+ sock = open_raw_sock(interface);
+ if (sock < 0) {
+ err = -2;
+ fprintf(stderr, "Failed to open raw socket\n");
+ goto cleanup;
+ }
+
+ /* Attach BPF program to raw socket */
+ prog_fd = bpf_program__fd(skel->progs.socket_handler);
+ if (setsockopt(sock, SOL_SOCKET, SO_ATTACH_BPF, &prog_fd, sizeof(prog_fd))) {
+ err = -3;
+ fprintf(stderr, "Failed to attach to raw socket\n");
+ goto cleanup;
+ }
+
+ /* Process events */
+ while (!exiting) {
+ err = ring_buffer__poll(rb, 100 /* timeout, ms */);
+ /* Ctrl-C will cause -EINTR */
+ if (err == -EINTR) {
+ err = 0;
+ break;
+ }
+ if (err < 0) {
+ fprintf(stderr, "Error polling perf buffer: %d\n", err);
+ break;
+ }
+ sleep(1);
+ }
+
+cleanup:
+ ring_buffer__free(rb);
+ sockfilter_bpf__destroy(skel);
+ return -err;
+}
\ No newline at end of file
diff --git a/23-http/sockfilter.h b/23-http/sockfilter.h
new file mode 100644
index 0000000..b302b28
--- /dev/null
+++ b/23-http/sockfilter.h
@@ -0,0 +1,22 @@
+// SPDX-License-Identifier: GPL-2.0 OR BSD-3-Clause
+/* Copyright (c) 2022 Jacky Yin */
+#ifndef __SOCKFILTER_H
+#define __SOCKFILTER_H
+
+#define MAX_BUF_SIZE 64
+
+struct so_event {
+ __be32 src_addr;
+ __be32 dst_addr;
+ union {
+ __be32 ports;
+ __be16 port16[2];
+ };
+ __u32 ip_proto;
+ __u32 pkt_type;
+ __u32 ifindex;
+ __u32 payload_length;
+ __u8 payload[MAX_BUF_SIZE];
+};
+
+#endif /* __SOCKFILTER_H */
diff --git a/23-http/sourcecode.c b/23-http/sourcecode.c
deleted file mode 100644
index 01bfc92..0000000
--- a/23-http/sourcecode.c
+++ /dev/null
@@ -1,497 +0,0 @@
-// +build ignore
-
-/*
- * Copyright 2018- The Pixie Authors.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- *
- * SPDX-License-Identifier: Apache-2.0
- */
-
-#include
-#include
-#include
-#include
-
-// Defines
-
-#define socklen_t size_t
-
-// Data buffer message size. BPF can submit at most this amount of data to a perf buffer.
-// Kernel size limit is 32KiB. See https://github.com/iovisor/bcc/issues/2519 for more details.
-#define MAX_MSG_SIZE 30720 // 30KiB
-
-// This defines how many chunks a perf_submit can support.
-// This applies to messages that are over MAX_MSG_SIZE,
-// and effectively makes the maximum message size to be CHUNK_LIMIT*MAX_MSG_SIZE.
-#define CHUNK_LIMIT 4
-
-enum traffic_direction_t {
- kEgress,
- kIngress,
-};
-
-// Structs
-
-// A struct representing a unique ID that is composed of the pid, the file
-// descriptor and the creation time of the struct.
-struct conn_id_t {
- // Process ID
- uint32_t pid;
- // The file descriptor to the opened network connection.
- int32_t fd;
- // Timestamp at the initialization of the struct.
- uint64_t tsid;
-};
-
-// This struct contains information collected when a connection is established,
-// via an accept4() syscall.
-struct conn_info_t {
- // Connection identifier.
- struct conn_id_t conn_id;
-
- // The number of bytes written/read on this connection.
- int64_t wr_bytes;
- int64_t rd_bytes;
-
- // A flag indicating we identified the connection as HTTP.
- bool is_http;
-};
-
-// An helper struct that hold the addr argument of the syscall.
-struct accept_args_t {
- struct sockaddr_in* addr;
-};
-
-// An helper struct to cache input argument of read/write syscalls between the
-// entry hook and the exit hook.
-struct data_args_t {
- int32_t fd;
- const char* buf;
-};
-
-// An helper struct that hold the input arguments of the close syscall.
-struct close_args_t {
- int32_t fd;
-};
-
-// A struct describing the event that we send to the user mode upon a new connection.
-struct socket_open_event_t {
- // The time of the event.
- uint64_t timestamp_ns;
- // A unique ID for the connection.
- struct conn_id_t conn_id;
- // The address of the client.
- struct sockaddr_in addr;
-};
-
-// Struct describing the close event being sent to the user mode.
-struct socket_close_event_t {
- // Timestamp of the close syscall
- uint64_t timestamp_ns;
- // The unique ID of the connection
- struct conn_id_t conn_id;
- // Total number of bytes written on that connection
- int64_t wr_bytes;
- // Total number of bytes read on that connection
- int64_t rd_bytes;
-};
-
-struct socket_data_event_t {
- // We split attributes into a separate struct, because BPF gets upset if you do lots of
- // size arithmetic. This makes it so that it's attributes followed by message.
- struct attr_t {
- // The timestamp when syscall completed (return probe was triggered).
- uint64_t timestamp_ns;
-
- // Connection identifier (PID, FD, etc.).
- struct conn_id_t conn_id;
-
- // The type of the actual data that the msg field encodes, which is used by the caller
- // to determine how to interpret the data.
- enum traffic_direction_t direction;
-
- // The size of the original message. We use this to truncate msg field to minimize the amount
- // of data being transferred.
- uint32_t msg_size;
-
- // A 0-based position number for this event on the connection, in terms of byte position.
- // The position is for the first byte of this message.
- uint64_t pos;
- } attr;
- char msg[MAX_MSG_SIZE];
-};
-
-// Maps
-
-// A map of the active connections. The name of the map is conn_info_map
-// the key is of type uint64_t, the value is of type struct conn_info_t,
-// and the map won't be bigger than 128KB.
-BPF_HASH(conn_info_map, uint64_t, struct conn_info_t, 131072);
-// An helper map that will help us cache the input arguments of the accept syscall
-// between the entry hook and the return hook.
-BPF_HASH(active_accept_args_map, uint64_t, struct accept_args_t);
-// Perf buffer to send to the user-mode the data events.
-BPF_PERF_OUTPUT(socket_data_events);
-// A perf buffer that allows us send events from kernel to user mode.
-// This perf buffer is dedicated for special type of events - open events.
-BPF_PERF_OUTPUT(socket_open_events);
-// Perf buffer to send to the user-mode the close events.
-BPF_PERF_OUTPUT(socket_close_events);
-BPF_PERCPU_ARRAY(socket_data_event_buffer_heap, struct socket_data_event_t, 1);
-BPF_HASH(active_write_args_map, uint64_t, struct data_args_t);
-// Helper map to store read syscall arguments between entry and exit hooks.
-BPF_HASH(active_read_args_map, uint64_t, struct data_args_t);
-// An helper map to store close syscall arguments between entry and exit syscalls.
-BPF_HASH(active_close_args_map, uint64_t, struct close_args_t);
-
-// Helper functions
-
-// Generates a unique identifier using a tgid (Thread Global ID) and a fd (File Descriptor).
-static __inline uint64_t gen_tgid_fd(uint32_t tgid, int fd) {
- return ((uint64_t)tgid << 32) | (uint32_t)fd;
-}
-
-// An helper function that checks if the syscall finished successfully and if it did
-// saves the new connection in a dedicated map of connections
-static __inline void process_syscall_accept(struct pt_regs* ctx, uint64_t id, const struct accept_args_t* args) {
- // Extracting the return code, and checking if it represent a failure,
- // if it does, we abort the as we have nothing to do.
- int ret_fd = PT_REGS_RC(ctx);
- if (ret_fd <= 0) {
- return;
- }
-
- struct conn_info_t conn_info = {};
- uint32_t pid = id >> 32;
- conn_info.conn_id.pid = pid;
- conn_info.conn_id.fd = ret_fd;
- conn_info.conn_id.tsid = bpf_ktime_get_ns();
-
- uint64_t pid_fd = ((uint64_t)pid << 32) | (uint32_t)ret_fd;
- // Saving the connection info in a global map, so in the other syscalls
- // (read, write and close) we will be able to know that we have seen
- // the connection
- conn_info_map.update(&pid_fd, &conn_info);
-
- // Sending an open event to the user mode, to let the user mode know that we
- // have identified a new connection.
- struct socket_open_event_t open_event = {};
- open_event.timestamp_ns = bpf_ktime_get_ns();
- open_event.conn_id = conn_info.conn_id;
- bpf_probe_read(&open_event.addr, sizeof(open_event.addr), args->addr);
-
- socket_open_events.perf_submit(ctx, &open_event, sizeof(struct socket_open_event_t));
-}
-
-static inline __attribute__((__always_inline__)) void process_syscall_close(struct pt_regs* ctx, uint64_t id,
- const struct close_args_t* close_args) {
- int ret_val = PT_REGS_RC(ctx);
- if (ret_val < 0) {
- return;
- }
-
- uint32_t tgid = id >> 32;
- uint64_t tgid_fd = gen_tgid_fd(tgid, close_args->fd);
- struct conn_info_t* conn_info = conn_info_map.lookup(&tgid_fd);
- if (conn_info == NULL) {
- // The FD being closed does not represent an IPv4 socket FD.
- return;
- }
-
- // Send to the user mode an event indicating the connection was closed.
- struct socket_close_event_t close_event = {};
- close_event.timestamp_ns = bpf_ktime_get_ns();
- close_event.conn_id = conn_info->conn_id;
- close_event.rd_bytes = conn_info->rd_bytes;
- close_event.wr_bytes = conn_info->wr_bytes;
-
- socket_close_events.perf_submit(ctx, &close_event, sizeof(struct socket_close_event_t));
-
- // Remove the connection from the mapping.
- conn_info_map.delete(&tgid_fd);
-}
-
-static inline __attribute__((__always_inline__)) bool is_http_connection(struct conn_info_t* conn_info, const char* buf, size_t count) {
- // If the connection was already identified as HTTP connection, no need to re-check it.
- if (conn_info->is_http) {
- return true;
- }
-
- // The minimum length of http request or response.
- if (count < 16) {
- return false;
- }
-
- bool res = false;
- if (buf[0] == 'H' && buf[1] == 'T' && buf[2] == 'T' && buf[3] == 'P') {
- res = true;
- }
- if (buf[0] == 'G' && buf[1] == 'E' && buf[2] == 'T') {
- res = true;
- }
- if (buf[0] == 'P' && buf[1] == 'O' && buf[2] == 'S' && buf[3] == 'T') {
- res = true;
- }
-
- if (res) {
- conn_info->is_http = true;
- }
-
- return res;
-}
-
-static __inline void perf_submit_buf(struct pt_regs* ctx, const enum traffic_direction_t direction,
- const char* buf, size_t buf_size, size_t offset,
- struct conn_info_t* conn_info,
- struct socket_data_event_t* event) {
- switch (direction) {
- case kEgress:
- event->attr.pos = conn_info->wr_bytes + offset;
- break;
- case kIngress:
- event->attr.pos = conn_info->rd_bytes + offset;
- break;
- }
-
- // Note that buf_size_minus_1 will be positive due to the if-statement above.
- size_t buf_size_minus_1 = buf_size - 1;
-
- // Clang is too smart for us, and tries to remove some of the obvious hints we are leaving for the
- // BPF verifier. So we add this NOP volatile statement, so clang can't optimize away some of our
- // if-statements below.
- // By telling clang that buf_size_minus_1 is both an input and output to some black box assembly
- // code, clang has to discard any assumptions on what values this variable can take.
- asm volatile("" : "+r"(buf_size_minus_1) :);
-
- buf_size = buf_size_minus_1 + 1;
-
- // 4.14 kernels reject bpf_probe_read with size that they may think is zero.
- // Without the if statement, it somehow can't reason that the bpf_probe_read is non-zero.
- size_t amount_copied = 0;
- if (buf_size_minus_1 < MAX_MSG_SIZE) {
- bpf_probe_read(&event->msg, buf_size, buf);
- amount_copied = buf_size;
- } else {
- bpf_probe_read(&event->msg, MAX_MSG_SIZE, buf);
- amount_copied = MAX_MSG_SIZE;
- }
-
- // If-statement is redundant, but is required to keep the 4.14 verifier happy.
- if (amount_copied > 0) {
- event->attr.msg_size = amount_copied;
- socket_data_events.perf_submit(ctx, event, sizeof(event->attr) + amount_copied);
- }
-}
-
-static __inline void perf_submit_wrapper(struct pt_regs* ctx,
- const enum traffic_direction_t direction, const char* buf,
- const size_t buf_size, struct conn_info_t* conn_info,
- struct socket_data_event_t* event) {
- int bytes_sent = 0;
- unsigned int i;
-#pragma unroll
- for (i = 0; i < CHUNK_LIMIT; ++i) {
- const int bytes_remaining = buf_size - bytes_sent;
- const size_t current_size = (bytes_remaining > MAX_MSG_SIZE && (i != CHUNK_LIMIT - 1)) ? MAX_MSG_SIZE : bytes_remaining;
- perf_submit_buf(ctx, direction, buf + bytes_sent, current_size, bytes_sent, conn_info, event);
- bytes_sent += current_size;
- if (buf_size == bytes_sent) {
- return;
- }
- }
-}
-
-static inline __attribute__((__always_inline__)) void process_data(struct pt_regs* ctx, uint64_t id,
- enum traffic_direction_t direction,
- const struct data_args_t* args, ssize_t bytes_count) {
- // Always check access to pointer before accessing them.
- if (args->buf == NULL) {
- return;
- }
-
- // For read and write syscall, the return code is the number of bytes written or read, so zero means nothing
- // was written or read, and negative means that the syscall failed. Anyhow, we have nothing to do with that syscall.
- if (bytes_count <= 0) {
- return;
- }
-
- uint32_t pid = id >> 32;
- uint64_t pid_fd = ((uint64_t)pid << 32) | (uint32_t)args->fd;
- struct conn_info_t* conn_info = conn_info_map.lookup(&pid_fd);
- if (conn_info == NULL) {
- // The FD being read/written does not represent an IPv4 socket FD.
- return;
- }
-
- // Check if the connection is already HTTP, or check if that's a new connection, check protocol and return true if that's HTTP.
- if (is_http_connection(conn_info, args->buf, bytes_count)) {
- // allocate new event.
- uint32_t kZero = 0;
- struct socket_data_event_t* event = socket_data_event_buffer_heap.lookup(&kZero);
- if (event == NULL) {
- return;
- }
-
- // Fill the metadata of the data event.
- event->attr.timestamp_ns = bpf_ktime_get_ns();
- event->attr.direction = direction;
- event->attr.conn_id = conn_info->conn_id;
-
- perf_submit_wrapper(ctx, direction, args->buf, bytes_count, conn_info, event);
- }
-
- // Update the conn_info total written/read bytes.
- switch (direction) {
- case kEgress:
- conn_info->wr_bytes += bytes_count;
- break;
- case kIngress:
- conn_info->rd_bytes += bytes_count;
- break;
- }
-}
-
-// Hooks
-int syscall__probe_entry_accept(struct pt_regs* ctx, int sockfd, struct sockaddr* addr, socklen_t* addrlen) {
- uint64_t id = bpf_get_current_pid_tgid();
-
- // Keep the addr in a map to use during the exit method.
- struct accept_args_t accept_args = {};
- accept_args.addr = (struct sockaddr_in *)addr;
- active_accept_args_map.update(&id, &accept_args);
-
- return 0;
-}
-
-int syscall__probe_ret_accept(struct pt_regs* ctx) {
- uint64_t id = bpf_get_current_pid_tgid();
-
- // Pulling the addr from the map.
- struct accept_args_t* accept_args = active_accept_args_map.lookup(&id);
- if (accept_args != NULL) {
- process_syscall_accept(ctx, id, accept_args);
- }
-
- active_accept_args_map.delete(&id);
- return 0;
-}
-
-
-// Hooking the entry of accept4
-// the signature of the syscall is int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
-int syscall__probe_entry_accept4(struct pt_regs* ctx, int sockfd, struct sockaddr* addr, socklen_t* addrlen) {
- // Getting a unique ID for the relevant thread in the relevant pid.
- // That way we can link different calls from the same thread.
- uint64_t id = bpf_get_current_pid_tgid();
-
- // Keep the addr in a map to use during the accpet4 exit hook.
- struct accept_args_t accept_args = {};
- accept_args.addr = (struct sockaddr_in *)addr;
- active_accept_args_map.update(&id, &accept_args);
-
- return 0;
-}
-
-// Hooking the exit of accept4
-int syscall__probe_ret_accept4(struct pt_regs* ctx) {
- uint64_t id = bpf_get_current_pid_tgid();
-
- // Pulling the addr from the map.
- struct accept_args_t* accept_args = active_accept_args_map.lookup(&id);
- // If the id exist in the map, we will get a non empty pointer that holds
- // the input address argument from the entry of the syscall.
- if (accept_args != NULL) {
- process_syscall_accept(ctx, id, accept_args);
- }
-
- // Anyway, in the end clean the map.
- active_accept_args_map.delete(&id);
- return 0;
-}
-
-// original signature: ssize_t write(int fd, const void *buf, size_t count);
-int syscall__probe_entry_write(struct pt_regs* ctx, int fd, char* buf, size_t count) {
- uint64_t id = bpf_get_current_pid_tgid();
-
- struct data_args_t write_args = {};
- write_args.fd = fd;
- write_args.buf = buf;
- active_write_args_map.update(&id, &write_args);
-
- return 0;
-}
-
-int syscall__probe_ret_write(struct pt_regs* ctx) {
- uint64_t id = bpf_get_current_pid_tgid();
- ssize_t bytes_count = PT_REGS_RC(ctx); // Also stands for return code.
-
- // Unstash arguments, and process syscall.
- struct data_args_t* write_args = active_write_args_map.lookup(&id);
- if (write_args != NULL) {
- process_data(ctx, id, kEgress, write_args, bytes_count);
- }
-
- active_write_args_map.delete(&id);
- return 0;
-}
-
-// original signature: ssize_t read(int fd, void *buf, size_t count);
-int syscall__probe_entry_read(struct pt_regs* ctx, int fd, char* buf, size_t count) {
- uint64_t id = bpf_get_current_pid_tgid();
-
- // Stash arguments.
- struct data_args_t read_args = {};
- read_args.fd = fd;
- read_args.buf = buf;
- active_read_args_map.update(&id, &read_args);
-
- return 0;
-}
-
-int syscall__probe_ret_read(struct pt_regs* ctx) {
- uint64_t id = bpf_get_current_pid_tgid();
-
- // The return code the syscall is the number of bytes read as well.
- ssize_t bytes_count = PT_REGS_RC(ctx);
- struct data_args_t* read_args = active_read_args_map.lookup(&id);
- if (read_args != NULL) {
- // kIngress is an enum value that let's the process_data function
- // to know whether the input buffer is incoming or outgoing.
- process_data(ctx, id, kIngress, read_args, bytes_count);
- }
-
- active_read_args_map.delete(&id);
- return 0;
-}
-
-// original signature: int close(int fd)
-int syscall__probe_entry_close(struct pt_regs* ctx, int fd) {
- uint64_t id = bpf_get_current_pid_tgid();
- struct close_args_t close_args;
- close_args.fd = fd;
- active_close_args_map.update(&id, &close_args);
-
- return 0;
-}
-
-int syscall__probe_ret_close(struct pt_regs* ctx) {
- uint64_t id = bpf_get_current_pid_tgid();
- const struct close_args_t* close_args = active_close_args_map.lookup(&id);
- if (close_args != NULL) {
- process_syscall_close(ctx, id, close_args);
- }
-
- active_close_args_map.delete(&id);
- return 0;
-}
diff --git a/24-hide/index.html b/24-hide/index.html
index 91672bc..60d1132 100644
--- a/24-hide/index.html
+++ b/24-hide/index.html
@@ -83,7 +83,7 @@
diff --git a/25-signal/index.html b/25-signal/index.html
index ebe329b..04002f7 100644
--- a/25-signal/index.html
+++ b/25-signal/index.html
@@ -83,7 +83,7 @@
@@ -300,6 +300,7 @@ int bpf_dos(struct trace_event_raw_sys_enter *ctx)
使用方式:
$ sudo ./ecli package.json
+TIME PID COMM SUCCESS
这个程序会对任何试图使用 ptrace 系统调用的程序,例如 strace,发出 SIG_KILL 信号。
一旦 eBPF 程序开始运行,你可以通过运行以下命令进行测试:
diff --git a/26-sudo/index.html b/26-sudo/index.html
index 76e54fa..54ed134 100644
--- a/26-sudo/index.html
+++ b/26-sudo/index.html
@@ -83,7 +83,7 @@
diff --git a/27-replace/index.html b/27-replace/index.html
index 2449bf4..41021ea 100644
--- a/27-replace/index.html
+++ b/27-replace/index.html
@@ -83,7 +83,7 @@
diff --git a/27-replace/replace.bpf.c b/27-replace/replace.bpf.c
index 019b6cf..361bff3 100644
--- a/27-replace/replace.bpf.c
+++ b/27-replace/replace.bpf.c
@@ -3,7 +3,7 @@
#include
#include
#include
-#include "common.h"
+#include "replace.h"
char LICENSE[] SEC("license") = "Dual BSD/GPL";
@@ -268,7 +268,8 @@ int check_possible_addresses(struct trace_event_raw_sys_exit *ctx) {
// break;
// }
// }
- // we can use bpf_strncmp here, but it's not available in the kernel version older
+ // we can use bpf_strncmp here,
+ // but it's not available in the kernel version older than 5.17
if (bpf_strncmp(name, text_len_max, (const char *)text_find) == 0) {
// ***********
// We've found out text!
diff --git a/27-replace/replace.c b/27-replace/replace.c
index 4e139e1..943f9aa 100644
--- a/27-replace/replace.c
+++ b/27-replace/replace.c
@@ -2,7 +2,7 @@
#include
#include
#include "replace.skel.h"
-#include "common.h"
+#include "replace.h"
#include
diff --git a/27-replace/common.h b/27-replace/replace.h
similarity index 79%
rename from 27-replace/common.h
rename to 27-replace/replace.h
index 1fda5c3..5cd3d63 100644
--- a/27-replace/common.h
+++ b/27-replace/replace.h
@@ -26,14 +26,4 @@ struct event {
bool success;
};
-struct tr_file {
- char filename[FILENAME_LEN_MAX];
- unsigned int filename_len;
-};
-
-struct tr_text {
- char text[TEXT_LEN_MAX];
- unsigned int text_len;
-};
-
#endif // BAD_BPF_COMMON_H
diff --git a/28-detach/index.html b/28-detach/index.html
index 5841acd..df93984 100644
--- a/28-detach/index.html
+++ b/28-detach/index.html
@@ -83,7 +83,7 @@
diff --git a/29-sockops/index.html b/29-sockops/index.html
index fbc50e3..ae2a7dc 100644
--- a/29-sockops/index.html
+++ b/29-sockops/index.html
@@ -83,7 +83,7 @@
diff --git a/3-fentry-unlink/fentry-link.bpf.c b/3-fentry-unlink/fentry-link.bpf.c
index baf5575..9b93479 100644
--- a/3-fentry-unlink/fentry-link.bpf.c
+++ b/3-fentry-unlink/fentry-link.bpf.c
@@ -1,5 +1,6 @@
// SPDX-License-Identifier: GPL-2.0 OR BSD-3-Clause
/* Copyright (c) 2021 Sartura */
+#define BPF_NO_GLOBAL_DATA
#include "vmlinux.h"
#include
#include
diff --git a/3-fentry-unlink/index.html b/3-fentry-unlink/index.html
index f36c122..f7cee03 100644
--- a/3-fentry-unlink/index.html
+++ b/3-fentry-unlink/index.html
@@ -83,7 +83,7 @@
diff --git a/30-sslsniff/index.html b/30-sslsniff/index.html
index 6ec0821..1983e8a 100644
--- a/30-sslsniff/index.html
+++ b/30-sslsniff/index.html
@@ -83,7 +83,7 @@
diff --git a/4-opensnoop/index.html b/4-opensnoop/index.html
index b003c2c..7b80b5d 100644
--- a/4-opensnoop/index.html
+++ b/4-opensnoop/index.html
@@ -83,7 +83,7 @@
diff --git a/4-opensnoop/opensnoop.bpf.c b/4-opensnoop/opensnoop.bpf.c
index 1830be6..50c082e 100644
--- a/4-opensnoop/opensnoop.bpf.c
+++ b/4-opensnoop/opensnoop.bpf.c
@@ -1,3 +1,4 @@
+#define BPF_NO_GLOBAL_DATA
#include
#include
diff --git a/404.html b/404.html
index 0f44336..f788cfa 100644
--- a/404.html
+++ b/404.html
@@ -84,7 +84,7 @@
diff --git a/5-uprobe-bashreadline/bashreadline.bpf.c b/5-uprobe-bashreadline/bashreadline.bpf.c
index 8058734..6062db0 100644
--- a/5-uprobe-bashreadline/bashreadline.bpf.c
+++ b/5-uprobe-bashreadline/bashreadline.bpf.c
@@ -1,3 +1,4 @@
+#define BPF_NO_GLOBAL_DATA
#include
#include
#include
diff --git a/5-uprobe-bashreadline/index.html b/5-uprobe-bashreadline/index.html
index 8d6e09e..7098cb0 100644
--- a/5-uprobe-bashreadline/index.html
+++ b/5-uprobe-bashreadline/index.html
@@ -83,7 +83,7 @@
diff --git a/6-sigsnoop/index.html b/6-sigsnoop/index.html
index f2a2b5f..0beb666 100644
--- a/6-sigsnoop/index.html
+++ b/6-sigsnoop/index.html
@@ -83,7 +83,7 @@
diff --git a/6-sigsnoop/sigsnoop.bpf.c b/6-sigsnoop/sigsnoop.bpf.c
index 79d57bd..2027522 100755
--- a/6-sigsnoop/sigsnoop.bpf.c
+++ b/6-sigsnoop/sigsnoop.bpf.c
@@ -1,3 +1,4 @@
+#define BPF_NO_GLOBAL_DATA
#include
#include
#include
diff --git a/7-execsnoop/index.html b/7-execsnoop/index.html
index 279be7a..60ea869 100644
--- a/7-execsnoop/index.html
+++ b/7-execsnoop/index.html
@@ -83,7 +83,7 @@
diff --git a/8-exitsnoop/index.html b/8-exitsnoop/index.html
index 527034b..cf4e17b 100644
--- a/8-exitsnoop/index.html
+++ b/8-exitsnoop/index.html
@@ -83,7 +83,7 @@
diff --git a/9-runqlat/index.html b/9-runqlat/index.html
index 7a3264d..51f1ab0 100644
--- a/9-runqlat/index.html
+++ b/9-runqlat/index.html
@@ -83,7 +83,7 @@
diff --git a/bcc-documents/kernel-versions.html b/bcc-documents/kernel-versions.html
index 2971c08..90683ed 100644
--- a/bcc-documents/kernel-versions.html
+++ b/bcc-documents/kernel-versions.html
@@ -83,7 +83,7 @@
diff --git a/bcc-documents/kernel_config.html b/bcc-documents/kernel_config.html
index 6fe5f10..4722016 100644
--- a/bcc-documents/kernel_config.html
+++ b/bcc-documents/kernel_config.html
@@ -83,7 +83,7 @@
diff --git a/bcc-documents/reference_guide.html b/bcc-documents/reference_guide.html
index 06bff74..6befac8 100644
--- a/bcc-documents/reference_guide.html
+++ b/bcc-documents/reference_guide.html
@@ -83,7 +83,7 @@
diff --git a/bcc-documents/special_filtering.html b/bcc-documents/special_filtering.html
index c6c0973..cfc3199 100644
--- a/bcc-documents/special_filtering.html
+++ b/bcc-documents/special_filtering.html
@@ -83,7 +83,7 @@
diff --git a/bcc-documents/tutorial.html b/bcc-documents/tutorial.html
index 4de9e44..82138e8 100644
--- a/bcc-documents/tutorial.html
+++ b/bcc-documents/tutorial.html
@@ -83,7 +83,7 @@
diff --git a/bcc-documents/tutorial_bcc_python_developer.html b/bcc-documents/tutorial_bcc_python_developer.html
index 8dc102b..4bd1cf3 100644
--- a/bcc-documents/tutorial_bcc_python_developer.html
+++ b/bcc-documents/tutorial_bcc_python_developer.html
@@ -83,7 +83,7 @@
diff --git a/https:/github.com/eunomia-bpf/bpf-developer-tutorial.html b/https:/github.com/eunomia-bpf/bpf-developer-tutorial.html
index 548a132..7b9570e 100644
--- a/https:/github.com/eunomia-bpf/bpf-developer-tutorial.html
+++ b/https:/github.com/eunomia-bpf/bpf-developer-tutorial.html
@@ -83,7 +83,7 @@
diff --git a/index.html b/index.html
index 130644a..72035e6 100644
--- a/index.html
+++ b/index.html
@@ -83,7 +83,7 @@
diff --git a/print.html b/print.html
index 056487e..414187c 100644
--- a/print.html
+++ b/print.html
@@ -84,7 +84,7 @@
@@ -4451,8 +4451,581 @@ Error: BpfError("load and attach ebpf program failed")
:https://mp.weixin.qq.com/s/mul4n5D3nXThjxuHV7GpMA
:https://blog.seeflower.dev/archives/138/
-
-TODO
+
+在当今的技术环境中,随着微服务、云原生应用和复杂的分布式系统的崛起,系统的可观测性已成为确保其健康、性能和安全的关键要素。特别是在微服务架构中,应用程序的组件可能分布在多个容器和服务器上,这使得传统的监控方法往往难以提供足够的深度和广度来全面了解系统的行为。这就是为什么观测七层协议,如 HTTP、gRPC、MQTT 等,变得尤为重要。
+七层协议为我们提供了关于应用程序如何与其他服务和组件交互的详细信息。在微服务环境中,了解这些交互是至关重要的,因为它们经常是性能瓶颈、故障和安全问题的根源。然而,监控这些协议并不简单。传统的网络监控工具,如 tcpdump,虽然在捕获网络流量方面非常有效,但在处理七层协议的复杂性和动态性时,它们往往显得力不从心。
+这正是 eBPF 技术发挥作用的地方。eBPF 允许开发者和运维人员深入到系统的内核层,实时观测和分析系统的行为,而无需对应用程序代码进行任何修改或插入埋点。这为我们提供了一个独特的机会,可以更简单、更高效地处理应用层流量,特别是在微服务环境中。
+在本教程中,我们将深入探讨以下内容:
+
+- 追踪七层协议,如 HTTP,以及与其相关的挑战。
+- eBPF 的 socket filter 和 syscall 追踪:这两种技术如何帮助我们在不同的内核层次追踪 HTTP 网络请求数据,以及这两种方法的优势和局限性。
+- eBPF 实践教程:如何开发一个 eBPF 程序,使用 eBPF socket filter 或 syscall 追踪来捕获和分析 HTTP 流量
+
+随着网络流量的增加和应用程序的复杂性增加,对七层协议的深入了解变得越来越重要。通过本教程,您将获得必要的知识和工具,以便更有效地监控和分析您的网络流量,从而为您的应用程序和服务器提供最佳的性能。
+本文是 eBPF 开发者教程的一部分,更详细的内容可以在这里找到:https://eunomia.dev/tutorials/ 源代码在 GitHub 仓库 中开源。
+
+在现代的网络环境中,七层协议不仅仅局限于 HTTP。实际上,有许多七层协议,如 HTTP/2, gRPC, MQTT, WebSocket, AMQP 和 SMTP,它们都在不同的应用场景中发挥着关键作用。这些协议为我们提供了关于应用程序如何与其他服务和组件交互的详细信息。但是,追踪这些协议并不是一个简单的任务,尤其是在复杂的分布式系统中。
+
+-
+
多样性和复杂性:每种七层协议都有其特定的设计和工作原理。例如,gRPC 使用了 HTTP/2 作为其传输协议,并支持多种语言。而 MQTT 是为低带宽和不可靠的网络设计的轻量级发布/订阅消息传输协议。
+
+-
+
动态性:许多七层协议都是动态的,这意味着它们的行为可能会根据网络条件、应用需求或其他因素而变化。
+
+-
+
加密和安全性:随着安全意识的增强,许多七层协议都采用了加密技术,如 TLS/SSL。这为追踪和分析带来了额外的挑战,因为需要解密流量才能进行深入的分析。
+
+-
+
高性能需求:在高流量的生产环境中,捕获和分析七层协议的流量可能会对系统性能产生影响。传统的网络监控工具可能无法处理大量的并发会话。
+
+-
+
数据的完整性和连续性:与 tcpdump 这样的工具只捕获单独的数据包不同,追踪七层协议需要捕获完整的会话,这可能涉及多个数据包。这要求工具能够正确地重组和解析这些数据包,以提供连续的会话视图。
+
+-
+
代码侵入性:为了深入了解七层协议的行为,开发人员可能需要修改应用程序代码以添加监控功能。这不仅增加了开发和维护的复杂性,而且可能会影响应用程序的性能。
+
+
+正如上文所述,eBPF 提供了一个强大的解决方案,允许我们在内核层面捕获和分析七层协议的流量,而无需对应用程序进行任何修改。这种方法为我们提供了一个独特的机会,可以更简单、更高效地处理应用层流量,特别是在微服务和分布式环境中。
+在处理网络流量和系统行为时,选择在内核态而非用户态进行处理有其独特的优势。首先,内核态处理可以直接访问系统资源和硬件,从而提供更高的性能和效率。其次,由于内核是操作系统的核心部分,它可以提供对系统行为的全面视图,而不受任何用户空间应用程序的限制。
+**无插桩追踪("zero-instrumentation observability")**的优势如下:
+
+- 性能开销小:由于不需要修改或添加额外的代码到应用程序中,所以对性能的影响最小化。
+- 透明性:开发者和运维人员不需要知道应用程序的内部工作原理,也不需要访问源代码。
+- 灵活性:可以轻松地在不同的环境和应用程序中部署和使用,无需进行任何特定的配置或修改。
+- 安全性:由于不需要修改应用程序代码,所以降低了引入潜在安全漏洞的风险。
+
+利用 eBPF 在内核态进行无插桩追踪,我们可以实时捕获和分析系统的行为,而不需要对应用程序进行任何修改。这种方法不仅提供了对系统深入的洞察力,而且确保了最佳的性能和效率。这是为什么 eBPF 成为现代可观测性工具的首选技术,特别是在需要高性能和低延迟的生产环境中。
+
+
+是什么?
+eBPF socket filter 是经典的 Berkeley Packet Filter (BPF) 的扩展,允许在内核中直接进行更高级的数据包过滤。它在套接字层操作,使得可以精细地控制哪些数据包被用户空间应用程序处理。
+主要特点:
+
+- 性能:通过在内核中直接处理数据包,eBPF socket filters 减少了用户和内核空间之间的上下文切换的开销。
+- 灵活性:eBPF socket filters 可以附加到任何套接字,为各种协议和套接字类型提供了通用的数据包过滤机制。
+- 可编程性:开发者可以编写自定义的 eBPF 程序来定义复杂的过滤逻辑,超越简单的数据包匹配。
+
+用途:
+
+- 流量控制:根据自定义条件限制或优先处理流量。
+- 安全性:在它们到达用户空间应用程序之前丢弃恶意数据包。
+- 监控:捕获特定数据包进行分析,而不影响其它流量。
+
+
+是什么?
+使用 eBPF 进行的系统调用跟踪允许监视和操作应用程序发出的系统调用。系统调用是用户空间应用程序与内核交互的主要机制,因此跟踪它们可以深入了解应用程序的行为。
+主要特点:
+
+- 粒度:eBPF 允许跟踪特定的系统调用,甚至是这些系统调用中的特定参数。
+- 低开销:与其他跟踪方法相比,eBPF 系统调用跟踪旨在具有最小的性能影响。
+- 安全性:内核验证 eBPF 程序,以确保它们不会损害系统稳定性。
+
+工作原理:
+eBPF 系统调用跟踪通常涉及将 eBPF 程序附加到与系统调用相关的 tracepoints 或 kprobes。当跟踪的系统调用被调用时,执行 eBPF 程序,允许收集数据或甚至修改系统调用参数。
+
+| 项目 | eBPF Socket Filter | eBPF Syscall Tracing |
+| 操作层 | 套接字层,主要处理从套接字接收或发送的网络数据包 | 系统调用层,监视和可能更改应用程序发出的系统调用的行为 |
+| 主要用途 | 主要用于网络数据包的过滤、监控和操作 | 用于性能分析、安全监控和系统调用交互的调试 |
+| 粒度 | 专注于单个网络数据包 | 可以监视与网络无关的广泛的系统活动 |
+| 追踪 HTTP 流量 | 可以用于过滤和捕获通过套接字传递的 HTTP 数据包 | 可以跟踪与网络操作相关的系统调用 |
+
+
+总之,eBPF 的 socket filter 和 syscall 追踪都可以用于追踪 HTTP 流量,但 socket filters 更直接且更适合此目的。然而,如果您对应用程序如何与系统交互的更广泛的上下文感兴趣(例如,哪些系统调用导致了 HTTP 流量),那么系统调用跟踪将是非常有价值的。在许多高级的可观察性设置中,这两种工具可能会同时使用,以提供系统和网络行为的全面视图。
+
+eBPF 代码由用户态和内核态组成,这里主要关注于内核态代码。这是使用 eBPF socket filter 技术来在内核中捕获HTTP流量的主要逻辑,完整代码如下:
+SEC("socket")
+int socket_handler(struct __sk_buff *skb)
+{
+ struct so_event *e;
+ __u8 verlen;
+ __u16 proto;
+ __u32 nhoff = ETH_HLEN;
+ __u32 ip_proto = 0;
+ __u32 tcp_hdr_len = 0;
+ __u16 tlen;
+ __u32 payload_offset = 0;
+ __u32 payload_length = 0;
+ __u8 hdr_len;
+
+ bpf_skb_load_bytes(skb, 12, &proto, 2);
+ proto = __bpf_ntohs(proto);
+ if (proto != ETH_P_IP)
+ return 0;
+
+ if (ip_is_fragment(skb, nhoff))
+ return 0;
+
+ // ip4 header lengths are variable
+ // access ihl as a u8 (linux/include/linux/skbuff.h)
+ bpf_skb_load_bytes(skb, ETH_HLEN, &hdr_len, sizeof(hdr_len));
+ hdr_len &= 0x0f;
+ hdr_len *= 4;
+
+ /* verify hlen meets minimum size requirements */
+ if (hdr_len < sizeof(struct iphdr))
+ {
+ return 0;
+ }
+
+ bpf_skb_load_bytes(skb, nhoff + offsetof(struct iphdr, protocol), &ip_proto, 1);
+
+ if (ip_proto != IPPROTO_TCP)
+ {
+ return 0;
+ }
+
+ tcp_hdr_len = nhoff + hdr_len;
+ bpf_skb_load_bytes(skb, nhoff + 0, &verlen, 1);
+ bpf_skb_load_bytes(skb, nhoff + offsetof(struct iphdr, tot_len), &tlen, sizeof(tlen));
+
+ __u8 doff;
+ bpf_skb_load_bytes(skb, tcp_hdr_len + offsetof(struct __tcphdr, ack_seq) + 4, &doff, sizeof(doff)); // read the first byte past __tcphdr->ack_seq, we can't do offsetof bit fields
+ doff &= 0xf0; // clean-up res1
+ doff >>= 4; // move the upper 4 bits to low
+ doff *= 4; // convert to bytes length
+
+ payload_offset = ETH_HLEN + hdr_len + doff;
+ payload_length = __bpf_ntohs(tlen) - hdr_len - doff;
+
+ char line_buffer[7];
+ if (payload_length < 7 || payload_offset < 0)
+ {
+ return 0;
+ }
+ bpf_skb_load_bytes(skb, payload_offset, line_buffer, 7);
+ bpf_printk("%d len %d buffer: %s", payload_offset, payload_length, line_buffer);
+ if (bpf_strncmp(line_buffer, 3, "GET") != 0 &&
+ bpf_strncmp(line_buffer, 4, "POST") != 0 &&
+ bpf_strncmp(line_buffer, 3, "PUT") != 0 &&
+ bpf_strncmp(line_buffer, 6, "DELETE") != 0 &&
+ bpf_strncmp(line_buffer, 4, "HTTP") != 0)
+ {
+ return 0;
+ }
+
+ /* reserve sample from BPF ringbuf */
+ e = bpf_ringbuf_reserve(&rb, sizeof(*e), 0);
+ if (!e)
+ return 0;
+
+ e->ip_proto = ip_proto;
+ bpf_skb_load_bytes(skb, nhoff + hdr_len, &(e->ports), 4);
+ e->pkt_type = skb->pkt_type;
+ e->ifindex = skb->ifindex;
+
+ e->payload_length = payload_length;
+ bpf_skb_load_bytes(skb, payload_offset, e->payload, MAX_BUF_SIZE);
+
+ bpf_skb_load_bytes(skb, nhoff + offsetof(struct iphdr, saddr), &(e->src_addr), 4);
+ bpf_skb_load_bytes(skb, nhoff + offsetof(struct iphdr, daddr), &(e->dst_addr), 4);
+ bpf_ringbuf_submit(e, 0);
+
+ return skb->len;
+}
+
+当分析这段eBPF程序时,我们将按照每个代码块的内容来详细解释,并提供相关的背景知识:
+SEC("socket")
+int socket_handler(struct __sk_buff *skb)
+{
+ // ...
+}
+
+这是eBPF程序的入口点,它定义了一个名为 socket_handler 的函数,它会被内核用于处理传入的网络数据包。这个函数位于一个名为 socket 的 eBPF 节(section)中,表明这个程序用于套接字处理。
+struct so_event *e;
+__u8 verlen;
+__u16 proto;
+__u32 nhoff = ETH_HLEN;
+__u32 ip_proto = 0;
+__u32 tcp_hdr_len = 0;
+__u16 tlen;
+__u32 payload_offset = 0;
+__u32 payload_length = 0;
+__u8 hdr_len;
+
+在这个代码块中,我们定义了一些变量来存储在处理数据包时需要的信息。这些变量包括了struct so_event *e用于存储事件信息,verlen、proto、nhoff、ip_proto、tcp_hdr_len、tlen、payload_offset、payload_length、hdr_len等用于存储数据包信息的变量。
+
+struct so_event *e;:这是一个指向so_event结构体的指针,用于存储捕获到的事件信息。该结构体的具体定义在程序的其他部分。
+__u8 verlen;、__u16 proto;、__u32 nhoff = ETH_HLEN;:这些变量用于存储各种信息,例如协议类型、数据包偏移量等。nhoff初始化为以太网帧头部的长度,通常为14字节,因为以太网帧头部包括目标MAC地址、源MAC地址和帧类型字段。
+__u32 ip_proto = 0;:这个变量用于存储IP协议的类型,初始化为0。
+__u32 tcp_hdr_len = 0;:这个变量用于存储TCP头部的长度,初始化为0。
+__u16 tlen;:这个变量用于存储IP数据包的总长度。
+__u32 payload_offset = 0;、__u32 payload_length = 0;:这两个变量用于存储HTTP请求的载荷(payload)的偏移量和长度。
+__u8 hdr_len;:这个变量用于存储IP头部的长度。
+
+bpf_skb_load_bytes(skb, 12, &proto, 2);
+proto = __bpf_ntohs(proto);
+if (proto != ETH_P_IP)
+ return 0;
+
+在这里,代码从数据包中加载了以太网帧的类型字段,这个字段告诉我们数据包使用的网络层协议。然后,使用__bpf_ntohs函数将网络字节序的类型字段转换为主机字节序。接下来,代码检查类型字段是否等于IPv4的以太网帧类型(0x0800)。如果不等于,说明这个数据包不是IPv4数据包,直接返回0,放弃处理。
+这里需要了解以下几个概念:
+
+- 以太网帧(Ethernet Frame):是数据链路层(第二层)的协议,用于在局域网中传输数据帧。以太网帧通常包括目标MAC地址、源MAC地址和帧类型字段。
+- 网络字节序(Network Byte Order):网络协议通常使用大端字节序(Big-Endian)来表示数据。因此,需要将从网络中接收到的数据转换为主机字节序,以便在主机上正确解释数据。
+- IPv4帧类型(ETH_P_IP):表示以太网帧中包含的协议类型字段,0x0800表示IPv4。
+
+if (ip_is_fragment(skb, nhoff))
+ return 0;
+
+这一部分的代码检查是否处理IP分片。IP分片是将较大的IP数据包分割成多个小片段以进行传输的机制。在这里,如果数据包是IP分片,则直接返回0,表示不处理分片,只处理完整的数据包。
+static inline int ip_is_fragment(struct __sk_buff *skb, __u32 nhoff)
+{
+ __u16 frag_off;
+
+ bpf_skb_load_bytes(skb, nhoff + offsetof(struct iphdr, frag_off), &frag_off, 2);
+ frag_off = __bpf_ntohs(frag_off);
+ return frag_off & (IP_MF | IP_OFFSET);
+}
+
+上述代码是一个辅助函数,用于检查传入的IPv4数据包是否为IP分片。IP分片是一种机制,当IP数据包的大小超过了网络的最大传输单元(MTU),路由器会将其分割成多个较小的片段,以便在网络上进行传输。这个函数的目的是检查数据包的分片标志(Fragmentation Flag)以及片偏移(Fragment Offset)字段,以确定是否为分片。
+下面是代码的逐行解释:
+
+__u16 frag_off;:定义一个16位无符号整数变量frag_off,用于存储片偏移字段的值。
+bpf_skb_load_bytes(skb, nhoff + offsetof(struct iphdr, frag_off), &frag_off, 2);:这行代码使用bpf_skb_load_bytes函数从数据包中加载IPv4头部的片偏移字段(frag_off),并加载2个字节。nhoff是IPv4头部在数据包中的偏移量,offsetof(struct iphdr, frag_off)用于计算片偏移字段在IPv4头部中的偏移量。
+frag_off = __bpf_ntohs(frag_off);:将加载的片偏移字段从网络字节序(Big-Endian)转换为主机字节序。网络协议通常使用大端字节序表示数据,而主机可能使用大端或小端字节序。这里将片偏移字段转换为主机字节序,以便进一步处理。
+return frag_off & (IP_MF | IP_OFFSET);:这行代码通过使用位运算检查片偏移字段的值,以确定是否为IP分片。具体来说,它使用位与运算符&将片偏移字段与两个标志位进行位与运算:
+
+IP_MF:表示"更多分片"标志(More Fragments)。如果这个标志位被设置为1,表示数据包是分片的一部分,还有更多分片。
+IP_OFFSET:表示片偏移字段。如果片偏移字段不为0,表示数据包是分片的一部分,且具有片偏移值。
+如果这两个标志位中的任何一个被设置为1,那么结果就不为零,说明数据包是IP分片。如果都为零,说明数据包不是分片。
+
+
+
+需要注意的是,IP头部的片偏移字段以8字节为单位,所以实际的片偏移值需要左移3位来得到字节偏移。此外,IP头部的"更多分片"标志(IP_MF)表示数据包是否有更多的分片,通常与片偏移字段一起使用来指示整个数据包的分片情况。这个函数只关心这两个标志位,如果其中一个标志被设置,就认为是IP分片。
+bpf_skb_load_bytes(skb, ETH_HLEN, &hdr_len, sizeof(hdr_len));
+hdr_len &= 0x0f;
+hdr_len *= 4;
+
+这一部分的代码从数据包中加载IP头部的长度字段。IP头部长度字段包含了IP头部的长度信息,以4字节为单位,需要将其转换为字节数。这里通过按位与和乘以4来进行转换。
+需要了解:
+
+- IP头部(IP Header):IP头部包含了关于数据包的基本信息,如源IP地址、目标IP地址、协议类型和头部校验和等。头部长度字段(IHL,Header Length)表示IP头部的长度,以4字节为单位,通常为20字节(5个4字节的字)。
+
+if (hdr_len < sizeof(struct iphdr))
+{
+ return 0;
+}
+
+这段代码检查IP头部的长度是否满足最小长度要求,通常IP头部的最小长度是20字节。如果IP头部的长度小于20字节,说明数据包不完整或损坏,直接返回0,放弃处理。
+需要了解:
+
+struct iphdr:这是Linux内核中定义的结构体,表示IPv4头部的格式。它包括了版本、头部长度、服务类型、总长度、
+
+标识符、标志位、片偏移、生存时间、协议、头部校验和、源IP地址和目标IP地址等字段。
+bpf_skb_load_bytes(skb, nhoff + offsetof(struct iphdr, protocol), &ip_proto, 1);
+if (ip_proto != IPPROTO_TCP)
+{
+ return 0;
+}
+
+在这里,代码从数据包中加载IP头部中的协议字段,以确定数据包使用的传输层协议。然后,它检查协议字段是否为TCP协议(IPPROTO_TCP)。如果不是TCP协议,说明不是HTTP请求或响应,直接返回0。
+需要了解:
+
+- 传输层协议:IP头部中的协议字段指示了数据包所使用的传输层协议,例如TCP、UDP或ICMP。
+
+tcp_hdr_len = nhoff + hdr_len;
+
+这行代码计算了TCP头部的偏移量。它将以太网帧头部的长度(nhoff)与IP头部的长度(hdr_len)相加,得到TCP头部的起始位置。
+bpf_skb_load_bytes(skb, nhoff + 0, &verlen, 1);
+
+这行代码从数据包中加载TCP头部的第一个字节,该字节包含了TCP头部长度信息。这个长度字段以4字节为单位,需要进行后续的转换。
+bpf_skb_load_bytes(skb, nhoff + offsetof(struct iphdr, tot_len), &tlen, sizeof(tlen));
+
+这行代码从数据包中加载IP头部的总长度字段。IP头部总长度字段表示整个IP数据包的长度,包括IP头部和数据部分。
+__u8 doff;
+bpf_skb_load_bytes(skb, tcp_hdr_len + offsetof(struct __tcphdr, ack_seq) + 4, &doff, sizeof(doff));
+doff &= 0xf0;
+doff >>= 4;
+doff *= 4;
+
+这段代码用于计算TCP头部的长度。它加载TCP头部中的数据偏移字段(Data Offset,也称为头部长度字段),该字段表示TCP头部的长度以4字节为单位。代码将偏移字段的高四位清零,然后将其右移4位,最后乘以4,得到TCP头部的实际长度。
+需要了解:
+
+- TCP头部(TCP Header):TCP头部包含了TCP协议相关的信息,如源端口、目标端口、序列号、确认号、标志位(如SYN、ACK、FIN等)、窗口大小和校验和等。
+
+payload_offset = ETH_HLEN + hdr_len + doff;
+payload_length = __bpf_ntohs(tlen) - hdr_len - doff;
+
+这两行代码计算HTTP请求的载荷(payload)的偏移量和长度。它们将以太网帧头部长度、IP头部长度和TCP头部长度相加,得到HTTP请求的数据部分的偏移量,然后通过减去总长度、IP头部长度和TCP头部长度,计算出HTTP请求数据的长度。
+需要了解:
+
+- HTTP请求载荷(Payload):HTTP请求中包含的实际数据部分,通常是HTTP请求头和请求体。
+
+char line_buffer[7];
+if (payload_length < 7 || payload_offset < 0)
+{
+ return 0;
+}
+bpf_skb_load_bytes(skb, payload_offset, line_buffer, 7);
+bpf_printk("%d len %d buffer: %s", payload_offset, payload_length, line_buffer);
+
+这部分代码用于加载HTTP请求行的前7个字节,存储在名为line_buffer的字符数组中。然后,它检查HTTP请求数据的长度是否小于7字节或偏移量是否为负数,如果满足这些条件,说明HTTP请求不完整,直接返回0。最后,它使用bpf_printk函数将HTTP请求行的内容打印到内核日志中,以供调试和分析。
+if (bpf_strncmp(line_buffer, 3, "GET") != 0 &&
+ bpf_strncmp(line_buffer, 4, "POST") != 0 &&
+ bpf_strncmp(line_buffer, 3, "PUT") != 0 &&
+ bpf_strncmp(line_buffer, 6, "DELETE") != 0 &&
+ bpf_strncmp(line_buffer, 4, "HTTP") != 0)
+{
+ return 0;
+}
+
+这段代码使用bpf_strncmp函数比较line_buffer中的数据与HTTP请求方法(GET、POST、PUT、DELETE、HTTP)是否匹配。如果不匹配,说明不是HTTP请求,直接返回0,放弃处理。
+e = bpf_ringbuf_reserve(&rb, sizeof(*e), 0);
+if (!e)
+ return 0;
+
+这部分代码尝试从BPF环形缓冲区中保留一块内存以存储事件信息。如果无法保留内存块,返回0。BPF环形缓冲区用于在eBPF程序和用户空间之间传递事件数据。
+需要了解:
+
+- BPF环形缓冲区:BPF环形缓冲区是一种在eBPF程序和用户空间之间传递数据的机制。它可以用来存储事件信息,以便用户空间应用程序进行进一步处理或分析。
+
+e->ip_proto = ip_proto;
+bpf_skb_load_bytes(skb, nhoff + hdr_len, &(e->ports), 4);
+e->pkt_type = skb->pkt_type;
+e->ifindex = skb->ifindex;
+
+e->payload_length = payload_length;
+bpf_skb_load_bytes(skb, payload_offset, e->payload, MAX_BUF_SIZE);
+
+bpf_skb_load_bytes(skb, nhoff + offsetof(struct iphdr, saddr), &(e->src_addr), 4);
+bpf_skb_load_bytes(skb, nhoff + offsetof(struct iphdr, daddr), &(e->dst_addr), 4);
+bpf_ringbuf_submit(e, 0);
+
+return skb->len;
+
+最后,这段代码将捕获到的事件信息存储在e结构体中,并将
+其提交到BPF环形缓冲区。它包括了捕获的IP协议、源端口和目标端口、数据包类型、接口索引、载荷长度、源IP地址和目标IP地址等信息。最后,它返回数据包的长度,表示成功处理了数据包。
+这段代码主要用于将捕获的事件信息存储起来,以便后续的处理和分析。 BPF环形缓冲区用于将这些信息传递到用户空间,供用户空间应用程序进一步处理或记录。
+总结:这段eBPF程序的主要任务是捕获HTTP请求,它通过解析数据包的以太网帧、IP头部和TCP头部来确定数据包是否包含HTTP请求,并将有关请求的信息存储在so_event结构体中,然后提交到BPF环形缓冲区。这是一种高效的方法,可以在内核层面捕获HTTP流量,适用于网络监控和安全分析等应用。
+
+上述代码也存在一些潜在的缺陷,其中一个主要缺陷是它无法处理跨多个数据包的URL。
+
+- 跨包URL:代码中通过解析单个数据包来检查HTTP请求中的URL,如果HTTP请求的URL跨足够多的数据包,那么只会检查第一个数据包中的URL部分。这会导致丢失或部分记录那些跨多个数据包的长URL。
+
+解决这个问题的方法通常需要对多个数据包进行重新组装,以还原完整的HTTP请求。这可能需要在eBPF程序中实现数据包的缓存和组装逻辑,并在检测到HTTP请求结束之前等待并收集所有相关数据包。这需要更复杂的逻辑和额外的内存来处理跨多个数据包的情况。
+
+用户态代码的主要目的是创建一个原始套接字(raw socket),然后将先前在内核中定义的eBPF程序附加到该套接字上,从而允许eBPF程序捕获和处理从该套接字接收到的网络数据包,例如:
+ /* Create raw socket for localhost interface */
+ sock = open_raw_sock(interface);
+ if (sock < 0) {
+ err = -2;
+ fprintf(stderr, "Failed to open raw socket\n");
+ goto cleanup;
+ }
+
+ /* Attach BPF program to raw socket */
+ prog_fd = bpf_program__fd(skel->progs.socket_handler);
+ if (setsockopt(sock, SOL_SOCKET, SO_ATTACH_BPF, &prog_fd, sizeof(prog_fd))) {
+ err = -3;
+ fprintf(stderr, "Failed to attach to raw socket\n");
+ goto cleanup;
+ }
+
+
+sock = open_raw_sock(interface);:这行代码调用了一个自定义的函数open_raw_sock,该函数用于创建一个原始套接字。原始套接字允许用户态应用程序直接处理网络数据包,而不经过协议栈的处理。函数open_raw_sock可能需要一个参数 interface,用于指定网络接口,以便确定从哪个接口接收数据包。如果创建套接字失败,它将返回一个负数,否则返回套接字的文件描述符sock。
+- 如果
sock的值小于0,表示打开原始套接字失败,那么将err设置为-2,并在标准错误流上输出一条错误信息。
+prog_fd = bpf_program__fd(skel->progs.socket_handler);:这行代码获取之前在eBPF程序定义中的套接字过滤器程序(socket_handler)的文件描述符,以便后续将它附加到套接字上。skel是一个eBPF程序对象的指针,可以通过它来访问程序集合。
+setsockopt(sock, SOL_SOCKET, SO_ATTACH_BPF, &prog_fd, sizeof(prog_fd)):这行代码使用setsockopt系统调用将eBPF程序附加到原始套接字。它设置了SO_ATTACH_BPF选项,将eBPF程序的文件描述符传递给该选项,以便内核知道要将哪个eBPF程序应用于这个套接字。如果附加成功,套接字将开始捕获和处理从中接收到的网络数据包。
+- 如果
setsockopt失败,它将err设置为-3,并在标准错误流上输出一条错误信息。
+
+
+完整的源代码可以在 https://github.com/eunomia-bpf/bpf-developer-tutorial/tree/main/src/23-http 中找到。编译运行上述代码:
+$ git submodule update --init --recursive
+$ make
+ BPF .output/sockfilter.bpf.o
+ GEN-SKEL .output/sockfilter.skel.h
+ CC .output/sockfilter.o
+ BINARY sockfilter
+$ sudo ./sockfilter
+...
+
+在另外一个窗口中,使用 python 启动一个简单的 web server:
+python3 -m http.server
+Serving HTTP on 0.0.0.0 port 8000 (http://0.0.0.0:8000/) ...
+127.0.0.1 - - [18/Sep/2023 01:05:52] "GET / HTTP/1.1" 200 -
+
+可以使用 curl 发起请求:
+$ curl http://0.0.0.0:8000/
+<!DOCTYPE HTML>
+<html lang="en">
+<head>
+<meta charset="utf-8">
+<title>Directory listing for /</title>
+....
+
+在 eBPF 程序中,可以看到打印出了 HTTP 请求的内容:
+127.0.0.1:34552(src) -> 127.0.0.1:8000(dst)
+payload: GET / HTTP/1.1
+Host: 0.0.0.0:8000
+User-Agent: curl/7.88.1
+...
+127.0.0.1:8000(src) -> 127.0.0.1:34552(dst)
+payload: HTTP/1.0 200 OK
+Server: SimpleHTTP/0.6 Python/3.11.4
+...
+
+分别包含了请求和响应的内容。
+
+eBPF 提供了一种强大的机制,允许我们在内核级别追踪系统调用。在这个示例中,我们将使用 eBPF 追踪 accept 和 read 系统调用,以捕获 HTTP 流量。由于篇幅有限,这里我们仅仅对代码框架做简要的介绍。
+struct
+{
+ __uint(type, BPF_MAP_TYPE_HASH);
+ __uint(max_entries, 4096);
+ __type(key, u64);
+ __type(value, struct accept_args_t);
+} active_accept_args_map SEC(".maps");
+
+// 定义在 accept 系统调用入口的追踪点
+SEC("tracepoint/syscalls/sys_enter_accept")
+int sys_enter_accept(struct trace_event_raw_sys_enter *ctx)
+{
+ u64 id = bpf_get_current_pid_tgid();
+ // ... 获取和存储 accept 调用的参数
+ bpf_map_update_elem(&active_accept_args_map, &id, &accept_args, BPF_ANY);
+ return 0;
+}
+
+// 定义在 accept 系统调用退出的追踪点
+SEC("tracepoint/syscalls/sys_exit_accept")
+int sys_exit_accept(struct trace_event_raw_sys_exit *ctx)
+{
+ // ... 处理 accept 调用的结果
+ struct accept_args_t *args =
+ bpf_map_lookup_elem(&active_accept_args_map, &id);
+ // ... 获取和存储 accept 调用获得的 socket 文件描述符
+ __u64 pid_fd = ((__u64)pid << 32) | (u32)ret_fd;
+ bpf_map_update_elem(&conn_info_map, &pid_fd, &conn_info, BPF_ANY);
+ // ...
+}
+
+struct
+{
+ __uint(type, BPF_MAP_TYPE_HASH);
+ __uint(max_entries, 4096);
+ __type(key, u64);
+ __type(value, struct data_args_t);
+} active_read_args_map SEC(".maps");
+
+// 定义在 read 系统调用入口的追踪点
+SEC("tracepoint/syscalls/sys_enter_read")
+int sys_enter_read(struct trace_event_raw_sys_enter *ctx)
+{
+ // ... 获取和存储 read 调用的参数
+ bpf_map_update_elem(&active_read_args_map, &id, &read_args, BPF_ANY);
+ return 0;
+}
+
+// 辅助函数,检查是否为 HTTP 连接
+static inline bool is_http_connection(const char *line_buffer, u64 bytes_count)
+{
+ // ... 检查数据是否为 HTTP 请求或响应
+}
+
+// 辅助函数,处理读取的数据
+static inline void process_data(struct trace_event_raw_sys_exit *ctx,
+ u64 id, const struct data_args_t *args, u64 bytes_count)
+{
+ // ... 处理读取的数据,检查是否为 HTTP 流量,并发送事件
+ if (is_http_connection(line_buffer, bytes_count))
+ {
+ // ...
+ bpf_probe_read_kernel(&event.msg, read_size, args->buf);
+ // ...
+ bpf_perf_event_output(ctx, &events, BPF_F_CURRENT_CPU,
+ &event, sizeof(struct socket_data_event_t));
+ }
+}
+
+// 定义在 read 系统调用退出的追踪点
+SEC("tracepoint/syscalls/sys_exit_read")
+int sys_exit_read(struct trace_event_raw_sys_exit *ctx)
+{
+ // ... 处理 read 调用的结果
+ struct data_args_t *read_args = bpf_map_lookup_elem(&active_read_args_map, &id);
+ if (read_args != NULL)
+ {
+ process_data(ctx, id, read_args, bytes_count);
+ }
+ // ...
+ return 0;
+}
+
+char _license[] SEC("license") = "GPL";
+
+这段代码简要展示了如何使用eBPF追踪Linux内核中的系统调用来捕获HTTP流量。以下是对代码的hook位置和流程的详细解释,以及需要hook哪些系统调用来实现完整的请求追踪:
+
+
+-
+
该代码使用了eBPF的Tracepoint功能,具体来说,它定义了一系列的eBPF程序,并将它们绑定到了特定的系统调用的Tracepoint上,以捕获这些系统调用的入口和退出事件。
+
+-
+
首先,它定义了两个eBPF哈希映射(active_accept_args_map和active_read_args_map)来存储系统调用参数。这些映射用于跟踪accept和read系统调用。
+
+-
+
接着,它定义了多个Tracepoint追踪程序,其中包括:
+
+sys_enter_accept:定义在accept系统调用的入口处,用于捕获accept系统调用的参数,并将它们存储在哈希映射中。
+sys_exit_accept:定义在accept系统调用的退出处,用于处理accept系统调用的结果,包括获取和存储新的套接字文件描述符以及建立连接的相关信息。
+sys_enter_read:定义在read系统调用的入口处,用于捕获read系统调用的参数,并将它们存储在哈希映射中。
+sys_exit_read:定义在read系统调用的退出处,用于处理read系统调用的结果,包括检查读取的数据是否为HTTP流量,如果是,则发送事件。
+
+
+-
+
在sys_exit_accept和sys_exit_read中,还涉及一些数据处理和事件发送的逻辑,例如检查数据是否为HTTP连接,组装事件数据,并使用bpf_perf_event_output将事件发送到用户空间供进一步处理。
+
+
+
+要实现完整的HTTP请求追踪,通常需要hook的系统调用包括:
+
+socket:用于捕获套接字创建,以追踪新的连接。
+bind:用于获取绑定的端口信息。
+listen:用于开始监听连接请求。
+accept:用于接受连接请求,获取新的套接字文件描述符。
+read:用于捕获接收到的数据,以检查其中是否包含 HTTP 请求。
+write:用于捕获发送的数据,以检查其中是否包含 HTTP 响应。
+
+上述代码已经涵盖了accept和read系统调用的追踪。要完整实现HTTP请求的追踪,还需要hook其他系统调用,并实现相应的逻辑来处理这些系统调用的参数和结果。
+完整的源代码可以在 https://github.com/eunomia-bpf/bpf-developer-tutorial/tree/main/src/23-http 中找到。
+
+在当今复杂的技术环境中,系统的可观测性变得至关重要,特别是在微服务和云原生应用程序的背景下。本文探讨了如何利用eBPF技术来追踪七层协议,以及在这个过程中可能面临的挑战和解决方案。以下是对本文内容的总结:
+
+-
+
背景介绍:
+
+- 现代应用程序通常由多个微服务和分布式组件组成,因此观测整个系统的行为至关重要。
+- 七层协议(如HTTP、gRPC、MQTT等)提供了深入了解应用程序交互的详细信息,但监控这些协议通常具有挑战性。
+
+
+-
+
eBPF技术的作用:
+
+- eBPF允许开发者在不修改或插入应用程序代码的情况下,深入内核层来实时观测和分析系统行为。
+- eBPF技术为监控七层协议提供了一个强大的工具,特别适用于微服务环境。
+
+
+-
+
追踪七层协议:
+
+- 本文介绍了如何追踪HTTP等七层协议的挑战,包括协议的复杂性和动态性。
+- 传统的网络监控工具难以应对七层协议的复杂性。
+
+
+-
+
eBPF的应用:
+
+- eBPF提供两种主要方法来追踪七层协议:socket filter和syscall trace。
+- 这两种方法可以帮助捕获HTTP等协议的网络请求数据,并分析它们。
+
+
+-
+
eBPF实践教程:
+
+- 本文提供了一个实际的eBPF教程,演示如何使用eBPF socket filter或syscall trace来捕获和分析HTTP流量。
+- 教程内容包括开发eBPF程序、使用eBPF工具链和实施HTTP请求的追踪。
+
+
+
+通过这篇文章,读者可以获得深入了解如何使用eBPF技术来追踪七层协议,尤其是HTTP流量的知识。这将有助于更好地监控和分析网络流量,从而提高应用程序性能和安全性。如果您希望学习更多关于 eBPF 的知识和实践,可以访问我们的教程代码仓库 https://github.com/eunomia-bpf/bpf-developer-tutorial 或网站 https://eunomia.dev/zh/tutorials/ 以获取更多示例和完整的教程。
随着TLS在现代网络环境中的广泛应用,跟踪微服务RPC消息已经变得愈加棘手。传统的流量嗅探技术常常受限于只能获取到加密后的数据,导致无法真正观察到通信的原始内容。这种限制为系统的调试和分析带来了不小的障碍。
但现在,我们有了新的解决方案。使用 eBPF 技术,通过其能力在用户空间进行探测,提供了一种方法重新获得明文数据,使得我们可以直观地查看加密前的通信内容。然而,每个应用可能使用不同的库,每个库都有多个版本,这种多样性给跟踪带来了复杂性。
@@ -4868,7 +5441,7 @@ WRITE/SEND 0.000000000 curl 16104 24
...
-
+
eBPF 是一个非常强大的技术,它可以帮助我们深入了解系统的工作原理。本教程是一个简单的示例,展示了如何使用 eBPF 来监控 SSL/TLS 通信。如果您对 eBPF 技术感兴趣,并希望进一步了解和实践,可以访问我们的教程代码仓库 https://github.com/eunomia-bpf/bpf-developer-tutorial 和教程网站 https://eunomia.dev/zh/tutorials/。
参考资料:
@@ -5283,7 +5856,7 @@ root 31524 0.0 0.0 22004 812 pts/3 Ss 05:42 0:00 sudo ./pidhid
root 31525 0.3 0.0 3808 2456 pts/3 S+ 05:42 0:00 ./pidhide -p 1534
yunwei 31583 0.0 0.0 17712 2612 pts/1 S+ 05:42 0:00 grep --color=auto 1534
-
+
通过本篇 eBPF 入门实践教程,我们深入了解了如何使用 eBPF 来隐藏进程或文件信息。我们学习了如何编写和加载 eBPF 程序,如何通过 eBPF 拦截系统调用并修改它们的行为,以及如何将这些知识应用到实际的网络安全和防御工作中。此外,我们也了解了 eBPF 的强大性,尤其是它能在不需要修改内核源代码或重启内核的情况下,允许用户在内核中执行自定义代码的能力。
您还可以访问我们的教程代码仓库 https://github.com/eunomia-bpf/bpf-developer-tutorial 或网站 https://eunomia.dev/zh/tutorials/ 以获取更多示例和完整的教程。
@@ -5413,13 +5986,14 @@ int bpf_dos(struct trace_event_raw_sys_enter *ctx)
总结:这个 eBPF 程序提供了一个方法,允许系统管理员或安全团队在内核级别监控和干预 ptrace 调用,提供了一个对抗潜在恶意活动或误操作的额外层次。
-
+
eunomia-bpf 是一个结合 Wasm 的开源 eBPF 动态加载运行时和开发工具链,它的目的是简化 eBPF 程序的开发、构建、分发、运行。可以参考 https://github.com/eunomia-bpf/eunomia-bpf 下载和安装 ecc 编译工具链和 ecli 运行时。我们使用 eunomia-bpf 编译运行这个例子。
编译:
./ecc signal.bpf.c signal.h
使用方式:
$ sudo ./ecli package.json
+TIME PID COMM SUCCESS
这个程序会对任何试图使用 ptrace 系统调用的程序,例如 strace,发出 SIG_KILL 信号。
一旦 eBPF 程序开始运行,你可以通过运行以下命令进行测试:
@@ -5432,7 +6006,7 @@ TIME PID COMM SUCCESS
13:54:45 8857 strace true
完整的源代码可以参考:https://github.com/eunomia-bpf/bpf-developer-tutorial/tree/main/src/25-signal
-
+
通过这个实例,我们深入了解了如何将 eBPF 程序与用户态程序相结合,实现对系统调用的监控和干预。eBPF 提供了一种在内核空间执行程序的机制,这种技术不仅限于监控,还可用于性能优化、安全防御、系统诊断等多种场景。对于开发者来说,这为Linux系统的性能调优和故障排查提供了一种强大且灵活的工具。
最后,如果您对 eBPF 技术感兴趣,并希望进一步了解和实践,可以访问我们的教程代码仓库 https://github.com/eunomia-bpf/bpf-developer-tutorial 和教程网站 https://eunomia.dev/zh/tutorials/。
@@ -5923,7 +6497,7 @@ WRITE/SEND 0.000000000 curl 16104 24
...
-
+
eBPF 是一个非常强大的技术,它可以帮助我们深入了解系统的工作原理。本教程是一个简单的示例,展示了如何使用 eBPF 来监控 SSL/TLS 通信。如果您对 eBPF 技术感兴趣,并希望进一步了解和实践,可以访问我们的教程代码仓库 https://github.com/eunomia-bpf/bpf-developer-tutorial 和教程网站 https://eunomia.dev/zh/tutorials/。
参考资料:
diff --git a/searchindex.js b/searchindex.js
index 8e65c51..1153a6f 100644
--- a/searchindex.js
+++ b/searchindex.js
@@ -1 +1 @@
-Object.assign(window.search, {"doc_urls":["https://github.com/eunomia-bpf/bpf-developer-tutorial.html#httpsgithubcomeunomia-bpfbpf-developer-tutorial","0-introduce/index.html#ebpf-入门开发实践教程零介绍-ebpf-的基本概念常见的开发工具","0-introduce/index.html#1-ebpf简介安全和有效地扩展内核","0-introduce/index.html#ebpf-的未来内核的-javascript-可编程接口","0-introduce/index.html#2-关于如何学习-ebpf-相关的开发的一些建议","0-introduce/index.html#ebpf-入门5-7h","0-introduce/index.html#了解如何开发-ebpf-程序10-15h","0-introduce/index.html#3-如何使用ebpf编程","0-introduce/index.html#编写-ebpf-程序","0-introduce/index.html#bcc","0-introduce/index.html#ebpf-go-library","0-introduce/index.html#libbpf","0-introduce/index.html#eunomia-bpf","0-introduce/index.html#参考资料","1-helloworld/index.html#ebpf-入门开发实践教程一hello-world基本框架和开发流程","1-helloworld/index.html#ebpf开发环境准备与基本开发流程","1-helloworld/index.html#安装必要的软件和工具","1-helloworld/index.html#下载安装-eunomia-bpf-开发工具","1-helloworld/index.html#hello-world---minimal-ebpf-program","1-helloworld/index.html#ebpf-程序的基本框架","1-helloworld/index.html#tracepoints","1-helloworld/index.html#github-模板轻松构建-ebpf-项目和开发环境","1-helloworld/index.html#总结","2-kprobe-unlink/index.html#ebpf-入门开发实践教程二在-ebpf-中使用-kprobe-监测捕获-unlink-系统调用","2-kprobe-unlink/index.html#kprobes-技术背景","2-kprobe-unlink/index.html#kprobe-示例","2-kprobe-unlink/index.html#总结","3-fentry-unlink/index.html#ebpf-入门开发实践教程三在-ebpf-中使用-fentry-监测捕获-unlink-系统调用","3-fentry-unlink/index.html#fentry","3-fentry-unlink/index.html#总结","4-opensnoop/index.html#ebpf-入门开发实践教程四在-ebpf-中捕获进程打开文件的系统调用集合使用全局变量过滤进程-pid","4-opensnoop/index.html#在-ebpf-中捕获进程打开文件的系统调用集合","4-opensnoop/index.html#使用全局变量在-ebpf-中过滤进程-pid","4-opensnoop/index.html#总结","5-uprobe-bashreadline/index.html#ebpf-入门开发实践教程五在-ebpf-中使用--uprobe-捕获-bash-的-readline-函数调用","5-uprobe-bashreadline/index.html#什么是uprobe","5-uprobe-bashreadline/index.html#使用-uprobe-捕获-bash-的-readline-函数调用","5-uprobe-bashreadline/index.html#总结","6-sigsnoop/index.html#ebpf-入门开发实践教程六捕获进程发送信号的系统调用集合使用-hash-map-保存状态","6-sigsnoop/index.html#sigsnoop","6-sigsnoop/index.html#总结","7-execsnoop/index.html#ebpf-入门实践教程七捕获进程执行事件通过-perf-event-array-向用户态打印输出","7-execsnoop/index.html#perf-buffer","7-execsnoop/index.html#execsnoop","7-execsnoop/index.html#总结","8-exitsnoop/index.html#ebpf-入门开发实践教程八在-ebpf-中使用-exitsnoop-监控进程退出事件使用-ring-buffer-向用户态打印输出","8-exitsnoop/index.html#ring-buffer","8-exitsnoop/index.html#ebpf-ringbuf-vs-ebpf-perfbuf","8-exitsnoop/index.html#exitsnoop","8-exitsnoop/index.html#compile-and-run","8-exitsnoop/index.html#总结","9-runqlat/index.html#ebpf-入门开发实践教程九捕获进程调度延迟以直方图方式记录","9-runqlat/index.html#runqlat-原理","9-runqlat/index.html#runqlat-代码实现","9-runqlat/index.html#runqlatbpfc","9-runqlat/index.html#runqlath","9-runqlat/index.html#编译运行","9-runqlat/index.html#总结","10-hardirqs/index.html#ebpf-入门开发实践教程十在-ebpf-中使用-hardirqs-或-softirqs-捕获中断事件","10-hardirqs/index.html#hardirqs-和-softirqs-是什么","10-hardirqs/index.html#实现原理","10-hardirqs/index.html#hardirqs-代码实现","10-hardirqs/index.html#运行代码","10-hardirqs/index.html#总结","11-bootstrap/index.html#ebpf-入门开发实践教程十一在-ebpf-中使用-libbpf-开发用户态程序并跟踪-exec-和-exit-系统调用","11-bootstrap/index.html#libbpf-库以及为什么需要使用它","11-bootstrap/index.html#什么是-bootstrap","11-bootstrap/index.html#bootstrap","11-bootstrap/index.html#内核态-ebpf-程序-bootstrapbpfc","11-bootstrap/index.html#用户态bootstrapc","11-bootstrap/index.html#安装依赖","11-bootstrap/index.html#编译运行","11-bootstrap/index.html#总结","13-tcpconnlat/index.html#ebpf入门开发实践教程十三统计-tcp-连接延时并使用-libbpf-在用户态处理数据","13-tcpconnlat/index.html#背景","13-tcpconnlat/index.html#tcpconnlat-工具概述","13-tcpconnlat/index.html#tcp-连接原理","13-tcpconnlat/index.html#tcpconnlat-的-ebpf-实现","13-tcpconnlat/index.html#tcp_v4_connect-函数解析","13-tcpconnlat/index.html#内核态代码","13-tcpconnlat/index.html#用户态数据处理","13-tcpconnlat/index.html#编译运行","13-tcpconnlat/index.html#总结","14-tcpstates/index.html#ebpf入门实践教程十四记录-tcp-连接状态与-tcp-rtt","14-tcpstates/index.html#tcprtt-与-tcpstates","14-tcpstates/index.html#tcpstate","14-tcpstates/index.html#定义-bpf-maps","14-tcpstates/index.html#追踪-tcp-连接状态变化","14-tcpstates/index.html#更新时间戳","14-tcpstates/index.html#tcprtt","14-tcpstates/index.html#编译运行","14-tcpstates/index.html#总结","15-javagc/index.html#ebpf-入门实践教程十五使用-usdt-捕获用户态-java-gc-事件耗时","15-javagc/index.html#usdt-介绍","15-javagc/index.html#用户层面的追踪机制用户级动态跟踪和-usdt","15-javagc/index.html#java-gc-介绍","15-javagc/index.html#ebpf-实现机制","15-javagc/index.html#内核态程序","15-javagc/index.html#用户态程序","15-javagc/index.html#安装依赖","15-javagc/index.html#编译运行","15-javagc/index.html#总结","16-memleak/index.html#ebpf-入门实践教程十六编写-ebpf-程序-memleak-监控内存泄漏","16-memleak/index.html#背景及其重要性","16-memleak/index.html#调试内存泄漏的挑战","16-memleak/index.html#ebpf-的作用","16-memleak/index.html#memleak-的实现原理","16-memleak/index.html#内核态-ebpf-程序实现","16-memleak/index.html#memleak-内核态-ebpf-程序实现","16-memleak/index.html#用户态程序","16-memleak/index.html#编译运行","16-memleak/index.html#总结","17-biopattern/index.html#ebpf-入门实践教程十七编写-ebpf-程序统计随机顺序磁盘-io","17-biopattern/index.html#随机顺序磁盘-io","17-biopattern/index.html#biopattern","17-biopattern/index.html#ebpf-biopattern-实现原理","17-biopattern/index.html#用户态代码","17-biopattern/index.html#总结","18-further-reading/index.html#更多的参考资料","19-lsm-connect/index.html#ebpf-入门实践教程使用-lsm-进行安全检测防御","19-lsm-connect/index.html#背景","19-lsm-connect/index.html#lsm-概述","19-lsm-connect/index.html#确认-bpf-lsm-是否可用","19-lsm-connect/index.html#编写-ebpf-程序","19-lsm-connect/index.html#编译运行","19-lsm-connect/index.html#总结","19-lsm-connect/index.html#参考","20-tc/index.html#ebpf-入门实践教程二十使用-ebpf-进行-tc-流量控制","20-tc/index.html#背景","20-tc/index.html#tc-概述","20-tc/index.html#编写-ebpf-程序","20-tc/index.html#编译运行","20-tc/index.html#总结","20-tc/index.html#参考","22-android/index.html#在-andorid-上使用-ebpf-程序","22-android/index.html#背景","22-android/index.html#测试环境","22-android/index.html#环境搭建","22-android/index.html#工具构建","22-android/index.html#结果","22-android/index.html#成功案例","22-android/index.html#一些可能的报错原因","22-android/index.html#总结","22-android/index.html#参考","23-http/index.html#http","30-sslsniff/index.html#ebpf-实践教程使用-uprobe-捕获多种库的-ssltls-明文数据","30-sslsniff/index.html#背景知识","30-sslsniff/index.html#ssl-和-tls","30-sslsniff/index.html#tls-的工作原理","30-sslsniff/index.html#ebpf-和-uprobe","30-sslsniff/index.html#用户态库","30-sslsniff/index.html#openssl-api-分析","30-sslsniff/index.html#1-ssl_read-函数","30-sslsniff/index.html#2-ssl_write-函数","30-sslsniff/index.html#ebpf-内核态代码编写","30-sslsniff/index.html#数据结构","30-sslsniff/index.html#hook-函数","30-sslsniff/index.html#hook到握手过程","30-sslsniff/index.html#用户态辅助代码分析与解读","30-sslsniff/index.html#1-支持的库挂载","30-sslsniff/index.html#2-详细挂载逻辑","30-sslsniff/index.html#编译与运行","30-sslsniff/index.html#启动-sslsniff","30-sslsniff/index.html#执行-curl-命令","30-sslsniff/index.html#sslsniff-输出","30-sslsniff/index.html#显示延迟和握手过程","30-sslsniff/index.html#16进制输出","30-sslsniff/index.html#总结","29-sockops/index.html#ebpf-sockops-示例","29-sockops/index.html#利用-ebpf-的-sockops-进行性能优化","29-sockops/index.html#运行样例","29-sockops/index.html#编译-ebpf-程序","29-sockops/index.html#加载-ebpf-程序","29-sockops/index.html#运行--iperf3--服务器","29-sockops/index.html#运行--iperf3--客户端","29-sockops/index.html#收集追踪","29-sockops/index.html#卸载-ebpf-程序","29-sockops/index.html#参考资料","24-hide/index.html#ebpf-开发实践使用-ebpf-隐藏进程或文件信息","24-hide/index.html#背景知识与实现机制","24-hide/index.html#内核态-ebpf-程序实现","24-hide/index.html#用户态-ebpf-程序实现","24-hide/index.html#编译运行隐藏-pid","24-hide/index.html#总结","25-signal/index.html#ebpf-入门实践教程用-bpf_send_signal-发送信号终止恶意进程","25-signal/index.html#使用场景","25-signal/index.html#现有方案的不足","25-signal/index.html#新方案的优势","25-signal/index.html#内核态代码分析","25-signal/index.html#代码分析","25-signal/index.html#1-数据结构定义-signalh","25-signal/index.html#2-ebpf-程序-signalbpfc","25-signal/index.html#编译运行","25-signal/index.html#总结","25-signal/index.html#参考资料","26-sudo/index.html#使用-ebpf-添加-sudo-用户","26-sudo/index.html#参考资料","27-replace/index.html#使用-ebpf-替换任意程序读取或写入的文本","27-replace/index.html#参考资料","28-detach/index.html#在用户态应用退出后运行-ebpf-程序ebpf-程序的生命周期","28-detach/index.html#ebpf-程序的生命周期","28-detach/index.html#运行","28-detach/index.html#参考资料","30-sslsniff/index.html#ebpf-实践教程使用-uprobe-捕获多种库的-ssltls-明文数据","30-sslsniff/index.html#背景知识","30-sslsniff/index.html#ssl-和-tls","30-sslsniff/index.html#tls-的工作原理","30-sslsniff/index.html#ebpf-和-uprobe","30-sslsniff/index.html#用户态库","30-sslsniff/index.html#openssl-api-分析","30-sslsniff/index.html#1-ssl_read-函数","30-sslsniff/index.html#2-ssl_write-函数","30-sslsniff/index.html#ebpf-内核态代码编写","30-sslsniff/index.html#数据结构","30-sslsniff/index.html#hook-函数","30-sslsniff/index.html#hook到握手过程","30-sslsniff/index.html#用户态辅助代码分析与解读","30-sslsniff/index.html#1-支持的库挂载","30-sslsniff/index.html#2-详细挂载逻辑","30-sslsniff/index.html#编译与运行","30-sslsniff/index.html#启动-sslsniff","30-sslsniff/index.html#执行-curl-命令","30-sslsniff/index.html#sslsniff-输出","30-sslsniff/index.html#显示延迟和握手过程","30-sslsniff/index.html#16进制输出","30-sslsniff/index.html#总结","bcc-documents/kernel-versions.html#linux-内核版本的-bpf-功能","bcc-documents/kernel-versions.html#ebpf支持","bcc-documents/kernel-versions.html#jit编译","bcc-documents/kernel-versions.html#主要特性","bcc-documents/kernel-versions.html#程序类型","bcc-documents/kernel-versions.html#map-types--aka--表格-在-bcc-术语中","bcc-documents/kernel-versions.html#map-类型","bcc-documents/kernel-versions.html#map-userspace-api","bcc-documents/kernel-versions.html#xdp","bcc-documents/kernel-versions.html#程序类型-1","bcc-documents/kernel_config.html#bpf-特性的内核配置","bcc-documents/kernel_config.html#与-bpf-相关的内核配置","bcc-documents/reference_guide.html#bcc-参考指南","bcc-documents/reference_guide.html#目录","bcc-documents/reference_guide.html#bpf-c","bcc-documents/reference_guide.html#events--arguments","bcc-documents/reference_guide.html#1-kprobes","bcc-documents/reference_guide.html#2-kretprobes","bcc-documents/reference_guide.html#3-tracepoints","bcc-documents/reference_guide.html#4-uprobes","bcc-documents/reference_guide.html#6-usdt探测点","bcc-documents/reference_guide.html#7-原始跟踪点","bcc-documents/reference_guide.html#8-系统调用跟踪点","bcc-documents/reference_guide.html#9-kfuncs","bcc-documents/reference_guide.html#10-kretfuncs","bcc-documents/reference_guide.html#11-lsm-probes","bcc-documents/reference_guide.html#12-bpf迭代器","bcc-documents/reference_guide.html#数据","bcc-documents/reference_guide.html#1-bpf_probe_read_kernel","bcc-documents/reference_guide.html#2-bpf_probe_read_kernel_strshell","bcc-documents/reference_guide.html#3-bpf_ktime_get_ns","bcc-documents/reference_guide.html#4-bpf_get_current_pid_tgid","bcc-documents/reference_guide.html#5-bpf_get_current_uid_gid","bcc-documents/reference_guide.html#6-bpf_get_current_comm","bcc-documents/reference_guide.html#7-bpf_get_current_task","bcc-documents/reference_guide.html#8-bpf_log2l","bcc-documents/reference_guide.html#9-bpf_get_prandom_u32","bcc-documents/reference_guide.html#10-bpf_probe_read_user","bcc-documents/reference_guide.html#11-bpf_probe_read_user_str","bcc-documents/reference_guide.html#12-bpf_get_ns_current_pid_tgid","bcc-documents/reference_guide.html#调试","bcc-documents/reference_guide.html#1-bpf_override_return","bcc-documents/reference_guide.html#输出","bcc-documents/reference_guide.html#1-bpf_trace_printk","bcc-documents/reference_guide.html#2-bpf_perf_output","bcc-documents/reference_guide.html#3-perf_submit","bcc-documents/reference_guide.html#4-perf_submit_skb","bcc-documents/reference_guide.html#5-bpf_ringbuf_output","bcc-documents/reference_guide.html#6-ringbuf_output","bcc-documents/reference_guide.html#7-ringbuf_reserve","bcc-documents/reference_guide.html#8-ringbuf_submit","bcc-documents/reference_guide.html#9-ringbuf_discard","bcc-documents/reference_guide.html#maps","bcc-documents/reference_guide.html#1-bpf_table","bcc-documents/reference_guide.html#2-bpf_hash","bcc-documents/reference_guide.html#3-bpf_array","bcc-documents/reference_guide.html#4-bpf_histogram","bcc-documents/reference_guide.html#5-bpf_stack_trace","bcc-documents/reference_guide.html#6-bpf_perf_array","bcc-documents/reference_guide.html#7-bpf_percpu_hash","bcc-documents/reference_guide.html#8-bpf_percpu_array","bcc-documents/reference_guide.html#9-bpf_lpm_trie","bcc-documents/reference_guide.html#10-bpf_prog_array","bcc-documents/reference_guide.html#11-bpf_devmap","bcc-documents/reference_guide.html#12-bpf_cpumap","bcc-documents/reference_guide.html#13-bpf_xskmap","bcc-documents/reference_guide.html#14-bpf_array_of_maps","bcc-documents/reference_guide.html#15-bpf_hash_of_maps","bcc-documents/reference_guide.html#16-bpf_stack","bcc-documents/reference_guide.html#17-bpf_queue","bcc-documents/reference_guide.html#18-bpf_sockhash","bcc-documents/reference_guide.html#19-maplookup","bcc-documents/reference_guide.html#20-maplookup_or_try_init","bcc-documents/reference_guide.html#21-mapdelete","bcc-documents/reference_guide.html#22-mapupdate","bcc-documents/reference_guide.html#23-mapinsert","bcc-documents/reference_guide.html#24-mapincrement","bcc-documents/reference_guide.html#25-mapget_stackid","bcc-documents/reference_guide.html#26-mapperf_read","bcc-documents/reference_guide.html#27-mapcall","bcc-documents/reference_guide.html#28-mapredirect_map","bcc-documents/reference_guide.html#29-mappush","bcc-documents/reference_guide.html#30-mappop","bcc-documents/reference_guide.html#31-mappeek","bcc-documents/reference_guide.html#32-mapsock_hash_update","bcc-documents/reference_guide.html#33-mapmsg_redirect_hash","bcc-documents/reference_guide.html#34-mapsk_redirect_hash","bcc-documents/reference_guide.html#许可证","bcc-documents/reference_guide.html#rewriter","bcc-documents/reference_guide.html#bcc-python","bcc-documents/reference_guide.html#初始化","bcc-documents/reference_guide.html#1-bpf","bcc-documents/reference_guide.html#事件","bcc-documents/reference_guide.html#1-attach_kprobe","bcc-documents/reference_guide.html#2-attach_kretprobe","bcc-documents/reference_guide.html#3-attach_tracepoint","bcc-documents/reference_guide.html#4-attach_uprobe","bcc-documents/reference_guide.html#5-attach_uretprobe","bcc-documents/reference_guide.html#6-usdtenable_probe","bcc-documents/reference_guide.html#7-attach_raw_tracepoint","bcc-documents/reference_guide.html#8-attach_raw_socket","bcc-documents/reference_guide.html#9-attach_xdp","bcc-documents/reference_guide.html#10-attach_func","bcc-documents/reference_guide.html#12-detach_kprobe","bcc-documents/reference_guide.html#13-detach_kretprobe","bcc-documents/reference_guide.html#调试输出","bcc-documents/reference_guide.html#1-trace_print","bcc-documents/reference_guide.html#2-trace_fields","bcc-documents/reference_guide.html#输出-api","bcc-documents/reference_guide.html#1-perf_buffer_poll","bcc-documents/reference_guide.html#2-ring_buffer_poll","bcc-documents/reference_guide.html#3-ring_buffer_consume","bcc-documents/reference_guide.html#map-apis","bcc-documents/reference_guide.html#1-get_table","bcc-documents/reference_guide.html#2-open_perf_buffer","bcc-documents/reference_guide.html#4-values","bcc-documents/reference_guide.html#5-clear","bcc-documents/reference_guide.html#6-items_lookup_and_delete_batch","bcc-documents/reference_guide.html#7-items_lookup_batch","bcc-documents/reference_guide.html#8-items_delete_batch","bcc-documents/reference_guide.html#9-items_update_batch","bcc-documents/reference_guide.html#11-print_linear_hist语法-tableprint_linear_histval_typevalue-section_headerbucket-ptr-section_print_fnnone","bcc-documents/reference_guide.html#12-open_ring_buffer","bcc-documents/reference_guide.html#13-push","bcc-documents/reference_guide.html#14-pop","bcc-documents/reference_guide.html#15-peek","bcc-documents/reference_guide.html#辅助方法","bcc-documents/reference_guide.html#1-ksym","bcc-documents/reference_guide.html#2-ksymname","bcc-documents/reference_guide.html#3-sym","bcc-documents/reference_guide.html#4-num_open_kprobes","bcc-documents/reference_guide.html#5-get_syscall_fnname","bcc-documents/reference_guide.html#bpf-错误","bcc-documents/reference_guide.html#1-invalid-mem-access","bcc-documents/reference_guide.html#2-无法从专有程序调用-gpl-only-函数","bcc-documents/reference_guide.html#环境变量","bcc-documents/reference_guide.html#1-内核源代码目录","bcc-documents/reference_guide.html#2-内核版本覆盖","bcc-documents/special_filtering.html#特殊过滤","bcc-documents/special_filtering.html#按-cgroups过滤","bcc-documents/special_filtering.html#按命名空间选择挂载点进行过滤","bcc-documents/tutorial.html#bcc-教程","bcc-documents/tutorial.html#可观察性","bcc-documents/tutorial.html#0-使用bcc之前","bcc-documents/tutorial.html#1-性能分析","bcc-documents/tutorial.html#2-使用通用工具进行可观察性","bcc-documents/tutorial.html#网络","bcc-documents/tutorial_bcc_python_developer.html#bcc-python-开发者教程","bcc-documents/tutorial_bcc_python_developer.html#可观测性","bcc-documents/tutorial_bcc_python_developer.html#第1课-你好世界","bcc-documents/tutorial_bcc_python_developer.html#第二课-sys_sync","bcc-documents/tutorial_bcc_python_developer.html#第三课-hello_fieldspy","bcc-documents/tutorial_bcc_python_developer.html#lesson-4-sync_timingpy","bcc-documents/tutorial_bcc_python_developer.html#第5课-sync_countpy","bcc-documents/tutorial_bcc_python_developer.html#第6课-disksnooppy","bcc-documents/tutorial_bcc_python_developer.html#lesson-7-hello_perf_outputpy","bcc-documents/tutorial_bcc_python_developer.html#第八课-sync_perf_outputpy","bcc-documents/tutorial_bcc_python_developer.html#第九课-bitehistpy","bcc-documents/tutorial_bcc_python_developer.html#lesson-10-disklatencypy-lesson-11-vfsreadlatpy","bcc-documents/tutorial_bcc_python_developer.html#lesson-12-urandomreadpy","bcc-documents/tutorial_bcc_python_developer.html#第13课-disksnooppy已修复","bcc-documents/tutorial_bcc_python_developer.html#第14课-strlen_countpy","bcc-documents/tutorial_bcc_python_developer.html#第15课nodejs_http_serverpy","bcc-documents/tutorial_bcc_python_developer.html#第16课-task_switchc","bcc-documents/tutorial_bcc_python_developer.html#第17课-进一步研究","bcc-documents/tutorial_bcc_python_developer.html#网络"],"index":{"documentStore":{"docInfo":{"0":{"body":0,"breadcrumbs":8,"title":4},"1":{"body":0,"breadcrumbs":3,"title":2},"10":{"body":6,"breadcrumbs":4,"title":3},"100":{"body":51,"breadcrumbs":3,"title":0},"101":{"body":17,"breadcrumbs":3,"title":0},"102":{"body":4,"breadcrumbs":5,"title":3},"103":{"body":2,"breadcrumbs":2,"title":0},"104":{"body":5,"breadcrumbs":2,"title":0},"105":{"body":48,"breadcrumbs":3,"title":1},"106":{"body":12,"breadcrumbs":3,"title":1},"107":{"body":0,"breadcrumbs":3,"title":1},"108":{"body":564,"breadcrumbs":4,"title":2},"109":{"body":88,"breadcrumbs":2,"title":0},"11":{"body":10,"breadcrumbs":2,"title":1},"110":{"body":61,"breadcrumbs":2,"title":0},"111":{"body":19,"breadcrumbs":2,"title":0},"112":{"body":5,"breadcrumbs":6,"title":3},"113":{"body":22,"breadcrumbs":4,"title":1},"114":{"body":80,"breadcrumbs":4,"title":1},"115":{"body":287,"breadcrumbs":5,"title":2},"116":{"body":154,"breadcrumbs":3,"title":0},"117":{"body":23,"breadcrumbs":3,"title":0},"118":{"body":4,"breadcrumbs":0,"title":0},"119":{"body":9,"breadcrumbs":3,"title":2},"12":{"body":50,"breadcrumbs":3,"title":2},"120":{"body":14,"breadcrumbs":1,"title":0},"121":{"body":24,"breadcrumbs":2,"title":1},"122":{"body":19,"breadcrumbs":3,"title":2},"123":{"body":103,"breadcrumbs":2,"title":1},"124":{"body":122,"breadcrumbs":1,"title":0},"125":{"body":20,"breadcrumbs":1,"title":0},"126":{"body":10,"breadcrumbs":1,"title":0},"127":{"body":0,"breadcrumbs":5,"title":3},"128":{"body":15,"breadcrumbs":2,"title":0},"129":{"body":39,"breadcrumbs":3,"title":1},"13":{"body":19,"breadcrumbs":1,"title":0},"130":{"body":112,"breadcrumbs":3,"title":1},"131":{"body":75,"breadcrumbs":2,"title":0},"132":{"body":15,"breadcrumbs":2,"title":0},"133":{"body":6,"breadcrumbs":2,"title":0},"134":{"body":13,"breadcrumbs":4,"title":2},"135":{"body":60,"breadcrumbs":2,"title":0},"136":{"body":19,"breadcrumbs":2,"title":0},"137":{"body":76,"breadcrumbs":2,"title":0},"138":{"body":10,"breadcrumbs":2,"title":0},"139":{"body":2,"breadcrumbs":2,"title":0},"14":{"body":11,"breadcrumbs":6,"title":3},"140":{"body":440,"breadcrumbs":2,"title":0},"141":{"body":45,"breadcrumbs":2,"title":0},"142":{"body":35,"breadcrumbs":2,"title":0},"143":{"body":5,"breadcrumbs":2,"title":0},"144":{"body":1,"breadcrumbs":3,"title":1},"145":{"body":5,"breadcrumbs":5,"title":3},"146":{"body":0,"breadcrumbs":2,"title":0},"147":{"body":12,"breadcrumbs":4,"title":2},"148":{"body":15,"breadcrumbs":3,"title":1},"149":{"body":6,"breadcrumbs":4,"title":2},"15":{"body":0,"breadcrumbs":4,"title":1},"150":{"body":3,"breadcrumbs":2,"title":0},"151":{"body":6,"breadcrumbs":4,"title":2},"152":{"body":26,"breadcrumbs":4,"title":2},"153":{"body":28,"breadcrumbs":4,"title":2},"154":{"body":5,"breadcrumbs":3,"title":1},"155":{"body":35,"breadcrumbs":2,"title":0},"156":{"body":166,"breadcrumbs":3,"title":1},"157":{"body":174,"breadcrumbs":3,"title":1},"158":{"body":3,"breadcrumbs":2,"title":0},"159":{"body":42,"breadcrumbs":3,"title":1},"16":{"body":28,"breadcrumbs":3,"title":0},"160":{"body":213,"breadcrumbs":3,"title":1},"161":{"body":2,"breadcrumbs":2,"title":0},"162":{"body":3,"breadcrumbs":3,"title":1},"163":{"body":13,"breadcrumbs":3,"title":1},"164":{"body":20,"breadcrumbs":3,"title":1},"165":{"body":37,"breadcrumbs":2,"title":0},"166":{"body":14,"breadcrumbs":3,"title":1},"167":{"body":14,"breadcrumbs":2,"title":0},"168":{"body":0,"breadcrumbs":3,"title":2},"169":{"body":21,"breadcrumbs":3,"title":2},"17":{"body":65,"breadcrumbs":5,"title":2},"170":{"body":1,"breadcrumbs":1,"title":0},"171":{"body":23,"breadcrumbs":2,"title":1},"172":{"body":51,"breadcrumbs":2,"title":1},"173":{"body":4,"breadcrumbs":2,"title":1},"174":{"body":9,"breadcrumbs":2,"title":1},"175":{"body":49,"breadcrumbs":1,"title":0},"176":{"body":2,"breadcrumbs":2,"title":1},"177":{"body":3,"breadcrumbs":1,"title":0},"178":{"body":4,"breadcrumbs":3,"title":2},"179":{"body":12,"breadcrumbs":1,"title":0},"18":{"body":152,"breadcrumbs":8,"title":5},"180":{"body":661,"breadcrumbs":2,"title":1},"181":{"body":145,"breadcrumbs":2,"title":1},"182":{"body":178,"breadcrumbs":2,"title":1},"183":{"body":10,"breadcrumbs":1,"title":0},"184":{"body":9,"breadcrumbs":3,"title":2},"185":{"body":4,"breadcrumbs":1,"title":0},"186":{"body":5,"breadcrumbs":1,"title":0},"187":{"body":8,"breadcrumbs":1,"title":0},"188":{"body":5,"breadcrumbs":1,"title":0},"189":{"body":0,"breadcrumbs":1,"title":0},"19":{"body":17,"breadcrumbs":4,"title":1},"190":{"body":22,"breadcrumbs":3,"title":2},"191":{"body":134,"breadcrumbs":4,"title":3},"192":{"body":44,"breadcrumbs":1,"title":0},"193":{"body":7,"breadcrumbs":1,"title":0},"194":{"body":4,"breadcrumbs":1,"title":0},"195":{"body":17,"breadcrumbs":4,"title":2},"196":{"body":2,"breadcrumbs":2,"title":0},"197":{"body":35,"breadcrumbs":2,"title":1},"198":{"body":2,"breadcrumbs":1,"title":0},"199":{"body":3,"breadcrumbs":5,"title":2},"2":{"body":4,"breadcrumbs":3,"title":2},"20":{"body":5,"breadcrumbs":4,"title":1},"200":{"body":28,"breadcrumbs":4,"title":1},"201":{"body":34,"breadcrumbs":3,"title":0},"202":{"body":4,"breadcrumbs":3,"title":0},"203":{"body":5,"breadcrumbs":5,"title":3},"204":{"body":0,"breadcrumbs":2,"title":0},"205":{"body":12,"breadcrumbs":4,"title":2},"206":{"body":15,"breadcrumbs":3,"title":1},"207":{"body":6,"breadcrumbs":4,"title":2},"208":{"body":3,"breadcrumbs":2,"title":0},"209":{"body":6,"breadcrumbs":4,"title":2},"21":{"body":43,"breadcrumbs":5,"title":2},"210":{"body":26,"breadcrumbs":4,"title":2},"211":{"body":28,"breadcrumbs":4,"title":2},"212":{"body":5,"breadcrumbs":3,"title":1},"213":{"body":35,"breadcrumbs":2,"title":0},"214":{"body":166,"breadcrumbs":3,"title":1},"215":{"body":174,"breadcrumbs":3,"title":1},"216":{"body":3,"breadcrumbs":2,"title":0},"217":{"body":42,"breadcrumbs":3,"title":1},"218":{"body":213,"breadcrumbs":3,"title":1},"219":{"body":2,"breadcrumbs":2,"title":0},"22":{"body":41,"breadcrumbs":3,"title":0},"220":{"body":3,"breadcrumbs":3,"title":1},"221":{"body":13,"breadcrumbs":3,"title":1},"222":{"body":20,"breadcrumbs":3,"title":1},"223":{"body":37,"breadcrumbs":2,"title":0},"224":{"body":14,"breadcrumbs":3,"title":1},"225":{"body":14,"breadcrumbs":2,"title":0},"226":{"body":0,"breadcrumbs":7,"title":2},"227":{"body":2,"breadcrumbs":6,"title":1},"228":{"body":48,"breadcrumbs":6,"title":1},"229":{"body":164,"breadcrumbs":5,"title":0},"23":{"body":14,"breadcrumbs":6,"title":4},"230":{"body":225,"breadcrumbs":5,"title":0},"231":{"body":0,"breadcrumbs":9,"title":4},"232":{"body":163,"breadcrumbs":6,"title":1},"233":{"body":81,"breadcrumbs":8,"title":3},"234":{"body":962,"breadcrumbs":6,"title":1},"235":{"body":145,"breadcrumbs":5,"title":0},"236":{"body":0,"breadcrumbs":5,"title":1},"237":{"body":100,"breadcrumbs":5,"title":1},"238":{"body":4,"breadcrumbs":4,"title":1},"239":{"body":238,"breadcrumbs":3,"title":0},"24":{"body":58,"breadcrumbs":3,"title":1},"240":{"body":2,"breadcrumbs":5,"title":2},"241":{"body":0,"breadcrumbs":5,"title":2},"242":{"body":34,"breadcrumbs":5,"title":2},"243":{"body":16,"breadcrumbs":5,"title":2},"244":{"body":34,"breadcrumbs":5,"title":2},"245":{"body":39,"breadcrumbs":5,"title":2},"246":{"body":23,"breadcrumbs":5,"title":2},"247":{"body":48,"breadcrumbs":4,"title":1},"248":{"body":36,"breadcrumbs":4,"title":1},"249":{"body":20,"breadcrumbs":5,"title":2},"25":{"body":221,"breadcrumbs":3,"title":1},"250":{"body":22,"breadcrumbs":5,"title":2},"251":{"body":31,"breadcrumbs":6,"title":3},"252":{"body":36,"breadcrumbs":5,"title":2},"253":{"body":0,"breadcrumbs":3,"title":0},"254":{"body":10,"breadcrumbs":5,"title":2},"255":{"body":13,"breadcrumbs":5,"title":2},"256":{"body":5,"breadcrumbs":5,"title":2},"257":{"body":9,"breadcrumbs":5,"title":2},"258":{"body":8,"breadcrumbs":5,"title":2},"259":{"body":17,"breadcrumbs":5,"title":2},"26":{"body":19,"breadcrumbs":2,"title":0},"260":{"body":22,"breadcrumbs":5,"title":2},"261":{"body":6,"breadcrumbs":5,"title":2},"262":{"body":3,"breadcrumbs":5,"title":2},"263":{"body":8,"breadcrumbs":5,"title":2},"264":{"body":11,"breadcrumbs":5,"title":2},"265":{"body":18,"breadcrumbs":5,"title":2},"266":{"body":0,"breadcrumbs":3,"title":0},"267":{"body":19,"breadcrumbs":5,"title":2},"268":{"body":0,"breadcrumbs":3,"title":0},"269":{"body":7,"breadcrumbs":5,"title":2},"27":{"body":10,"breadcrumbs":6,"title":4},"270":{"body":29,"breadcrumbs":5,"title":2},"271":{"body":13,"breadcrumbs":5,"title":2},"272":{"body":10,"breadcrumbs":5,"title":2},"273":{"body":16,"breadcrumbs":5,"title":2},"274":{"body":9,"breadcrumbs":5,"title":2},"275":{"body":5,"breadcrumbs":5,"title":2},"276":{"body":7,"breadcrumbs":5,"title":2},"277":{"body":8,"breadcrumbs":5,"title":2},"278":{"body":0,"breadcrumbs":4,"title":1},"279":{"body":17,"breadcrumbs":5,"title":2},"28":{"body":198,"breadcrumbs":3,"title":1},"280":{"body":15,"breadcrumbs":5,"title":2},"281":{"body":12,"breadcrumbs":5,"title":2},"282":{"body":15,"breadcrumbs":5,"title":2},"283":{"body":11,"breadcrumbs":5,"title":2},"284":{"body":16,"breadcrumbs":5,"title":2},"285":{"body":18,"breadcrumbs":5,"title":2},"286":{"body":13,"breadcrumbs":5,"title":2},"287":{"body":18,"breadcrumbs":5,"title":2},"288":{"body":13,"breadcrumbs":5,"title":2},"289":{"body":10,"breadcrumbs":5,"title":2},"29":{"body":24,"breadcrumbs":2,"title":0},"290":{"body":13,"breadcrumbs":5,"title":2},"291":{"body":8,"breadcrumbs":5,"title":2},"292":{"body":16,"breadcrumbs":5,"title":2},"293":{"body":15,"breadcrumbs":5,"title":2},"294":{"body":20,"breadcrumbs":5,"title":2},"295":{"body":20,"breadcrumbs":5,"title":2},"296":{"body":27,"breadcrumbs":5,"title":2},"297":{"body":4,"breadcrumbs":5,"title":2},"298":{"body":5,"breadcrumbs":5,"title":2},"299":{"body":3,"breadcrumbs":5,"title":2},"3":{"body":40,"breadcrumbs":3,"title":2},"30":{"body":10,"breadcrumbs":4,"title":3},"300":{"body":4,"breadcrumbs":5,"title":2},"301":{"body":4,"breadcrumbs":5,"title":2},"302":{"body":13,"breadcrumbs":5,"title":2},"303":{"body":12,"breadcrumbs":5,"title":2},"304":{"body":4,"breadcrumbs":5,"title":2},"305":{"body":39,"breadcrumbs":5,"title":2},"306":{"body":52,"breadcrumbs":5,"title":2},"307":{"body":6,"breadcrumbs":5,"title":2},"308":{"body":3,"breadcrumbs":5,"title":2},"309":{"body":4,"breadcrumbs":5,"title":2},"31":{"body":154,"breadcrumbs":2,"title":1},"310":{"body":12,"breadcrumbs":5,"title":2},"311":{"body":9,"breadcrumbs":5,"title":2},"312":{"body":65,"breadcrumbs":5,"title":2},"313":{"body":97,"breadcrumbs":3,"title":0},"314":{"body":0,"breadcrumbs":4,"title":1},"315":{"body":0,"breadcrumbs":5,"title":2},"316":{"body":0,"breadcrumbs":3,"title":0},"317":{"body":74,"breadcrumbs":5,"title":2},"318":{"body":0,"breadcrumbs":3,"title":0},"319":{"body":6,"breadcrumbs":5,"title":2},"32":{"body":106,"breadcrumbs":3,"title":2},"320":{"body":7,"breadcrumbs":5,"title":2},"321":{"body":45,"breadcrumbs":5,"title":2},"322":{"body":26,"breadcrumbs":5,"title":2},"323":{"body":15,"breadcrumbs":5,"title":2},"324":{"body":13,"breadcrumbs":5,"title":2},"325":{"body":4,"breadcrumbs":5,"title":2},"326":{"body":28,"breadcrumbs":5,"title":2},"327":{"body":45,"breadcrumbs":5,"title":2},"328":{"body":31,"breadcrumbs":5,"title":2},"329":{"body":7,"breadcrumbs":5,"title":2},"33":{"body":28,"breadcrumbs":1,"title":0},"330":{"body":7,"breadcrumbs":5,"title":2},"331":{"body":0,"breadcrumbs":3,"title":0},"332":{"body":14,"breadcrumbs":5,"title":2},"333":{"body":20,"breadcrumbs":5,"title":2},"334":{"body":7,"breadcrumbs":4,"title":1},"335":{"body":13,"breadcrumbs":5,"title":2},"336":{"body":9,"breadcrumbs":5,"title":2},"337":{"body":10,"breadcrumbs":5,"title":2},"338":{"body":0,"breadcrumbs":5,"title":2},"339":{"body":4,"breadcrumbs":5,"title":2},"34":{"body":10,"breadcrumbs":8,"title":5},"340":{"body":81,"breadcrumbs":5,"title":2},"341":{"body":1,"breadcrumbs":5,"title":2},"342":{"body":14,"breadcrumbs":5,"title":2},"343":{"body":24,"breadcrumbs":5,"title":2},"344":{"body":24,"breadcrumbs":5,"title":2},"345":{"body":2,"breadcrumbs":5,"title":2},"346":{"body":62,"breadcrumbs":5,"title":2},"347":{"body":59,"breadcrumbs":9,"title":6},"348":{"body":56,"breadcrumbs":5,"title":2},"349":{"body":3,"breadcrumbs":5,"title":2},"35":{"body":0,"breadcrumbs":4,"title":1},"350":{"body":3,"breadcrumbs":5,"title":2},"351":{"body":3,"breadcrumbs":5,"title":2},"352":{"body":0,"breadcrumbs":3,"title":0},"353":{"body":6,"breadcrumbs":5,"title":2},"354":{"body":6,"breadcrumbs":5,"title":2},"355":{"body":9,"breadcrumbs":5,"title":2},"356":{"body":10,"breadcrumbs":5,"title":2},"357":{"body":9,"breadcrumbs":5,"title":2},"358":{"body":5,"breadcrumbs":4,"title":1},"359":{"body":71,"breadcrumbs":7,"title":4},"36":{"body":204,"breadcrumbs":6,"title":3},"360":{"body":25,"breadcrumbs":5,"title":2},"361":{"body":0,"breadcrumbs":3,"title":0},"362":{"body":4,"breadcrumbs":4,"title":1},"363":{"body":11,"breadcrumbs":4,"title":1},"364":{"body":0,"breadcrumbs":2,"title":0},"365":{"body":106,"breadcrumbs":3,"title":1},"366":{"body":75,"breadcrumbs":2,"title":0},"367":{"body":5,"breadcrumbs":3,"title":1},"368":{"body":0,"breadcrumbs":2,"title":0},"369":{"body":30,"breadcrumbs":4,"title":2},"37":{"body":23,"breadcrumbs":3,"title":0},"370":{"body":530,"breadcrumbs":3,"title":1},"371":{"body":278,"breadcrumbs":3,"title":1},"372":{"body":0,"breadcrumbs":2,"title":0},"373":{"body":6,"breadcrumbs":6,"title":2},"374":{"body":1,"breadcrumbs":4,"title":0},"375":{"body":55,"breadcrumbs":5,"title":1},"376":{"body":11,"breadcrumbs":5,"title":1},"377":{"body":63,"breadcrumbs":5,"title":1},"378":{"body":87,"breadcrumbs":7,"title":3},"379":{"body":0,"breadcrumbs":6,"title":2},"38":{"body":10,"breadcrumbs":5,"title":3},"380":{"body":98,"breadcrumbs":6,"title":2},"381":{"body":125,"breadcrumbs":7,"title":3},"382":{"body":1,"breadcrumbs":5,"title":1},"383":{"body":92,"breadcrumbs":5,"title":1},"384":{"body":104,"breadcrumbs":10,"title":6},"385":{"body":158,"breadcrumbs":7,"title":3},"386":{"body":3,"breadcrumbs":6,"title":2},"387":{"body":126,"breadcrumbs":6,"title":2},"388":{"body":112,"breadcrumbs":5,"title":1},"389":{"body":80,"breadcrumbs":6,"title":2},"39":{"body":256,"breadcrumbs":3,"title":1},"390":{"body":10,"breadcrumbs":5,"title":1},"391":{"body":1,"breadcrumbs":4,"title":0},"4":{"body":1,"breadcrumbs":3,"title":2},"40":{"body":30,"breadcrumbs":2,"title":0},"41":{"body":16,"breadcrumbs":7,"title":4},"42":{"body":4,"breadcrumbs":5,"title":2},"43":{"body":187,"breadcrumbs":4,"title":1},"44":{"body":28,"breadcrumbs":3,"title":0},"45":{"body":9,"breadcrumbs":8,"title":5},"46":{"body":8,"breadcrumbs":5,"title":2},"47":{"body":14,"breadcrumbs":8,"title":5},"48":{"body":195,"breadcrumbs":4,"title":1},"49":{"body":99,"breadcrumbs":5,"title":2},"5":{"body":27,"breadcrumbs":4,"title":3},"50":{"body":19,"breadcrumbs":3,"title":0},"51":{"body":12,"breadcrumbs":4,"title":1},"52":{"body":85,"breadcrumbs":4,"title":1},"53":{"body":0,"breadcrumbs":4,"title":1},"54":{"body":495,"breadcrumbs":4,"title":1},"55":{"body":26,"breadcrumbs":4,"title":1},"56":{"body":225,"breadcrumbs":3,"title":0},"57":{"body":17,"breadcrumbs":3,"title":0},"58":{"body":17,"breadcrumbs":6,"title":4},"59":{"body":2,"breadcrumbs":4,"title":2},"6":{"body":31,"breadcrumbs":4,"title":3},"60":{"body":25,"breadcrumbs":2,"title":0},"61":{"body":348,"breadcrumbs":3,"title":1},"62":{"body":27,"breadcrumbs":2,"title":0},"63":{"body":21,"breadcrumbs":2,"title":0},"64":{"body":9,"breadcrumbs":8,"title":5},"65":{"body":35,"breadcrumbs":4,"title":1},"66":{"body":19,"breadcrumbs":4,"title":1},"67":{"body":6,"breadcrumbs":4,"title":1},"68":{"body":565,"breadcrumbs":5,"title":2},"69":{"body":574,"breadcrumbs":4,"title":1},"7":{"body":11,"breadcrumbs":3,"title":2},"70":{"body":24,"breadcrumbs":3,"title":0},"71":{"body":77,"breadcrumbs":3,"title":0},"72":{"body":8,"breadcrumbs":3,"title":0},"73":{"body":11,"breadcrumbs":6,"title":3},"74":{"body":13,"breadcrumbs":3,"title":0},"75":{"body":4,"breadcrumbs":4,"title":1},"76":{"body":21,"breadcrumbs":4,"title":1},"77":{"body":51,"breadcrumbs":5,"title":2},"78":{"body":322,"breadcrumbs":4,"title":1},"79":{"body":385,"breadcrumbs":3,"title":0},"8":{"body":9,"breadcrumbs":2,"title":1},"80":{"body":169,"breadcrumbs":3,"title":0},"81":{"body":53,"breadcrumbs":3,"title":0},"82":{"body":22,"breadcrumbs":3,"title":0},"83":{"body":12,"breadcrumbs":9,"title":4},"84":{"body":80,"breadcrumbs":7,"title":2},"85":{"body":189,"breadcrumbs":6,"title":1},"86":{"body":10,"breadcrumbs":7,"title":2},"87":{"body":12,"breadcrumbs":6,"title":1},"88":{"body":131,"breadcrumbs":5,"title":0},"89":{"body":179,"breadcrumbs":6,"title":1},"9":{"body":6,"breadcrumbs":2,"title":1},"90":{"body":342,"breadcrumbs":5,"title":0},"91":{"body":17,"breadcrumbs":5,"title":0},"92":{"body":10,"breadcrumbs":7,"title":4},"93":{"body":5,"breadcrumbs":4,"title":1},"94":{"body":165,"breadcrumbs":4,"title":1},"95":{"body":10,"breadcrumbs":5,"title":2},"96":{"body":3,"breadcrumbs":4,"title":1},"97":{"body":182,"breadcrumbs":3,"title":0},"98":{"body":200,"breadcrumbs":3,"title":0},"99":{"body":24,"breadcrumbs":3,"title":0}},"docs":{"0":{"body":"","breadcrumbs":"https://github.com/eunomia-bpf/bpf-developer-tutorial » https://github.com/eunomia-bpf/bpf-developer-tutorial","id":"0","title":"https://github.com/eunomia-bpf/bpf-developer-tutorial"},"1":{"body":"","breadcrumbs":"介绍 eBPF 的基本概念、常见的开发工具 » eBPF 入门开发实践教程零:介绍 eBPF 的基本概念、常见的开发工具","id":"1","title":"eBPF 入门开发实践教程零:介绍 eBPF 的基本概念、常见的开发工具"},"10":{"body":"eBPF Go库提供了一个通用的eBPF库,它解耦了获取 eBPF 字节码的过程和 eBPF 程序的加载和管理,并实现了类似 libbpf 一样的 CO- 功能。eBPF程序通常是通过编写高级语言创建的,然后使用clang/LLVM编译器编译为eBPF字节码。","breadcrumbs":"介绍 eBPF 的基本概念、常见的开发工具 » eBPF Go library","id":"10","title":"eBPF Go library"},"100":{"body":"在对应的目录中,运行 Make 即可编译运行上述代码: $ make\n$ sudo ./javagc -p 12345\nTracing javagc time... Hit Ctrl-C to end.\nTIME CPU PID GC TIME\n10:00:01 10% 12345 50ms\n10:00:02 12% 12345 55ms\n10:00:03 9% 12345 47ms\n10:00:04 13% 12345 52ms\n10:00:05 11% 12345 50ms 完整源代码: https://github.com/eunomia-bpf/bpf-developer-tutorial/tree/main/src/15-javagc 参考资料: https://www.brendangregg.com/blog/2015-07-03/hacking-linux-usdt-ftrace.html https://github.com/iovisor/bcc/blob/master/libbpf-tools/javagc.c","breadcrumbs":"使用 USDT 捕获用户态 Java GC 事件耗时 » 编译运行","id":"100","title":"编译运行"},"101":{"body":"通过本篇 eBPF 入门实践教程,我们学习了如何使用 eBPF 和 USDT 动态跟踪和分析 Java 的垃圾回收(GC)事件。我们了解了如何在用户态应用程序中设置 USDT 跟踪点,以及如何编写 eBPF 程序来捕获这些跟踪点的信息,从而更深入地理解和优化 Java GC 的行为和性能。 此外,我们也介绍了一些关于 Java GC、USDT 和 eBPF 的基础知识和实践技巧,这些知识和技巧对于想要在网络和系统性能分析领域深入研究的开发者来说是非常有价值的。 如果您希望学习更多关于 eBPF 的知识和实践,可以访问我们的教程代码仓库 https://github.com/eunomia-bpf/bpf-developer-tutorial 或网站 https://eunomia.dev/zh/tutorials/ 以获取更多示例和完整的教程。","breadcrumbs":"使用 USDT 捕获用户态 Java GC 事件耗时 » 总结","id":"101","title":"总结"},"102":{"body":"eBPF(扩展的伯克利数据包过滤器)是一项强大的网络和性能分析工具,被广泛应用在 Linux 内核上。eBPF 使得开发者能够动态地加载、更新和运行用户定义的代码,而无需重启内核或更改内核源代码。 在本篇教程中,我们将探讨如何使用 eBPF 编写 Memleak 程序,以监控程序的内存泄漏。","breadcrumbs":"编写 eBPF 程序 Memleak 监控内存泄漏 » eBPF 入门实践教程十六:编写 eBPF 程序 Memleak 监控内存泄漏","id":"102","title":"eBPF 入门实践教程十六:编写 eBPF 程序 Memleak 监控内存泄漏"},"103":{"body":"内存泄漏是计算机编程中的一种常见问题,其严重程度不应被低估。内存泄漏发生时,程序会逐渐消耗更多的内存资源,但并未正确释放。随着时间的推移,这种行为会导致系统内存逐渐耗尽,从而显著降低程序及系统的整体性能。 内存泄漏有多种可能的原因。这可能是由于配置错误导致的,例如程序错误地配置了某些资源的动态分配。它也可能是由于软件缺陷或错误的内存管理策略导致的,如在程序执行过程中忘记释放不再需要的内存。此外,如果一个应用程序的内存使用量过大,那么系统性能可能会因页面交换(swapping)而大幅下降,甚至可能导致应用程序被系统强制终止(Linux 的 OOM killer)。","breadcrumbs":"编写 eBPF 程序 Memleak 监控内存泄漏 » 背景及其重要性","id":"103","title":"背景及其重要性"},"104":{"body":"调试内存泄漏问题是一项复杂且挑战性的任务。这涉及到详细检查应用程序的配置、内存分配和释放情况,通常需要应用专门的工具来帮助诊断。例如,有一些工具可以在应用程序启动时将 malloc() 函数调用与特定的检测工具关联起来,如 Valgrind memcheck,这类工具可以模拟 CPU 来检查所有内存访问,但可能会导致应用程序运行速度大大减慢。另一个选择是使用堆分析器,如 libtcmalloc,它相对较快,但仍可能使应用程序运行速度降低五倍以上。此外,还有一些工具,如 gdb,可以获取应用程序的核心转储并进行后处理以分析内存使用情况。然而,这些工具通常在获取核心转储时需要暂停应用程序,或在应用程序终止后才能调用 free() 函数。","breadcrumbs":"编写 eBPF 程序 Memleak 监控内存泄漏 » 调试内存泄漏的挑战","id":"104","title":"调试内存泄漏的挑战"},"105":{"body":"在这种背景下,eBPF 的作用就显得尤为重要。eBPF 提供了一种高效的机制来监控和追踪系统级别的事件,包括内存的分配和释放。通过 eBPF,我们可以跟踪内存分配和释放的请求,并收集每次分配的调用堆栈。然后,我们可以分 析这些信息,找出执行了内存分配但未执行释放操作的调用堆栈,这有助于我们找出导致内存泄漏的源头。这种方式的优点在于,它可以实时地在运行的应用程序中进行,而无需暂停应用程序或进行复杂的前后处理。 memleak eBPF 工具可以跟踪并匹配内存分配和释放的请求,并收集每次分配的调用堆栈。随后,memleak 可以打印一个总结,表明哪些调用堆栈执行了分配,但是并没有随后进行释放。例如,我们运行命令: # ./memleak -p $(pidof allocs)\nAttaching to pid 5193, Ctrl+C to quit.\n[11:16:33] Top 2 stacks with outstanding allocations: 80 bytes in 5 allocations from stack main+0x6d [allocs] __libc_start_main+0xf0 [libc-2.21.so] [11:16:34] Top 2 stacks with outstanding allocations: 160 bytes in 10 allocations from stack main+0x6d [allocs] __libc_start_main+0xf0 [libc-2.21.so] 运行这个命令后,我们可以看到分配但未释放的内存来自于哪些堆栈,并且可以看到这些未释放的内存的大小和数量。 随着时间的推移,很显然,allocs 进程的 main 函数正在泄漏内存,每次泄漏 16 字节。幸运的是,我们不需要检查每个分配,我们得到了一个很好的总结,告诉我们哪个堆栈负责大量的泄漏。","breadcrumbs":"编写 eBPF 程序 Memleak 监控内存泄漏 » eBPF 的作用","id":"105","title":"eBPF 的作用"},"106":{"body":"在基本层面上,memleak 的工作方式类似于在内存分配和释放路径上安装监控设备。它通过在内存分配和释放函数中插入 eBPF 程序来达到这个目标。这意味着,当这些函数被调用时,memleak 就会记录一些重要信息,如调用者的进程 ID(PID)、分配的内存地址以及分配的内存大小等。当释放内存的函数被调用时,memleak 则会在其内部的映射表(map)中删除相应的内存分配记录。这种机制使得 memleak 能够准确地追踪到哪些内存块已被分配但未被释放。 对于用户态的常用内存分配函数,如 malloc 和 calloc 等,memleak 利用了用户态探测(uprobe)技术来实现监控。uprobe 是一种用于用户空间应用程序的动态追踪技术,它可以在运行时不修改二进制文件的情况下在任意位置设置断点,从而实现对特定函数调用的追踪。 对于内核态的内存分配函数,如 kmalloc 等,memleak 则选择使用了 tracepoint 来实现监控。Tracepoint 是一种在 Linux 内核中提供的动态追踪技术,它可以在内核运行时动态地追踪特定的事件,而无需重新编译内核或加载内核模块。","breadcrumbs":"编写 eBPF 程序 Memleak 监控内存泄漏 » memleak 的实现原理","id":"106","title":"memleak 的实现原理"},"107":{"body":"","breadcrumbs":"编写 eBPF 程序 Memleak 监控内存泄漏 » 内核态 eBPF 程序实现","id":"107","title":"内核态 eBPF 程序实现"},"108":{"body":"memleak 的内核态 eBPF 程序包含一些用于跟踪内存分配和释放的关键函数。在我们深入了解这些函数之前,让我们首先观察 memleak 所定义的一些数据结构,这些结构在其内核态和用户态程序中均有使用。 #ifndef __MEMLEAK_H\n#define __MEMLEAK_H #define ALLOCS_MAX_ENTRIES 1000000\n#define COMBINED_ALLOCS_MAX_ENTRIES 10240 struct alloc_info { __u64 size; // 分配的内存大小 __u64 timestamp_ns; // 分配时的时间戳,单位为纳秒 int stack_id; // 分配时的调用堆栈ID\n}; union combined_alloc_info { struct { __u64 total_size : 40; // 所有未释放分配的总大小 __u64 number_of_allocs : 24; // 所有未释放分配的总次数 }; __u64 bits; // 结构的位图表示\n}; #endif /* __MEMLEAK_H */ 这里定义了两个主要的数据结构:alloc_info 和 combined_alloc_info。 alloc_info 结构体包含了一个内存分配的基本信息,包括分配的内存大小 size、分配发生时的时间戳 timestamp_ns,以及触发分配的调用堆栈 ID stack_id。 combined_alloc_info 是一个联合体(union),它包含一个嵌入的结构体和一个 __u64 类型的位图表示 bits。嵌入的结构体有两个成员:total_size 和 number_of_allocs,分别代表所有未释放分配的总大小和总次数。其中 40 和 24 分别表示 total_size 和 number_of_allocs这两个成员变量所占用的位数,用来限制其大小。通过这样的位数限制,可以节省combined_alloc_info结构的存储空间。同时,由于total_size和number_of_allocs在存储时是共用一个unsigned long long类型的变量bits,因此可以通过在成员变量bits上进行位运算来访问和修改total_size和number_of_allocs,从而避免了在程序中定义额外的变量和函数的复杂性。 接下来,memleak 定义了一系列用于保存内存分配信息和分析结果的 eBPF 映射(maps)。这些映射都以 SEC(\".maps\") 的形式定义,表示它们属于 eBPF 程序的映射部分。 const volatile size_t min_size = 0;\nconst volatile size_t max_size = -1;\nconst volatile size_t page_size = 4096;\nconst volatile __u64 sample_rate = 1;\nconst volatile bool trace_all = false;\nconst volatile __u64 stack_flags = 0;\nconst volatile bool wa_missing_free = false; struct { __uint(type, BPF_MAP_TYPE_HASH); __type(key, pid_t); __type(value, u64); __uint(max_entries, 10240);\n} sizes SEC(\".maps\"); struct { __uint(type, BPF_MAP_TYPE_HASH); __type(key, u64); /* address */ __type(value, struct alloc_info); __uint(max_entries, ALLOCS_MAX_ENTRIES);\n} allocs SEC(\".maps\"); struct { __uint(type, BPF_MAP_TYPE_HASH); __type(key, u64); /* stack id */ __type(value, union combined_alloc_info); __uint(max_entries, COMBINED_ALLOCS_MAX_ENTRIES);\n} combined_allocs SEC(\".maps\"); struct { __uint(type, BPF_MAP_TYPE_HASH); __type(key, u64); __type(value, u64); __uint(max_entries, 10240);\n} memptrs SEC(\".maps\"); struct { __uint(type, BPF_MAP_TYPE_STACK_TRACE); __type(key, u32);\n} stack_traces SEC(\".maps\"); static union combined_alloc_info initial_cinfo; 这段代码首先定义了一些可配置的参数,如 min_size, max_size, page_size, sample_rate, trace_all, stack_flags 和 wa_missing_free,分别表示最小分配大小、最大分配大小、页面大小、采样率、是否追踪所有分配、堆栈标志和是否工作在缺失释放(missing free)模式。 接着定义了五个映射: sizes:这是一个哈希类型的映射,键为进程 ID,值为 u64 类型,存储每个进程的分配大小。 allocs:这也是一个哈希类型的映射,键为分配的地址,值为 alloc_info 结构体,存储每个内存分配的详细信息。 combined_allocs:这是另一个哈希类型的映射,键为堆栈 ID,值为 combined_alloc_info 联合体,存储所有未释放分配的总大小和总次数。 memptrs:这也是一个哈希类型的映射,键和值都为 u64 类型,用于在用户空间和内核空间之间传递内存指针。 stack_traces:这是一个堆栈追踪类型的映射,键为 u32 类型,用于存储堆栈 ID。 以用户态的内存分配追踪部分为例,主要是挂钩内存相关的函数调用,如 malloc, free, calloc, realloc, mmap 和 munmap,以便在调用这些函数时进行数据记录。在用户态,memleak 主要使用了 uprobes 技术进行挂载。 每个函数调用被分为 \"enter\" 和 \"exit\" 两部分。\"enter\" 部分记录的是函数调用的参数,如分配的大小或者释放的地址。\"exit\" 部分则主要用于获取函数的返回值,如分配得到的内存地址。 这里,gen_alloc_enter, gen_alloc_exit, gen_free_enter 是实现记录行为的函数,他们分别用于记录分配开始、分配结束和释放开始的相关信息。 函数原型示例如下: SEC(\"uprobe\")\nint BPF_KPROBE(malloc_enter, size_t size)\n{ // 记录分配开始的相关信息 return gen_alloc_enter(size);\n} SEC(\"uretprobe\")\nint BPF_KRETPROBE(malloc_exit)\n{ // 记录分配结束的相关信息 return gen_alloc_exit(ctx);\n} SEC(\"uprobe\")\nint BPF_KPROBE(free_enter, void *address)\n{ // 记录释放开始的相关信息 return gen_free_enter(address);\n} 其中,malloc_enter 和 free_enter 是分别挂载在 malloc 和 free 函数入口处的探针(probes),用于在函数调用时进行数据记录。而 malloc_exit 则是挂载在 malloc 函数的返回处的探针,用于记录函数的返回值。 这些函数使用了 BPF_KPROBE 和 BPF_KRETPROBE 这两个宏来声明,这两个宏分别用于声明 kprobe(内核探针)和 kretprobe(内核返回探针)。具体来说,kprobe 用于在函数调用时触发,而 kretprobe 则是在函数返回时触发。 gen_alloc_enter 函数是在内存分配请求的开始时被调用的。这个函数主要负责在调用分配内存的函数时收集一些基本的信息。下面我们将深入探讨这个函数的实现。 static int gen_alloc_enter(size_t size)\n{ if (size < min_size || size > max_size) return 0; if (sample_rate > 1) { if (bpf_ktime_get_ns() % sample_rate != 0) return 0; } const pid_t pid = bpf_get_current_pid_tgid() >> 32; bpf_map_update_elem(&sizes, &pid, &size, BPF_ANY); if (trace_all) bpf_printk(\"alloc entered, size = %lu\\n\", size); return 0;\n} SEC(\"uprobe\")\nint BPF_KPROBE(malloc_enter, size_t size)\n{ return gen_alloc_enter(size);\n} 首先,gen_alloc_enter 函数接收一个 size 参数,这个参数表示请求分配的内存的大小。如果这个值不在 min_size 和 max_size 之间,函数将直接返回,不再进行后续的操作。这样可以使工具专注于追踪特定范围的内存分配请求,过滤掉不感兴趣的分配请求。 接下来,函数检查采样率 sample_rate。如果 sample_rate 大于1,意味着我们不需要追踪所有的内存分配请求,而是周期性地追踪。这里使用 bpf_ktime_get_ns 获取当前的时间戳,然后通过取模运算来决定是否需要追踪当前的内存分配请求。这是一种常见的采样技术,用于降低性能开销,同时还能够提供一个代表性的样本用于分析。 之后,函数使用 bpf_get_current_pid_tgid 函数获取当前进程的 PID。注意这里的 PID 实际上是进程和线程的组合 ID,我们通过右移 32 位来获取真正的进程 ID。 函数接下来更新 sizes 这个 map,这个 map 以进程 ID 为键,以请求的内存分配大小为值。BPF_ANY 表示如果 key 已存在,那么更新 value,否则就新建一个条目。 最后,如果启用了 trace_all 标志,函数将打印一条信息,说明发生了内存分配。 BPF_KPROBE 宏用于 最后定义了 BPF_KPROBE(malloc_enter, size_t size),它会在 malloc 函数被调用时被 BPF uprobe 拦截执行,并通过 gen_alloc_enter 来记录内存分配大小。 我们刚刚分析了内存分配的入口函数 gen_alloc_enter,现在我们来关注这个过程的退出部分。具体来说,我们将讨论 gen_alloc_exit2 函数以及如何从内存分配调用中获取返回的内存地址。 static int gen_alloc_exit2(void *ctx, u64 address)\n{ const pid_t pid = bpf_get_current_pid_tgid() >> 32; struct alloc_info info; const u64* size = bpf_map_lookup_elem(&sizes, &pid); if (!size) return 0; // missed alloc entry __builtin_memset(&info, 0, sizeof(info)); info.size = *size; bpf_map_delete_elem(&sizes, &pid); if (address != 0) { info.timestamp_ns = bpf_ktime_get_ns(); info.stack_id = bpf_get_stackid(ctx, &stack_traces, stack_flags); bpf_map_update_elem(&allocs, &address, &info, BPF_ANY); update_statistics_add(info.stack_id, info.size); } if (trace_all) { bpf_printk(\"alloc exited, size = %lu, result = %lx\\n\", info.size, address); } return 0;\n}\nstatic int gen_alloc_exit(struct pt_regs *ctx)\n{ return gen_alloc_exit2(ctx, PT_REGS_RC(ctx));\n} SEC(\"uretprobe\")\nint BPF_KRETPROBE(malloc_exit)\n{ return gen_alloc_exit(ctx);\n} gen_alloc_exit2 函数在内存分配操作完成时被调用,这个函数接收两个参数,一个是上下文 ctx,另一个是内存分配函数返回的内存地址 address。 首先,它获取当前线程的 PID,然后使用这个 PID 作为键在 sizes 这个 map 中查找对应的内存分配大小。如果没有找到(也就是说,没有对应的内存分配操作的入口),函数就会直接返回。 接着,函数清除 info 结构体的内容,并设置它的 size 字段为之前在 map 中找到的内存分配大小。并从 sizes 这个 map 中删除相应的元素,因为此时内存分配操作已经完成,不再需要这个信息。 接下来,如果 address 不为 0(也就是说,内存分配操作成功了),函数就会进一步收集一些额外的信息。首先,它获取当前的时间戳作为内存分配完成的时间,并获取当前的堆栈跟踪。这些信息都会被储存在 info 结构体中,并随后更新到 allocs 这个 map 中。 最后,函数调用 update_statistics_add 更新统计数据,如果启用了所有内存分配操作的跟踪,函数还会打印一些关于内存分配操作的信息。 请注意,gen_alloc_exit 函数是 gen_alloc_exit2 的一个包装,它将 PT_REGS_RC(ctx) 作为 address 参数传递给 gen_alloc_exit2。在我们的讨论中,我们刚刚提到在gen_alloc_exit2函数中,调用了update_statistics_add` 函数以更新内存分配的统计数据。下面我们详细看一下这个函数的具体实现。 static void update_statistics_add(u64 stack_id, u64 sz)\n{ union combined_alloc_info *existing_cinfo; existing_cinfo = bpf_map_lookup_or_try_init(&combined_allocs, &stack_id, &initial_cinfo); if (!existing_cinfo) return; const union combined_alloc_info incremental_cinfo = { .total_size = sz, .number_of_allocs = 1 }; __sync_fetch_and_add(&existing_cinfo->bits, incremental_cinfo.bits);\n} update_statistics_add 函数接收两个参数:当前的堆栈 ID stack_id 以及内存分配的大小 sz。这两个参数都在内存分配事件中收集到,并且用于更新内存分配的统计数据。 首先,函数尝试在 combined_allocs 这个 map 中查找键值为当前堆栈 ID 的元素,如果找不到,就用 initial_cinfo(这是一个默认的 combined_alloc_info 结构体,所有字段都为零)来初始化新的元素。 接着,函数创建一个 incremental_cinfo,并设置它的 total_size 为当前内存分配的大小,设置 number_of_allocs 为 1。这是因为每次调用 update_statistics_add 函数都表示有一个新的内存分配事件发生,而这个事件的内存分配大小就是 sz。 最后,函数使用 __sync_fetch_and_add 函数原子地将 incremental_cinfo 的值加到 existing_cinfo 中。请注意这个步骤是线程安全的,即使有多个线程并发地调用 update_statistics_add 函数,每个内存分配事件也能正确地记录到统计数据中。 总的来说,update_statistics_add 函数实现了内存分配统计的更新逻辑,通过维护每个堆栈 ID 的内存分配总量和次数,我们可以深入了解到程序的内存分配行为。 在我们对内存分配的统计跟踪过程中,我们不仅要统计内存的分配,还要考虑内存的释放。在上述代码中,我们定义了一个名为 update_statistics_del 的函数,其作用是在内存释放时更新统计信息。而 gen_free_enter 函数则是在进程调用 free 函数时被执行。 static void update_statistics_del(u64 stack_id, u64 sz)\n{ union combined_alloc_info *existing_cinfo; existing_cinfo = bpf_map_lookup_elem(&combined_allocs, &stack_id); if (!existing_cinfo) { bpf_printk(\"failed to lookup combined allocs\\n\"); return; } const union combined_alloc_info decremental_cinfo = { .total_size = sz, .number_of_allocs = 1 }; __sync_fetch_and_sub(&existing_cinfo->bits, decremental_cinfo.bits);\n} update_statistics_del 函数的参数为堆栈 ID 和要释放的内存块大小。函数首先在 combined_allocs 这个 map 中使用当前的堆栈 ID 作为键来查找相应的 combined_alloc_info 结构体。如果找不到,就输出错误信息,然后函数返回。如果找到了,就会构造一个名为 decremental_cinfo 的 combined_alloc_info 结构体,设置它的 total_size 为要释放的内存大小,设置 number_of_allocs 为 1。然后使用 __sync_fetch_and_sub 函数原子地从 existing_cinfo 中减去 decremental_cinfo 的值。请注意,这里的 number_of_allocs 是负数,表示减少了一个内存分配。 static int gen_free_enter(const void *address)\n{ const u64 addr = (u64)address; const struct alloc_info *info = bpf_map_lookup_elem(&allocs, &addr); if (!info) return 0; bpf_map_delete_elem(&allocs, &addr); update_statistics_del(info->stack_id, info->size); if (trace_all) { bpf_printk(\"free entered, address = %lx, size = %lu\\n\", address, info->size); } return 0;\n} SEC(\"uprobe\")\nint BPF_KPROBE(free_enter, void *address)\n{ return gen_free_enter(address);\n} 接下来看 gen_free_enter 函数。它接收一个地址作为参数,这个地址是内存分配的结果,也就是将要释放的内存的起始地址。函数首先在 allocs 这个 map 中使用这个地址作为键来查找对应的 alloc_info 结构体。如果找不到,那么就直接返回,因为这意味着这个地址并没有被分配过。如果找到了,那么就删除这个元素,并且调用 update_statistics_del 函数来更新统计数据。最后,如果启用了全局追踪,那么还会输出一条信息,包括这个地址以及它的大小。 在我们追踪和统计内存分配的同时,我们也需要对内核态的内存分配和释放进行追踪。在Linux内核中,kmem_cache_alloc函数和kfree函数分别用于内核态的内存分配和释放。 SEC(\"tracepoint/kmem/kfree\")\nint memleak__kfree(void *ctx)\n{ const void *ptr; if (has_kfree()) { struct trace_event_raw_kfree___x *args = ctx; ptr = BPF_CORE_READ(args, ptr); } else { struct trace_event_raw_kmem_free___x *args = ctx; ptr = BPF_CORE_READ(args, ptr); } return gen_free_enter(ptr);\n} 上述代码片段定义了一个函数memleak__kfree,这是一个bpf程序,会在内核调用kfree函数时执行。首先,该函数检查是否存在kfree函数。如果存在,则会读取传递给kfree函数的参数(即要释放的内存块的地址),并保存到变量ptr中;否则,会读取传递给kmem_free函数的参数(即要释放的内存块的地址),并保存到变量ptr中。接着,该函数会调用之前定义的gen_free_enter函数来处理该内存块的释放。 SEC(\"tracepoint/kmem/kmem_cache_alloc\")\nint memleak__kmem_cache_alloc(struct trace_event_raw_kmem_alloc *ctx)\n{ if (wa_missing_free) gen_free_enter(ctx->ptr); gen_alloc_enter(ctx->bytes_alloc); return gen_alloc_exit2(ctx, (u64)(ctx->ptr));\n} 这段代码定义了一个函数 memleak__kmem_cache_alloc,这也是一个bpf程序,会在内核调用 kmem_cache_alloc 函数时执行。如果标记 wa_missing_free 被设置,则调用 gen_free_enter 函数处理可能遗漏的释放操作。然后,该函数会调用 gen_alloc_enter 函数来处理内存分配,最后调用gen_alloc_exit2函数记录分配的结果。 这两个 bpf 程序都使用了 SEC 宏定义了对应的 tracepoint,以便在相应的内核函数被调用时得到执行。在Linux内核中,tracepoint 是一种可以在内核中插入的静态钩子,可以用来收集运行时的内核信息,它在调试和性能分析中非常有用。 在理解这些代码的过程中,要注意 BPF_CORE_READ 宏的使用。这个宏用于在 bpf 程序中读取内核数据。在 bpf 程序中,我们不能直接访问内核内存,而需要使用这样的宏来安全地读取数据。","breadcrumbs":"编写 eBPF 程序 Memleak 监控内存泄漏 » memleak 内核态 eBPF 程序实现","id":"108","title":"memleak 内核态 eBPF 程序实现"},"109":{"body":"在理解 BPF 内核部分之后,我们转到用户空间程序。用户空间程序与BPF内核程序紧密配合,它负责将BPF程序加载到内核,设置和管理BPF map,以及处理从BPF程序收集到的数据。用户态程序较长,我们这里可以简要参考一下它的挂载点。 int attach_uprobes(struct memleak_bpf *skel)\n{ ATTACH_UPROBE_CHECKED(skel, malloc, malloc_enter); ATTACH_URETPROBE_CHECKED(skel, malloc, malloc_exit); ATTACH_UPROBE_CHECKED(skel, calloc, calloc_enter); ATTACH_URETPROBE_CHECKED(skel, calloc, calloc_exit); ATTACH_UPROBE_CHECKED(skel, realloc, realloc_enter); ATTACH_URETPROBE_CHECKED(skel, realloc, realloc_exit); ATTACH_UPROBE_CHECKED(skel, mmap, mmap_enter); ATTACH_URETPROBE_CHECKED(skel, mmap, mmap_exit); ATTACH_UPROBE_CHECKED(skel, posix_memalign, posix_memalign_enter); ATTACH_URETPROBE_CHECKED(skel, posix_memalign, posix_memalign_exit); ATTACH_UPROBE_CHECKED(skel, memalign, memalign_enter); ATTACH_URETPROBE_CHECKED(skel, memalign, memalign_exit); ATTACH_UPROBE_CHECKED(skel, free, free_enter); ATTACH_UPROBE_CHECKED(skel, munmap, munmap_enter); // the following probes are intentinally allowed to fail attachment // deprecated in libc.so bionic ATTACH_UPROBE(skel, valloc, valloc_enter); ATTACH_URETPROBE(skel, valloc, valloc_exit); // deprecated in libc.so bionic ATTACH_UPROBE(skel, pvalloc, pvalloc_enter); ATTACH_URETPROBE(skel, pvalloc, pvalloc_exit); // added in C11 ATTACH_UPROBE(skel, aligned_alloc, aligned_alloc_enter); ATTACH_URETPROBE(skel, aligned_alloc, aligned_alloc_exit); return 0;\n} 在这段代码中,我们看到一个名为attach_uprobes的函数,该函数负责将uprobes(用户空间探测点)挂载到内存分配和释放函数上。在Linux中,uprobes是一种内核机制,可以在用户空间程序中的任意位置设置断点,这使得我们可以非常精确地观察和控制用户空间程序的行为。 这里,每个内存相关的函数都通过两个uprobes进行跟踪:一个在函数入口(enter),一个在函数退出(exit)。因此,每当这些函数被调用或返回时,都会触发一个uprobes事件,进而触发相应的BPF程序。 在具体的实现中,我们使用了ATTACH_UPROBE和ATTACH_URETPROBE两个宏来附加uprobes和uretprobes(函数返回探测点)。每个宏都需要三个参数:BPF程序的骨架(skel),要监视的函数名,以及要触发的BPF程序的名称。 这些挂载点包括常见的内存分配函数,如malloc、calloc、realloc、mmap、posix_memalign、memalign、free等,以及对应的退出点。另外,我们也观察一些可能的分配函数,如valloc、pvalloc、aligned_alloc等,尽管它们可能不总是存在。 这些挂载点的目标是捕获所有可能的内存分配和释放事件,从而使我们的内存泄露检测工具能够获取到尽可能全面的数据。这种方法可以让我们不仅能跟踪到内存分配和释放,还能得到它们发生的上下文信息,例如调用栈和调用次数,从而帮助我们定位和修复内存泄露问题。 注意,一些内存分配函数可能并不存在或已弃用,比如valloc、pvalloc等,因此它们的附加可能会失败。在这种情况下,我们允许附加失败,并不会阻止程序的执行。这是因为我们更关注的是主流和常用的内存分配函数,而这些已经被弃用的函数往往在实际应用中较少使用。 完整的源代码: https://github.com/eunomia-bpf/bpf-developer-tutorial/tree/main/src/16-memleak 参考: https://github.com/iovisor/bcc/blob/master/libbpf-tools/memleak.c","breadcrumbs":"编写 eBPF 程序 Memleak 监控内存泄漏 » 用户态程序","id":"109","title":"用户态程序"},"11":{"body":"libbpf-bootstrap是一个基于libbpf库的BPF开发脚手架,从其 github 上可以得到其源码。 libbpf-bootstrap综合了BPF社区过去多年的实践,为开发者提了一个现代化的、便捷的工作流,实 现了一次编译,重复使用的目的。 基于libbpf-bootstrap的BPF程序对于源文件有一定的命名规则, 用于生成内核态字节码的bpf文件以.bpf.c结尾,用户态加载字节码的文件以.c结尾,且这两个文件的 前缀必须相同。 基于libbpf-bootstrap的BPF程序在编译时会先将*.bpf.c文件编译为 对应的.o文件,然后根据此文件生成skeleton文件,即*.skel.h,这个文件会包含内核态中定义的一些 数据结构,以及用于装载内核态代码的关键函数。在用户态代码include此文件之后调用对应的装载函数即可将 字节码装载到内核中。同样的,libbpf-bootstrap也有非常完备的入门教程,用户可以在 该处 得到详细的入门操作介绍。","breadcrumbs":"介绍 eBPF 的基本概念、常见的开发工具 » libbpf","id":"11","title":"libbpf"},"110":{"body":"$ make\n$ sudo ./memleak using default object: libc.so.6\nusing page size: 4096\ntracing kernel: true\nTracing outstanding memory allocs... Hit Ctrl-C to end\n[17:17:27] Top 10 stacks with outstanding allocations:\n1236992 bytes in 302 allocations from stack 0 [] 1 [] 2 [] 3 [] 4 [] 5 [] 6 [] \n...","breadcrumbs":"编写 eBPF 程序 Memleak 监控内存泄漏 » 编译运行","id":"110","title":"编译运行"},"111":{"body":"通过本篇 eBPF 入门实践教程,您已经学习了如何编写 Memleak eBPF 监控程序,以实时监控程序的内存泄漏。您已经了解了 eBPF 在内存监控方面的应用,学会了使用 BPF API 编写 eBPF 程序,创建和使用 eBPF maps,并且明白了如何用 eBPF 工具监测和分析内存泄漏问题。我们展示了一个详细的例子,帮助您理解 eBPF 代码的运行流程和原理。 您可以访问我们的教程代码仓库 https://github.com/eunomia-bpf/bpf-developer-tutorial 或网站 https://eunomia.dev/zh/tutorials/ 以获取更多示例和完整的教程。 接下来的教程将进一步探讨 eBPF 的高级特性,我们会继续分享更多有关 eBPF 开发实践的内容。希望这些知识和技巧能帮助您更好地了解和使用 eBPF,以解决实际工作中遇到的问题。","breadcrumbs":"编写 eBPF 程序 Memleak 监控内存泄漏 » 总结","id":"111","title":"总结"},"112":{"body":"eBPF(扩展的伯克利数据包过滤器)是 Linux 内核中的一种新技术,允许用户在内核空间中执行自定义程序,而无需更改内核代码。这为系统管理员和开发者提供了强大的工具,可以深入了解和监控系统的行为,从而进行优化。 在本篇教程中,我们将探索如何使用 eBPF 编写程序来统计随机和顺序的磁盘 I/O。磁盘 I/O 是计算机性能的关键指标之一,特别是在数据密集型应用中。","breadcrumbs":"编写 eBPF 程序 Biopattern 统计随机/顺序磁盘 I/O » eBPF 入门实践教程十七:编写 eBPF 程序统计随机/顺序磁盘 I/O","id":"112","title":"eBPF 入门实践教程十七:编写 eBPF 程序统计随机/顺序磁盘 I/O"},"113":{"body":"随着技术的进步和数据量的爆炸性增长,磁盘 I/O 成为了系统性能的关键瓶颈。应用程序的性能很大程度上取决于其如何与存储层进行交互。因此,深入了解和优化磁盘 I/O,特别是随机和顺序的 I/O,变得尤为重要。 随机 I/O :随机 I/O 发生在应用程序从磁盘的非连续位置读取或写入数据时。这种 I/O 模式的主要特点是磁盘头需要频繁地在不同的位置之间移动,导致其通常比顺序 I/O 的速度慢。典型的产生随机 I/O 的场景包括数据库查询、文件系统的元数据操作以及虚拟化环境中的并发任务。 顺序 I/O :与随机 I/O 相反,顺序 I/O 是当应用程序连续地读取或写入磁盘上的数据块。这种 I/O 模式的优势在于磁盘头可以在一个方向上连续移动,从而大大提高了数据的读写速度。视频播放、大型文件的下载或上传以及连续的日志记录都是产生顺序 I/O 的典型应用。 为了实现存储性能的最优化,了解随机和顺序的磁盘 I/O 是至关重要的。例如,随机 I/O 敏感的应用程序在 SSD 上的性能通常远超于传统硬盘,因为 SSD 在处理随机 I/O 时几乎没有寻址延迟。相反,对于大量顺序 I/O 的应用,如何最大化磁盘的连续读写速度则更为关键。 在本教程的后续部分,我们将详细探讨如何使用 eBPF 工具来实时监控和统计这两种类型的磁盘 I/O。这不仅可以帮助我们更好地理解系统的 I/O 行为,还可以为进一步的性能优化提供有力的数据支持。","breadcrumbs":"编写 eBPF 程序 Biopattern 统计随机/顺序磁盘 I/O » 随机/顺序磁盘 I/O","id":"113","title":"随机/顺序磁盘 I/O"},"114":{"body":"Biopattern 可以统计随机/顺序磁盘I/O次数的比例。 首先,确保你已经正确安装了 libbpf 和相关的工具集,可以在这里找到对应的源代码: bpf-developer-tutorial 导航到 biopattern 的源代码目录,并使用 make 命令进行编译: cd ~/bpf-developer-tutorial/src/17-biopattern\nmake 编译成功后,你应该可以在当前目录下看到 biopattern 的可执行文件。基本的运行命令如下: sudo ./biopattern [interval] [count] 例如,要每秒打印一次输出,并持续10秒,你可以运行: $ sudo ./biopattern 1 10\nTracing block device I/O requested seeks... Hit Ctrl-C to end.\nDISK %RND %SEQ COUNT KBYTES\nsr0 0 100 3 0\nsr1 0 100 8 0\nsda 0 100 1 4\nsda 100 0 26 136\nsda 0 100 1 4 输出列的含义如下: DISK:被追踪的磁盘名称。 %RND:随机 I/O 的百分比。 %SEQ:顺序 I/O 的百分比。 COUNT:在指定的时间间隔内的 I/O 请求次数。 KBYTES:在指定的时间间隔内读写的数据量(以 KB 为单位)。 从上述输出中,我们可以得出以下结论: sr0 和 sr1 设备在观测期间主要进行了顺序 I/O,但数据量很小。 sda 设备在某些时间段内只进行了随机 I/O,而在其他时间段内只进行了顺序 I/O。 这些信息可以帮助我们了解系统的 I/O 模式,从而进行针对性的优化。","breadcrumbs":"编写 eBPF 程序 Biopattern 统计随机/顺序磁盘 I/O » Biopattern","id":"114","title":"Biopattern"},"115":{"body":"首先,让我们看一下 biopattern 的核心 eBPF 内核态代码: #include \n#include \n#include \n#include \"biopattern.h\"\n#include \"maps.bpf.h\"\n#include \"core_fixes.bpf.h\" const volatile bool filter_dev = false;\nconst volatile __u32 targ_dev = 0; struct { __uint(type, BPF_MAP_TYPE_HASH); __uint(max_entries, 64); __type(key, u32); __type(value, struct counter);\n} counters SEC(\".maps\"); SEC(\"tracepoint/block/block_rq_complete\")\nint handle__block_rq_complete(void *args)\n{ struct counter *counterp, zero = {}; sector_t sector; u32 nr_sector; u32 dev; if (has_block_rq_completion()) { struct trace_event_raw_block_rq_completion___x *ctx = args; sector = BPF_CORE_READ(ctx, sector); nr_sector = BPF_CORE_READ(ctx, nr_sector); dev = BPF_CORE_READ(ctx, dev); } else { struct trace_event_raw_block_rq_complete___x *ctx = args; sector = BPF_CORE_READ(ctx, sector); nr_sector = BPF_CORE_READ(ctx, nr_sector); dev = BPF_CORE_READ(ctx, dev); } if (filter_dev && targ_dev != dev) return 0; counterp = bpf_map_lookup_or_try_init(&counters, &dev, &zero); if (!counterp) return 0; if (counterp->last_sector) { if (counterp->last_sector == sector) __sync_fetch_and_add(&counterp->sequential, 1); else __sync_fetch_and_add(&counterp->random, 1); __sync_fetch_and_add(&counterp->bytes, nr_sector * 512); } counterp->last_sector = sector + nr_sector; return 0;\n} char LICENSE[] SEC(\"license\") = \"GPL\"; 全局变量定义 const volatile bool filter_dev = false; const volatile __u32 targ_dev = 0; 这两个全局变量用于设备过滤。filter_dev 决定是否启用设备过滤,而 targ_dev 是我们想要追踪的目标设备的标识符。 BPF map 定义: struct { __uint(type, BPF_MAP_TYPE_HASH); __uint(max_entries, 64); __type(key, u32); __type(value, struct counter); } counters SEC(\".maps\"); 这部分代码定义了一个 BPF map,类型为哈希表。该映射的键是设备的标识符,而值是一个 counter 结构体,用于存储设备的 I/O 统计信息。 追踪点函数: SEC(\"tracepoint/block/block_rq_complete\") int handle__block_rq_complete(void *args) { struct counter *counterp, zero = {}; sector_t sector; u32 nr_sector; u32 dev; if (has_block_rq_completion()) { struct trace_event_raw_block_rq_completion___x *ctx = args; sector = BPF_CORE_READ(ctx, sector); nr_sector = BPF_CORE_READ(ctx, nr_sector); dev = BPF_CORE_READ(ctx, dev); } else { struct trace_event_raw_block_rq_complete___x *ctx = args; sector = BPF_CORE_READ(ctx, sector); nr_sector = BPF_CORE_READ(ctx, nr_sector); dev = BPF_CORE_READ(ctx, dev); } if (filter_dev && targ_dev != dev) return 0; counterp = bpf_map_lookup_or_try_init(&counters, &dev, &zero); if (!counterp) return 0; if (counterp->last_sector) { if (counterp->last_sector == sector) __sync_fetch_and_add(&counterp->sequential, 1); else __sync_fetch_and_add(&counterp->random, 1); __sync_fetch_and_add(&counterp->bytes, nr_sector * 512); } counterp->last_sector = sector + nr_sector; return 0; } 在 Linux 中,每次块设备的 I/O 请求完成时,都会触发一个名为 block_rq_complete 的追踪点。这为我们提供了一个机会,通过 eBPF 来捕获这些事件,并进一步分析 I/O 的模式。 主要逻辑分析: 提取 I/O 请求信息 :从传入的参数中获取 I/O 请求的相关信息。这里有两种可能的上下文结构,取决于 has_block_rq_completion 的返回值。这是因为不同版本的 Linux 内核可能会有不同的追踪点定义。无论哪种情况,我们都从上下文中提取出扇区号 (sector)、扇区数量 (nr_sector) 和设备标识符 (dev)。 设备过滤 :如果启用了设备过滤 (filter_dev 为 true),并且当前设备不是目标设备 (targ_dev),则直接返回。这允许用户只追踪特定的设备,而不是所有设备。 统计信息更新 : - 查找或初始化统计信息 :使用 bpf_map_lookup_or_try_init 函数查找或初始化与当前设备相关的统计信息。如果映射中没有当前设备的统计信息,它会使用 zero 结构体进行初始化。 - 判断 I/O 模式 :根据当前 I/O 请求与上一个 I/O 请求的扇区号,我们可以判断当前请求是随机的还是顺序的。如果两次请求的扇区号相同,那么它是顺序的;否则,它是随机的。然后,我们使用 __sync_fetch_and_add 函数更新相应的统计信息。这是一个原子操作,确保在并发环境中数据的一致性。 - 更新数据量 :我们还更新了该设备的总数据量,这是通过将扇区数量 (nr_sector) 乘以 512(每个扇区的字节数)来实现的。 - 更新最后一个 I/O 请求的扇区号 :为了下一次的比较,我们更新了 last_sector 的值。 在 Linux 内核的某些版本中,由于引入了一个新的追踪点 block_rq_error,追踪点的命名和结构发生了变化。这意味着,原先的 block_rq_complete 追踪点的结构名称从 trace_event_raw_block_rq_complete 更改为 trace_event_raw_block_rq_completion。这种变化可能会导致 eBPF 程序在不同版本的内核上出现兼容性问题。 为了解决这个问题,biopattern 工具引入了一种机制来动态检测当前内核使用的是哪种追踪点结构,即 has_block_rq_completion 函数。 定义两种追踪点结构 : struct trace_event_raw_block_rq_complete___x { dev_t dev; sector_t sector; unsigned int nr_sector; } __attribute__((preserve_access_index)); struct trace_event_raw_block_rq_completion___x { dev_t dev; sector_t sector; unsigned int nr_sector; } __attribute__((preserve_access_index)); 这里定义了两种追踪点结构,分别对应于不同版本的内核。每种结构都包含设备标识符 (dev)、扇区号 (sector) 和扇区数量 (nr_sector)。 动态检测追踪点结构 : static __always_inline bool has_block_rq_completion() { if (bpf_core_type_exists(struct trace_event_raw_block_rq_completion___x)) return true; return false; } has_block_rq_completion 函数使用 bpf_core_type_exists 函数来检测当前内核是否存在 trace_event_raw_block_rq_completion___x 结构。如果存在,函数返回 true,表示当前内核使用的是新的追踪点结构;否则,返回 false,表示使用的是旧的结构。在对应的 eBPF 代码中,会根据两种不同的定义分别进行处理,这也是适配不同内核版本之间的变更常见的方案。","breadcrumbs":"编写 eBPF 程序 Biopattern 统计随机/顺序磁盘 I/O » eBPF Biopattern 实现原理","id":"115","title":"eBPF Biopattern 实现原理"},"116":{"body":"biopattern 工具的用户态代码负责从 BPF 映射中读取统计数据,并将其展示给用户。通过这种方式,系统管理员可以实时监控每个设备的 I/O 模式,从而更好地理解和优化系统的 I/O 性能。 主循环: /* main: poll */ while (1) { sleep(env.interval); err = print_map(obj->maps.counters, partitions); if (err) break; if (exiting || --env.times == 0) break; } 这是 biopattern 工具的主循环,它的工作流程如下: 等待 :使用 sleep 函数等待指定的时间间隔 (env.interval)。 打印映射 :调用 print_map 函数打印 BPF 映射中的统计数据。 退出条件 :如果收到退出信号 (exiting 为 true) 或者达到指定的运行次数 (env.times 达到 0),则退出循环。 打印映射函数: static int print_map(struct bpf_map *counters, struct partitions *partitions) { __u32 total, lookup_key = -1, next_key; int err, fd = bpf_map__fd(counters); const struct partition *partition; struct counter counter; struct tm *tm; char ts[32]; time_t t; while (!bpf_map_get_next_key(fd, &lookup_key, &next_key)) { err = bpf_map_lookup_elem(fd, &next_key, &counter); if (err < 0) { fprintf(stderr, \"failed to lookup counters: %d\\n\", err); return -1; } lookup_key = next_key; total = counter.sequential + counter.random; if (!total) continue; if (env.timestamp) { time(&t); tm = localtime(&t); strftime(ts, sizeof(ts), \"%H:%M:%S\", tm); printf(\"%-9s \", ts); } partition = partitions__get_by_dev(partitions, next_key); printf(\"%-7s %5ld %5ld %8d %10lld\\n\", partition ? partition->name : \"Unknown\", counter.random * 100L / total, counter.sequential * 100L / total, total, counter.bytes / 1024); } lookup_key = -1; while (!bpf_map_get_next_key(fd, &lookup_key, &next_key)) { err = bpf_map_delete_elem(fd, &next_key); if (err < 0) { fprintf(stderr, \"failed to cleanup counters: %d\\n\", err); return -1; } lookup_key = next_key; } return 0; } print_map 函数负责从 BPF 映射中读取统计数据,并将其打印到控制台。其主要逻辑如下: 遍历 BPF 映射 :使用 bpf_map_get_next_key 和 bpf_map_lookup_elem 函数遍历 BPF 映射,获取每个设备的统计数据。 计算总数 :计算每个设备的随机和顺序 I/O 的总数。 打印统计数据 :如果启用了时间戳 (env.timestamp 为 true),则首先打印当前时间。接着,打印设备名称、随机 I/O 的百分比、顺序 I/O 的百分比、总 I/O 数量和总数据量(以 KB 为单位)。 清理 BPF 映射 :为了下一次的统计,使用 bpf_map_get_next_key 和 bpf_map_delete_elem 函数清理 BPF 映射中的所有条目。","breadcrumbs":"编写 eBPF 程序 Biopattern 统计随机/顺序磁盘 I/O » 用户态代码","id":"116","title":"用户态代码"},"117":{"body":"在本教程中,我们深入探讨了如何使用 eBPF 工具 biopattern 来实时监控和统计随机和顺序的磁盘 I/O。我们首先了解了随机和顺序磁盘 I/O 的重要性,以及它们对系统性能的影响。接着,我们详细介绍了 biopattern 的工作原理,包括如何定义和使用 BPF maps,如何处理不同版本的 Linux 内核中的追踪点变化,以及如何在 eBPF 程序中捕获和分析磁盘 I/O 事件。 您可以访问我们的教程代码仓库 https://github.com/eunomia-bpf/bpf-developer-tutorial 或网站 https://eunomia.dev/zh/tutorials/ 以获取更多示例和完整的教程。 完整代码: https://github.com/eunomia-bpf/bpf-developer-tutorial/tree/main/src/17-biopattern bcc 工具: https://github.com/iovisor/bcc/blob/master/libbpf-tools/biopattern.c","breadcrumbs":"编写 eBPF 程序 Biopattern 统计随机/顺序磁盘 I/O » 总结","id":"117","title":"总结"},"118":{"body":"可以在这里找到更多关于 eBPF 的信息: https://github.com/zoidbergwill/awesome-ebpf https://ebpf.io/","breadcrumbs":"更多的参考资料 » 更多的参考资料","id":"118","title":"更多的参考资料"},"119":{"body":"eBPF (扩展的伯克利数据包过滤器) 是一项强大的网络和性能分析工具,被广泛应用在 Linux 内核上。eBPF 使得开发者能够动态地加载、更新和运行用户定义的代码,而无需重启内核或更改内核源代码。这个特性使得 eBPF 能够提供极高的灵活性和性能,使其在网络和系统性能分析方面具有广泛的应用。安全方面的 eBPF 应用也是如此,本文将介绍如何使用 eBPF LSM(Linux Security Modules)机制实现一个简单的安全检查程序。","breadcrumbs":"使用 LSM 进行安全检测防御 » eBPF 入门实践教程:使用 LSM 进行安全检测防御","id":"119","title":"eBPF 入门实践教程:使用 LSM 进行安全检测防御"},"12":{"body":"开发、构建和分发 eBPF 一直以来都是一个高门槛的工作,使用 BCC、bpftrace 等工具开发效率高、可移植性好,但是分发部署时需要安装 LLVM、Clang等编译环境,每次运行的时候执行本地或远程编译过程,资源消耗较大;使用原生的 CO-RE libbpf时又需要编写不少用户态加载代码来帮助 eBPF 程序正确加载和从内核中获取上报的信息,同时对于 eBPF 程序的分发、管理也没有很好地解决方案。 eunomia-bpf 是一个开源的 eBPF 动态加载运行时和开发工具链,是为了简化 eBPF 程序的开发、构建、分发、运行而设计的,基于 libbpf 的 CO-RE 轻量级开发框架。 使用 eunomia-bpf ,可以: 在编写 eBPF 程序或工具时只编写内核态代码,自动获取内核态导出信息,并作为模块动态加载; 使用 WASM 进行用户态交互程序的开发,在 WASM 虚拟机内部控制整个 eBPF 程序的加载和执行,以及处理相关数据; eunomia-bpf 可以将预编译的 eBPF 程序打包为通用的 JSON 或 WASM 模块,跨架构和内核版本进行分发,无需重新编译即可动态加载运行。 eunomia-bpf 由一个编译工具链和一个运行时库组成, 对比传统的 BCC、原生 libbpf 等框架,大幅简化了 eBPF 程序的开发流程,在大多数时候只需编写内核态代码,即可轻松构建、打包、发布完整的 eBPF 应用,同时内核态 eBPF 代码保证和主流的 libbpf, libbpfgo, libbpf-rs 等开发框架的 100% 兼容性。需要编写用户态代码的时候,也可以借助 Webassembly 实现通过多种语言进行用户态开发。和 bpftrace 等脚本工具相比, eunomia-bpf 保留了类似的便捷性, 同时不仅局限于 trace 方面, 可以用于更多的场景, 如网络、安全等等。 eunomia-bpf 项目 Github 地址: https://github.com/eunomia-bpf/eunomia-bpf gitee 镜像: https://gitee.com/anolis/eunomia","breadcrumbs":"介绍 eBPF 的基本概念、常见的开发工具 » eunomia-bpf","id":"12","title":"eunomia-bpf"},"120":{"body":"LSM 从 Linux 2.6 开始成为官方内核的一个安全框架,基于此的安全实现包括 SELinux 和 AppArmor 等。在 Linux 5.7 引入 BPF LSM 后,系统开发人员已经能够自由地实现函数粒度的安全检查能力,本文就提供了这样一个案例:限制通过 socket connect 函数对特定 IPv4 地址进行访问的 BPF LSM 程序。(可见其控制精度是很高的)","breadcrumbs":"使用 LSM 进行安全检测防御 » 背景","id":"120","title":"背景"},"121":{"body":"LSM(Linux Security Modules)是 Linux 内核中用于支持各种计算机安全模型的框架。LSM 在 Linux 内核安全相关的关键路径上预置了一批 hook 点,从而实现了内核和安全模块的解耦,使不同的安全模块可以自由地在内核中加载/卸载,无需修改原有的内核代码就可以加入安全检查功能。 在过去,使用 LSM 主要通过配置已有的安全模块(如 SELinux 和 AppArmor)或编写自己的内核模块;而在 Linux 5.7 引入 BPF LSM 机制后,一切都变得不同了:现在,开发人员可以通过 eBPF 编写自定义的安全策略,并将其动态加载到内核中的 LSM 挂载点,而无需配置或编写内核模块。 现在 LSM 支持的 hook 点包括但不限于: 对文件的打开、创建、删除和移动等; 文件系统的挂载; 对 task 和 process 的操作; 对 socket 的操作(创建、绑定 socket,发送和接收消息等); 更多 hook 点可以参考 lsm_hooks.h 。","breadcrumbs":"使用 LSM 进行安全检测防御 » LSM 概述","id":"121","title":"LSM 概述"},"122":{"body":"首先,请确认内核版本高于 5.7。接下来,可以通过 $ cat /boot/config-$(uname -r) | grep BPF_LSM\nCONFIG_BPF_LSM=y 判断是否内核是否支持 BPF LSM。上述条件都满足的情况下,可以通过 $ cat /sys/kernel/security/lsm\nndlock,lockdown,yama,integrity,apparmor 查看输出是否包含 bpf 选项,如果输出不包含(像上面的例子),可以通过修改 /etc/default/grub: GRUB_CMDLINE_LINUX=\"lsm=ndlock,lockdown,yama,integrity,apparmor,bpf\" 并通过 update-grub2 命令更新 grub 配置(不同系统的对应命令可能不同),然后重启系统。","breadcrumbs":"使用 LSM 进行安全检测防御 » 确认 BPF LSM 是否可用","id":"122","title":"确认 BPF LSM 是否可用"},"123":{"body":"// lsm-connect.bpf.c\n#include \"vmlinux.h\"\n#include \n#include \n#include char LICENSE[] SEC(\"license\") = \"GPL\"; #define EPERM 1\n#define AF_INET 2 const __u32 blockme = 16843009; // 1.1.1.1 -> int SEC(\"lsm/socket_connect\")\nint BPF_PROG(restrict_connect, struct socket *sock, struct sockaddr *address, int addrlen, int ret)\n{ // Satisfying \"cannot override a denial\" rule if (ret != 0) { return ret; } // Only IPv4 in this example if (address->sa_family != AF_INET) { return 0; } // Cast the address to an IPv4 socket address struct sockaddr_in *addr = (struct sockaddr_in *)address; // Where do you want to go? __u32 dest = addr->sin_addr.s_addr; bpf_printk(\"lsm: found connect to %d\", dest); if (dest == blockme) { bpf_printk(\"lsm: blocking %d\", dest); return -EPERM; } return 0;\n} 这是一段 C 实现的 eBPF 内核侧代码,它会阻碍所有试图通过 socket 对 1.1.1.1 的连接操作,其中: SEC(\"lsm/socket_connect\") 宏指出该程序期望的挂载点; 程序通过 BPF_PROG 宏定义(详情可查看 tools/lib/bpf/bpf_tracing.h ); restrict_connect 是 BPF_PROG 宏要求的程序名; ret 是该挂载点上(潜在的)当前函数之前的 LSM 检查程序的返回值; 整个程序的思路不难理解: 首先,若其他安全检查函数返回值不为 0(不通过),则无需检查,直接返回不通过; 接下来,判断是否为 IPV4 的连接请求,并比较试图连接的地址是否为 1.1.1.1; 若请求地址为 1.1.1.1 则拒绝连接,否则允许连接; 在程序运行期间,所有通过 socket 的连接操作都会被输出到 /sys/kernel/debug/tracing/trace_pipe。","breadcrumbs":"使用 LSM 进行安全检测防御 » 编写 eBPF 程序","id":"123","title":"编写 eBPF 程序"},"124":{"body":"通过容器编译: docker run -it -v `pwd`/:/src/ ghcr.io/eunomia-bpf/ecc-`uname -m`:latest 或是通过 ecc 编译: $ ecc lsm-connect.bpf.c\nCompiling bpf object...\nPacking ebpf object and config into package.json... 并通过 ecli 运行: sudo ecli run package.json 接下来,可以打开另一个 terminal,并尝试访问 1.1.1.1: $ ping 1.1.1.1\nping: connect: Operation not permitted\n$ curl 1.1.1.1\ncurl: (7) Couldn't connect to server\n$ wget 1.1.1.1\n--2023-04-23 08:41:18-- (try: 2) http://1.1.1.1/\nConnecting to 1.1.1.1:80... failed: Operation not permitted.\nRetrying. 同时,我们可以查看 bpf_printk 的输出: $ sudo cat /sys/kernel/debug/tracing/trace_pipe ping-7054 [000] d...1 6313.430872: bpf_trace_printk: lsm: found connect to 16843009 ping-7054 [000] d...1 6313.430874: bpf_trace_printk: lsm: blocking 16843009 curl-7058 [000] d...1 6316.346582: bpf_trace_printk: lsm: found connect to 16843009 curl-7058 [000] d...1 6316.346584: bpf_trace_printk: lsm: blocking 16843009 wget-7061 [000] d...1 6318.800698: bpf_trace_printk: lsm: found connect to 16843009 wget-7061 [000] d...1 6318.800700: bpf_trace_printk: lsm: blocking 16843009 完整源代码: https://github.com/eunomia-bpf/bpf-developer-tutorial/tree/main/src/19-lsm-connect","breadcrumbs":"使用 LSM 进行安全检测防御 » 编译运行","id":"124","title":"编译运行"},"125":{"body":"本文介绍了如何使用 BPF LSM 来限制通过 socket 对特定 IPv4 地址的访问。我们可以通过修改 GRUB 配置文件来开启 LSM 的 BPF 挂载点。在 eBPF 程序中,我们通过 BPF_PROG 宏定义函数,并通过 SEC 宏指定挂载点;在函数实现上,遵循 LSM 安全检查模块中 \"cannot override a denial\" 的原则,并根据 socket 连接请求的目的地址对该请求进行限制。 如果您希望学习更多关于 eBPF 的知识和实践,可以访问我们的教程代码仓库 https://github.com/eunomia-bpf/bpf-developer-tutorial 或网站 https://eunomia.dev/zh/tutorials/ 以获取更多示例和完整的教程。","breadcrumbs":"使用 LSM 进行安全检测防御 » 总结","id":"125","title":"总结"},"126":{"body":"https://github.com/leodido/demo-cloud-native-ebpf-day https://aya-rs.dev/book/programs/lsm/#writing-lsm-bpf-program","breadcrumbs":"使用 LSM 进行安全检测防御 » 参考","id":"126","title":"参考"},"127":{"body":"","breadcrumbs":"使用 eBPF 进行 tc 流量控制 » eBPF 入门实践教程二十:使用 eBPF 进行 tc 流量控制","id":"127","title":"eBPF 入门实践教程二十:使用 eBPF 进行 tc 流量控制"},"128":{"body":"Linux 的流量控制子系统(Traffic Control, tc)在内核中存在了多年,类似于 iptables 和 netfilter 的关系,tc 也包括一个用户态的 tc 程序和内核态的 trafiic control 框架,主要用于从速率、顺序等方面控制数据包的发送和接收。从 Linux 4.1 开始,tc 增加了一些新的挂载点,并支持将 eBPF 程序作为 filter 加载到这些挂载点上。","breadcrumbs":"使用 eBPF 进行 tc 流量控制 » 背景","id":"128","title":"背景"},"129":{"body":"从协议栈上看,tc 位于链路层,其所在位置已经完成了 sk_buff 的分配,要晚于 xdp。为了实现对数据包发送和接收的控制,tc 使用队列结构来临时保存并组织数据包,在 tc 子系统中对应的数据结构和算法控制机制被抽象为 qdisc(Queueing discipline),其对外暴露数据包入队和出队的两个回调接口,并在内部隐藏排队算法实现。在 qdisc 中我们可以基于 filter 和 class 实现复杂的树形结构,其中 filter 被挂载到 qdisc 或 class 上用于实现具体的过滤逻辑,返回值决定了该数据包是否属于特定 class。 当数据包到达顶层 qdisc 时,其入队接口被调用,其上挂载的 filter 被依次执行直到一个 filter 匹配成功;此后数据包被送入该 filter 指向的 class,进入该 class 配置的 qdisc 处理流程中。tc 框架提供了所谓 classifier-action 机制,即在数据包匹配到特定 filter 时执行该 filter 所挂载的 action 对数据包进行处理,实现了完整的数据包分类和处理机制。 现有的 tc 为 eBPF 提供了 direct-action 模式,它使得一个作为 filter 加载的 eBPF 程序可以返回像 TC_ACT_OK 等 tc action 的返回值,而不是像传统的 filter 那样仅仅返回一个 classid 并把对数据包的处理交给 action 模块。现在,eBPF 程序可以被挂载到特定的 qdisc 上,并完成对数据包的分类和处理动作。","breadcrumbs":"使用 eBPF 进行 tc 流量控制 » tc 概述","id":"129","title":"tc 概述"},"13":{"body":"eBPF 介绍: https://ebpf.io/ BPF Compiler Collection (BCC): https://github.com/iovisor/bcc eunomia-bpf: https://github.com/eunomia-bpf/eunomia-bpf 您还可以访问我们的教程代码仓库 https://github.com/eunomia-bpf/bpf-developer-tutorial 或网站 https://eunomia.dev/zh/tutorials/ 以获取更多示例和完整的教程源代码。我们会继续分享更多有关 eBPF 开发实践的内容,帮助您更好地理解和掌握 eBPF 技术。","breadcrumbs":"介绍 eBPF 的基本概念、常见的开发工具 » 参考资料","id":"13","title":"参考资料"},"130":{"body":"#include \n#include \n#include \n#include #define TC_ACT_OK 0\n#define ETH_P_IP 0x0800 /* Internet Protocol packet */ /// @tchook {\"ifindex\":1, \"attach_point\":\"BPF_TC_INGRESS\"}\n/// @tcopts {\"handle\":1, \"priority\":1}\nSEC(\"tc\")\nint tc_ingress(struct __sk_buff *ctx)\n{ void *data_end = (void *)(__u64)ctx->data_end; void *data = (void *)(__u64)ctx->data; struct ethhdr *l2; struct iphdr *l3; if (ctx->protocol != bpf_htons(ETH_P_IP)) return TC_ACT_OK; l2 = data; if ((void *)(l2 + 1) > data_end) return TC_ACT_OK; l3 = (struct iphdr *)(l2 + 1); if ((void *)(l3 + 1) > data_end) return TC_ACT_OK; bpf_printk(\"Got IP packet: tot_len: %d, ttl: %d\", bpf_ntohs(l3->tot_len), l3->ttl); return TC_ACT_OK;\n} char __license[] SEC(\"license\") = \"GPL\"; 这段代码定义了一个 eBPF 程序,它可以通过 Linux TC(Transmission Control)来捕获数据包并进行处理。在这个程序中,我们限定了只捕获 IPv4 协议的数据包,然后通过 bpf_printk 函数打印出数据包的总长度和 Time-To-Live(TTL)字段的值。 需要注意的是,我们在代码中使用了一些 BPF 库函数,例如 bpf_htons 和 bpf_ntohs 函数,它们用于进行网络字节序和主机字节序之间的转换。此外,我们还使用了一些注释来为 TC 提供附加点和选项信息。例如,在这段代码的开头,我们使用了以下注释: /// @tchook {\"ifindex\":1, \"attach_point\":\"BPF_TC_INGRESS\"}\n/// @tcopts {\"handle\":1, \"priority\":1} 这些注释告诉 TC 将 eBPF 程序附加到网络接口的 ingress 附加点,并指定了 handle 和 priority 选项的值。关于 libbpf 中 tc 相关的 API 可以参考 patchwork 中的介绍。 总之,这段代码实现了一个简单的 eBPF 程序,用于捕获数据包并打印出它们的信息。","breadcrumbs":"使用 eBPF 进行 tc 流量控制 » 编写 eBPF 程序","id":"130","title":"编写 eBPF 程序"},"131":{"body":"通过容器编译: docker run -it -v `pwd`/:/src/ ghcr.io/eunomia-bpf/ecc-`uname -m`:latest 或是通过 ecc 编译: $ ecc tc.bpf.c\nCompiling bpf object...\nPacking ebpf object and config into package.json... 并通过 ecli 运行: sudo ecli run ./package.json 可以通过如下方式查看程序的输出: $ sudo cat /sys/kernel/debug/tracing/trace_pipe node-1254811 [007] ..s1 8737831.671074: 0: Got IP packet: tot_len: 79, ttl: 64 sshd-1254728 [006] ..s1 8737831.674334: 0: Got IP packet: tot_len: 79, ttl: 64 sshd-1254728 [006] ..s1 8737831.674349: 0: Got IP packet: tot_len: 72, ttl: 64 node-1254811 [007] ..s1 8737831.674550: 0: Got IP packet: tot_len: 71, ttl: 64","breadcrumbs":"使用 eBPF 进行 tc 流量控制 » 编译运行","id":"131","title":"编译运行"},"132":{"body":"本文介绍了如何向 TC 流量控制子系统挂载 eBPF 类型的 filter 来实现对链路层数据包的排队处理。基于 eunomia-bpf 提供的通过注释向 libbpf 传递参数的方案,我们可以将自己编写的 tc BPF 程序以指定选项挂载到目标网络设备,并借助内核的 sk_buff 结构对数据包进行过滤处理。 如果您希望学习更多关于 eBPF 的知识和实践,可以访问我们的教程代码仓库 https://github.com/eunomia-bpf/bpf-developer-tutorial 或网站 https://eunomia.dev/zh/tutorials/ 以获取更多示例和完整的教程。","breadcrumbs":"使用 eBPF 进行 tc 流量控制 » 总结","id":"132","title":"总结"},"133":{"body":"http://just4coding.com/2022/08/05/tc/ https://arthurchiao.art/blog/understanding-tc-da-mode-zh/","breadcrumbs":"使用 eBPF 进行 tc 流量控制 » 参考","id":"133","title":"参考"},"134":{"body":"本文主要记录了笔者在 Android Studio Emulator 中测试高版本 Android Kernel 对基于 libbpf 的 CO-RE 技术支持程度的探索过程、结果和遇到的问题。 测试采用的方式是在 Android Shell 环境下构建 Debian 环境,并基于此尝试构建 eunomia-bpf 工具链、运行其测试用例。","breadcrumbs":"在 Android 上使用 eBPF 程序 » 在 Andorid 上使用 eBPF 程序","id":"134","title":"在 Andorid 上使用 eBPF 程序"},"135":{"body":"截至目前(2023-04),Android 还未对 eBPF 程序的动态加载做出较好的支持,无论是以 bcc 为代表的带编译器分发方案,还是基于 btf 和 libbpf 的 CO-RE 方案,都在较大程度上离不开 Linux 环境的支持,无法在 Android 系统上很好地运行 [1] 。 虽然如此,在 Android 平台上尝试 eBPF 也已经有了一些成功案例,除谷歌官方提供的修改 Android.bp 以将 eBPF 程序随整个系统一同构建并挂载的方案 [2] ,也有人提出基于 Android 内核构建 Linux 环境进而运行 eBPF 工具链的思路,并开发了相关工具。 目前已有的资料,大多基于 adeb/eadb 在 Android 内核基础上构建 Linux 沙箱,并对 bcc 和 bpftrace 相关工具链进行测试,而对 CO-RE 方案的测试工作较少。在 Android 上使用 bcc 工具目前有较多参考资料,如: SeeFlowerX: https://blog.seeflower.dev/category/eBPF/ evilpan: https://bbs.kanxue.com/thread-271043.htm 其主要思路是利用 chroot 在 Android 内核上运行一个 Debian 镜像,并在其中构建整个 bcc 工具链,从而使用 eBPF 工具。如果想要使用 bpftrace,原理也是类似的。 事实上,高版本的 Android 内核已支持 btf 选项,这意味着 eBPF 领域中新兴的 CO-RE 技术也应当能够运用到基于 Android 内核的 Linux 系统中。本文将基于此对 eunomia-bpf 在模拟器环境下进行测试运行。 eunomia-bpf 是一个结合了 libbpf 和 WebAssembly 技术的开源项目,旨在简化 eBPF 程序的编写、编译和部署。该项目可被视作 CO-RE 的一种实践方式,其核心依赖是 libbpf,相信对 eunomia-bpf 的测试工作能够为其他 CO-RE 方案提供参考。","breadcrumbs":"在 Android 上使用 eBPF 程序 » 背景","id":"135","title":"背景"},"136":{"body":"Android Emulator(Android Studio Flamingo | 2022.2.1) AVD: Pixel 6 Android Image: Tiramisu Android 13.0 x86_64(5.15.41-android13-8-00055-g4f5025129fe8-ab8949913)","breadcrumbs":"在 Android 上使用 eBPF 程序 » 测试环境","id":"136","title":"测试环境"},"137":{"body":"[3] 从 eadb 仓库 的 releases 页面获取 debianfs-amd64-full.tar.gz 作为 Linux 环境的 rootfs,同时还需要获取该项目的 assets 目录来构建环境; 从 Android Studio 的 Device Manager 配置并启动 Android Virtual Device; 通过 Android Studio SDK 的 adb 工具将 debianfs-amd64-full.tar.gz 和 assets 目录推送到 AVD 中: ./adb push debianfs-amd64-full.tar.gz /data/local/tmp/deb.tar.gz ./adb push assets /data/local/tmp/assets 通过 adb 进入 Android shell 环境并获取 root 权限: ./adb shell su 在 Android shell 中构建并进入 debian 环境: mkdir -p /data/eadb mv /data/local/tmp/assets/* /data/eadb mv /data/local/tmp/deb.tar.gz /data/eadb/deb.tar.gz rm -r /data/local/tmp/assets chmod +x /data/eadb/device-* /data/eadb/device-unpack /data/eadb/run /data/eadb/debian 至此,测试 eBPF 所需的 Linux 环境已经构建完毕。此外,在 Android shell 中(未进入 debian 时)可以通过 zcat /proc/config.gz 并配合 grep 查看内核编译选项。 目前,eadb 打包的 debian 环境存在 libc 版本低,缺少的工具依赖较多等情况;并且由于内核编译选项不同,一些 eBPF 功能可能也无法使用。","breadcrumbs":"在 Android 上使用 eBPF 程序 » 环境搭建","id":"137","title":"环境搭建"},"138":{"body":"在 debian 环境中将 eunomia-bpf 仓库 clone 到本地,具体的构建过程,可以参考仓库的 build.md 。在本次测试中,笔者选用了 ecc 编译生成 package.json 的方式,该工具的构建和使用方式请参考 仓库页面 。 在构建过程中,可能需要自行安装包括但不限于 curl,pkg-config,libssl-dev 等工具。","breadcrumbs":"在 Android 上使用 eBPF 程序 » 工具构建","id":"138","title":"工具构建"},"139":{"body":"有部分 eBPF 程序可以成功在 Android 上运行,但也会有部分应用因为种种原因无法成功被执行。","breadcrumbs":"在 Android 上使用 eBPF 程序 » 结果","id":"139","title":"结果"},"14":{"body":"在本篇博客中,我们将深入探讨eBPF(Extended Berkeley Packet Filter)的基本框架和开发流程。eBPF是一种在Linux内核上运行的强大网络和性能分析工具,它为开发者提供了在内核运行时动态加载、更新和运行用户定义代码的能力。这使得开发者可以实现高效、安全的内核级别的网络监控、性能分析和故障排查等功能。 本文是eBPF入门开发实践教程的第二篇,我们将重点关注如何编写一个简单的eBPF程序,并通过实际例子演示整个开发流程。在阅读本教程之前,建议您先学习第一篇教程,以便对eBPF的基本概念有个大致的了解。 在开发eBPF程序时,有多种开发框架可供选择,如 BCC(BPF Compiler Collection)libbpf、cilium/ebpf、eunomia-bpf 等。虽然不同工具的特点各异,但它们的基本开发流程大致相同。在接下来的内容中,我们将深入了解这些流程,并以 Hello World 程序为例,带领读者逐步掌握eBPF开发的基本技巧。 本教程将帮助您了解eBPF程序的基本结构、编译和加载过程、用户空间与内核空间的交互方式以及调试与优化技巧。通过学习本教程,您将掌握eBPF开发的基本知识,并为后续进一步学习和实践奠定坚实的基础。","breadcrumbs":"eBPF Hello World,基本框架和开发流程 » eBPF 入门开发实践教程一:Hello World,基本框架和开发流程","id":"14","title":"eBPF 入门开发实践教程一:Hello World,基本框架和开发流程"},"140":{"body":"bootstrap 运行输出如下: TIME PID PPID EXIT_CODE DURATION_NS COMM FILENAME EXIT_EVENT\n09:09:19 10217 479 0 0 sh /system/bin/sh 0\n09:09:19 10217 479 0 0 ps /system/bin/ps 0\n09:09:19 10217 479 0 54352100 ps 1\n09:09:21 10219 479 0 0 sh /system/bin/sh 0\n09:09:21 10219 479 0 0 ps /system/bin/ps 0\n09:09:21 10219 479 0 44260900 ps 1 tcpstates 开始监测后在 Linux 环境中通过 wget 下载 Web 页面: TIME SADDR DADDR SKADDR TS_US DELTA_US PID OLDSTATE NEWSTATE FAMILY SPORT DPORT TASK\n09:07:46 0x4007000200005000000000000f02000a 0x5000000000000f02000a8bc53f77 18446635827774444352 3315344998 0 10115 7 2 2 0 80 wget\n09:07:46 0x40020002d98e50003d99f8090f02000a 0xd98e50003d99f8090f02000a8bc53f77 18446635827774444352 3315465870 120872 0 2 1 2 55694 80 swapper/0\n09:07:46 0x40010002d98e50003d99f8090f02000a 0xd98e50003d99f8090f02000a8bc53f77 18446635827774444352 3315668799 202929 10115 1 4 2 55694 80 wget\n09:07:46 0x40040002d98e50003d99f8090f02000a 0xd98e50003d99f8090f02000a8bc53f77 18446635827774444352 3315670037 1237 0 4 5 2 55694 80 swapper/0\n09:07:46 0x40050002000050003d99f8090f02000a 0x50003d99f8090f02000a8bc53f77 18446635827774444352 3315670225 188 0 5 7 2 55694 80 swapper/0\n09:07:47 0x400200020000bb01565811650f02000a 0xbb01565811650f02000a6aa0d9ac 18446635828348806592 3316433261 0 2546 2 7 2 49970 443 ChromiumNet\n09:07:47 0x400200020000bb01db794a690f02000a 0xbb01db794a690f02000aea2afb8e 18446635827774427776 3316535591 0 1469 2 7 2 37386 443 ChromiumNet 开始检测后在 Android Studio 模拟界面打开 Chrome 浏览器并访问百度页面: TIME SADDR DADDR SKADDR TS_US DELTA_US PID OLDSTATE NEWSTATE FAMILY SPORT DPORT TASK\n07:46:58 0x400700020000bb01000000000f02000a 0xbb01000000000f02000aeb6f2270 18446631020066638144 192874641 0 3305 7 2 2 0 443 NetworkService\n07:46:58 0x40020002d28abb01494b6ebe0f02000a 0xd28abb01494b6ebe0f02000aeb6f2270 18446631020066638144 192921938 47297 3305 2 1 2 53898 443 NetworkService\n07:46:58 0x400700020000bb01000000000f02000a 0xbb01000000000f02000ae7e7e8b7 18446631020132433920 193111426 0 3305 7 2 2 0 443 NetworkService\n07:46:58 0x40020002b4a0bb0179ff85e80f02000a 0xb4a0bb0179ff85e80f02000ae7e7e8b7 18446631020132433920 193124670 13244 3305 2 1 2 46240 443 NetworkService\n07:46:58 0x40010002b4a0bb0179ff85e80f02000a 0xb4a0bb0179ff85e80f02000ae7e7e8b7 18446631020132433920 193185397 60727 3305 1 4 2 46240 443 NetworkService\n07:46:58 0x40040002b4a0bb0179ff85e80f02000a 0xb4a0bb0179ff85e80f02000ae7e7e8b7 18446631020132433920 193186122 724 3305 4 5 2 46240 443 NetworkService\n07:46:58 0x400500020000bb0179ff85e80f02000a 0xbb0179ff85e80f02000ae7e7e8b7 18446631020132433920 193186244 122 3305 5 7 2 46240 443 NetworkService\n07:46:59 0x40010002d01ebb01d0c52f5c0f02000a 0xd01ebb01d0c52f5c0f02000a51449c27 18446631020103553856 194110884 0 5130 1 8 2 53278 443 ThreadPoolForeg\n07:46:59 0x400800020000bb01d0c52f5c0f02000a 0xbb01d0c52f5c0f02000a51449c27 18446631020103553856 194121000 10116 3305 8 7 2 53278 443 NetworkService\n07:46:59 0x400700020000bb01000000000f02000a 0xbb01000000000f02000aeb6f2270 18446631020099513920 194603677 0 3305 7 2 2 0 443 NetworkService\n07:46:59 0x40020002d28ebb0182dd92990f02000a 0xd28ebb0182dd92990f02000aeb6f2270 18446631020099513920 194649313 45635 12 2 1 2 53902 443 ksoftirqd/0\n07:47:00 0x400700020000bb01000000000f02000a 0xbb01000000000f02000a26f6e878 18446631020132433920 195193350 0 3305 7 2 2 0 443 NetworkService\n07:47:00 0x40020002ba32bb01e0e09e3a0f02000a 0xba32bb01e0e09e3a0f02000a26f6e878 18446631020132433920 195206992 13642 0 2 1 2 47666 443 swapper/0\n07:47:00 0x400700020000bb01000000000f02000a 0xbb01000000000f02000ae7e7e8b7 18446631020132448128 195233125 0 3305 7 2 2 0 443 NetworkService\n07:47:00 0x40020002b4a8bb0136cac8dd0f02000a 0xb4a8bb0136cac8dd0f02000ae7e7e8b7 18446631020132448128 195246569 13444 3305 2 1 2 46248 443 NetworkService\n07:47:00 0xf02000affff00000000000000000000 0x1aca06cffff00000000000000000000 18446631019225912320 195383897 0 947 7 2 10 0 80 Thread-11\n07:47:00 0x40010002b4a8bb0136cac8dd0f02000a 0xb4a8bb0136cac8dd0f02000ae7e7e8b7 18446631020132448128 195421584 175014 3305 1 4 2 46248 443 NetworkService\n07:47:00 0x40040002b4a8bb0136cac8dd0f02000a 0xb4a8bb0136cac8dd0f02000ae7e7e8b7 18446631020132448128 195422361 777 3305 4 5 2 46248 443 NetworkService\n07:47:00 0x400500020000bb0136cac8dd0f02000a 0xbb0136cac8dd0f02000ae7e7e8b7 18446631020132448128 195422450 88 3305 5 7 2 46248 443 NetworkService\n07:47:01 0x400700020000bb01000000000f02000a 0xbb01000000000f02000aea2afb8e 18446631020099528128 196321556 0 1315 7 2 2 0 443 ChromiumNet","breadcrumbs":"在 Android 上使用 eBPF 程序 » 成功案例","id":"140","title":"成功案例"},"141":{"body":"opensnoop 例如 opensnoop 工具,可以在 Android 上成功构建,但运行报错: libbpf: failed to determine tracepoint 'syscalls/sys_enter_open' perf event ID: No such file or directory\nlibbpf: prog 'tracepoint__syscalls__sys_enter_open': failed to create tracepoint 'syscalls/sys_enter_open' perf event: No such file or directory\nlibbpf: prog 'tracepoint__syscalls__sys_enter_open': failed to auto-attach: -2\nfailed to attach skeleton\nError: BpfError(\"load and attach ebpf program failed\") 后经查看发现内核未开启 CONFIG_FTRACE_SYSCALLS 选项,导致无法使用 syscalls 的 tracepoint。","breadcrumbs":"在 Android 上使用 eBPF 程序 » 一些可能的报错原因","id":"141","title":"一些可能的报错原因"},"142":{"body":"在 Android shell 中查看内核编译选项可以发现 CONFIG_DEBUG_INFO_BTF 默认是打开的,在此基础上 eunomia-bpf 项目提供的 example 已有一些能够成功运行的案例,例如可以监测 exec 族函数的执行和 tcp 连接的状态。 对于无法运行的一些,原因主要是以下两个方面: 内核编译选项未支持相关 eBPF 功能; eadb 打包的 Linux 环境较弱,缺乏必须依赖; 目前在 Android 系统中使用 eBPF 工具基本上仍然需要构建完整的 Linux 运行环境,但 Android 内核本身对 eBPF 的支持已较为全面,本次测试证明较高版本的 Android 内核支持 BTF 调试信息和依赖 CO-RE 的 eBPF 程序的运行。 Android 系统 eBPF 工具的发展需要官方新特性的加入,目前看来通过 Android APP 直接使用 eBPF 工具需要的工作量较大,同时由于 eBPF 工具需要 root 权限,普通 Android 用户的使用会面临较多困难。 如果希望学习更多关于 eBPF 的知识和实践,可以访问我们的教程代码仓库 https://github.com/eunomia-bpf/bpf-developer-tutorial 或网站 https://eunomia.dev/zh/tutorials/ 以获取更多示例和完整的教程。","breadcrumbs":"在 Android 上使用 eBPF 程序 » 总结","id":"142","title":"总结"},"143":{"body":"https://source.android.google.cn/docs/core/architecture/kernel/bpf [1] : https://mp.weixin.qq.com/s/mul4n5D3nXThjxuHV7GpMA [3] : https://blog.seeflower.dev/archives/138/","breadcrumbs":"在 Android 上使用 eBPF 程序 » 参考","id":"143","title":"参考"},"144":{"body":"TODO","breadcrumbs":"使用 eBPF 追踪 HTTP 请求或其他七层协议 » http","id":"144","title":"http"},"145":{"body":"随着TLS在现代网络环境中的广泛应用,跟踪微服务RPC消息已经变得愈加棘手。传统的流量嗅探技术常常受限于只能获取到加密后的数据,导致无法真正观察到通信的原始内容。这种限制为系统的调试和分析带来了不小的障碍。 但现在,我们有了新的解决方案。使用 eBPF 技术,通过其能力在用户空间进行探测,提供了一种方法重新获得明文数据,使得我们可以直观地查看加密前的通信内容。然而,每个应用可能使用不同的库,每个库都有多个版本,这种多样性给跟踪带来了复杂性。 在本教程中,我们将带您了解一种跨多种用户态 SSL/TLS 库的 eBPF 追踪技术,它不仅可以同时跟踪 GnuTLS 和 OpenSSL 等用户态库,而且相比以往,大大降低了对新版本库的维护工作。","breadcrumbs":"使用 uprobe 捕获多种库的 SSL/TLS 明文数据 » eBPF 实践教程:使用 uprobe 捕获多种库的 SSL/TLS 明文数据","id":"145","title":"eBPF 实践教程:使用 uprobe 捕获多种库的 SSL/TLS 明文数据"},"146":{"body":"在深入本教程的主题之前,我们需要理解一些核心概念,这些概念将为我们后面的讨论提供基础。","breadcrumbs":"使用 uprobe 捕获多种库的 SSL/TLS 明文数据 » 背景知识","id":"146","title":"背景知识"},"147":{"body":"SSL (Secure Sockets Layer): 由 Netscape 在 1990 年代早期开发,为网络上的两台机器之间提供数据加密传输。然而,由于某些已知的安全问题,SSL的使用已被其后继者TLS所替代。 TLS (Transport Layer Security): 是 SSL 的继任者,旨在提供更强大和更安全的数据加密方式。TLS 工作通过一个握手过程,在这个过程中,客户端和服务器之间会选择一个加密算法和相应的密钥。一旦握手完成,数据传输开始,所有数据都使用选择的算法和密钥加密。","breadcrumbs":"使用 uprobe 捕获多种库的 SSL/TLS 明文数据 » SSL 和 TLS","id":"147","title":"SSL 和 TLS"},"148":{"body":"Transport Layer Security (TLS) 是一个密码学协议,旨在为计算机网络上的通信提供安全性。它主要目标是通过密码学,例如证书的使用,为两个或更多通信的计算机应用程序提供安全性,包括隐私(机密性)、完整性和真实性。TLS 由两个子层组成:TLS 记录协议和TLS 握手协议。 握手过程 当客户端与启用了TLS的服务器连接并请求建立安全连接时,握手过程开始。握手允许客户端和服务器通过不对称密码来建立连接的安全性参数,完整流程如下: 初始握手 :客户端连接到启用了TLS的服务器,请求安全连接,并提供它支持的密码套件列表(加密算法和哈希函数)。 选择密码套件 :从提供的列表中,服务器选择它也支持的密码套件和哈希函数,并通知客户端已做出的决定。 提供数字证书 :通常,服务器接下来会提供形式为数字证书的身份验证。此证书包含服务器名称、信任的证书授权机构(为证书的真实性提供担保)以及服务器的公共加密密钥。 验证证书 :客户端在继续之前确认证书的有效性。 生成会话密钥 :为了生成用于安全连接的会话密钥,客户端有以下两种方法: 使用服务器的公钥加密一个随机数(PreMasterSecret)并将结果发送到服务器(只有服务器才能使用其私钥解密);双方然后使用该随机数生成一个独特的会话密钥,用于会话期间的数据加密和解密。 使用 Diffie-Hellman 密钥交换(或其变体椭圆曲线DH)来安全地生成一个随机且独特的会话密钥,用于加密和解密,该密钥具有前向保密的额外属性:即使在未来公开了服务器的私钥,也不能用它来解密当前的会话,即使第三方拦截并记录了会话。 一旦上述步骤成功完成,握手过程便结束,加密的连接开始。此连接使用会话密钥进行加密和解密,直到连接关闭。如果上述任何步骤失败,则TLS握手失败,连接将不会建立。 OSI模型中的TLS TLS 和 SSL 不完全适合 OSI 模型或 TCP/IP 模型的任何单一层次。TLS 在“某些可靠的传输协议(例如,TCP)之上运行”,这意味着它位于传输层之上。它为更高的层提供加密,这通常是表示层的功能。但是,使用TLS 的应用程序通常视其为传输层,即使使用TLS的应用程序必须积极控制启动 TLS 握手和交换的认证证书的处理。","breadcrumbs":"使用 uprobe 捕获多种库的 SSL/TLS 明文数据 » TLS 的工作原理","id":"148","title":"TLS 的工作原理"},"149":{"body":"eBPF (Extended Berkeley Packet Filter): 是一种内核技术,允许用户在内核空间中运行预定义的程序,不需要修改内核源代码或重新加载模块。它创建了一个桥梁,使得用户空间和内核空间可以交互,从而为系统监控、性能分析和网络流量分析等任务提供了无前例的能力。 uprobes 是eBPF的一个重要特性,允许我们在用户空间应用程序中动态地插入探测点,特别适用于跟踪SSL/TLS库中的函数调用。","breadcrumbs":"使用 uprobe 捕获多种库的 SSL/TLS 明文数据 » eBPF 和 uprobe","id":"149","title":"eBPF 和 uprobe"},"15":{"body":"在开始编写eBPF程序之前,我们需要准备一个合适的开发环境,并了解eBPF程序的基本开发流程。本部分将详细介绍这些内容。","breadcrumbs":"eBPF Hello World,基本框架和开发流程 » eBPF开发环境准备与基本开发流程","id":"15","title":"eBPF开发环境准备与基本开发流程"},"150":{"body":"SSL/TLS协议的实现主要依赖于用户态库。以下是一些常见的库: OpenSSL: 一个开源的、功能齐全的加密库,广泛应用于许多开源和商业项目中。 BoringSSL: 是Google维护的OpenSSL的一个分支,重点是简化和优化,适用于Google的需求。 GnuTLS: 是GNU项目的一部分,提供了SSL,TLS和DTLS协议的实现。与OpenSSL和BoringSSL相比,GnuTLS在API设计、模块结构和许可证上有所不同。","breadcrumbs":"使用 uprobe 捕获多种库的 SSL/TLS 明文数据 » 用户态库","id":"150","title":"用户态库"},"151":{"body":"OpenSSL 是一个广泛应用的开源库,提供了 SSL 和 TLS 协议的完整实现,并广泛用于各种应用程序中以确保数据传输的安全性。其中,SSL_read() 和 SSL_write() 是两个核心的 API 函数,用于从 TLS/SSL 连接中读取和写入数据。本章节,我们将深入这两个函数,帮助你理解其工作机制。","breadcrumbs":"使用 uprobe 捕获多种库的 SSL/TLS 明文数据 » OpenSSL API 分析","id":"151","title":"OpenSSL API 分析"},"152":{"body":"当我们想从一个已建立的 SSL 连接中读取数据时,可以使用 SSL_read 或 SSL_read_ex 函数。函数原型如下: int SSL_read_ex(SSL *ssl, void *buf, size_t num, size_t *readbytes);\nint SSL_read(SSL *ssl, void *buf, int num); SSL_read 和 SSL_read_ex 试图从指定的 ssl 中读取最多 num 字节的数据到缓冲区 buf 中。成功时,SSL_read_ex 会在 *readbytes 中存储实际读取到的字节数。","breadcrumbs":"使用 uprobe 捕获多种库的 SSL/TLS 明文数据 » 1. SSL_read 函数","id":"152","title":"1. SSL_read 函数"},"153":{"body":"当我们想往一个已建立的 SSL 连接中写入数据时,可以使用 SSL_write 或 SSL_write_ex 函数。 函数原型: int SSL_write_ex(SSL *s, const void *buf, size_t num, size_t *written);\nint SSL_write(SSL *ssl, const void *buf, int num); SSL_write 和 SSL_write_ex 会从缓冲区 buf 中将最多 num 字节的数据写入到指定的 ssl 连接中。成功时,SSL_write_ex 会在 *written 中存储实际写入的字节数。","breadcrumbs":"使用 uprobe 捕获多种库的 SSL/TLS 明文数据 » 2. SSL_write 函数","id":"153","title":"2. SSL_write 函数"},"154":{"body":"在我们的例子中,我们使用 eBPF 来 hook ssl_read 和 ssl_write 函数,从而在数据读取或写入 SSL 连接时执行自定义操作。","breadcrumbs":"使用 uprobe 捕获多种库的 SSL/TLS 明文数据 » eBPF 内核态代码编写","id":"154","title":"eBPF 内核态代码编写"},"155":{"body":"首先,我们定义了一个数据结构 probe_SSL_data_t 用于在内核态和用户态之间传输数据: #define MAX_BUF_SIZE 8192\n#define TASK_COMM_LEN 16 struct probe_SSL_data_t { __u64 timestamp_ns; // 时间戳(纳秒) __u64 delta_ns; // 函数执行时间 __u32 pid; // 进程 ID __u32 tid; // 线程 ID __u32 uid; // 用户 ID __u32 len; // 读/写数据的长度 int buf_filled; // 缓冲区是否填充完整 int rw; // 读或写(0为读,1为写) char comm[TASK_COMM_LEN]; // 进程名 __u8 buf[MAX_BUF_SIZE]; // 数据缓冲区 int is_handshake; // 是否是握手数据\n};","breadcrumbs":"使用 uprobe 捕获多种库的 SSL/TLS 明文数据 » 数据结构","id":"155","title":"数据结构"},"156":{"body":"我们的目标是 hook 到 SSL_read 和 SSL_write 函数。我们定义了一个函数 SSL_exit 来处理这两个函数的返回值。该函数会根据当前进程和线程的 ID,确定是否需要追踪并收集数据。 static int SSL_exit(struct pt_regs *ctx, int rw) { int ret = 0; u32 zero = 0; u64 pid_tgid = bpf_get_current_pid_tgid(); u32 pid = pid_tgid >> 32; u32 tid = (u32)pid_tgid; u32 uid = bpf_get_current_uid_gid(); u64 ts = bpf_ktime_get_ns(); if (!trace_allowed(uid, pid)) { return 0; } /* store arg info for later lookup */ u64 *bufp = bpf_map_lookup_elem(&bufs, &tid); if (bufp == 0) return 0; u64 *tsp = bpf_map_lookup_elem(&start_ns, &tid); if (!tsp) return 0; u64 delta_ns = ts - *tsp; int len = PT_REGS_RC(ctx); if (len <= 0) // no data return 0; struct probe_SSL_data_t *data = bpf_map_lookup_elem(&ssl_data, &zero); if (!data) return 0; data->timestamp_ns = ts; data->delta_ns = delta_ns; data->pid = pid; data->tid = tid; data->uid = uid; data->len = (u32)len; data->buf_filled = 0; data->rw = rw; data->is_handshake = false; u32 buf_copy_size = min((size_t)MAX_BUF_SIZE, (size_t)len); bpf_get_current_comm(&data->comm, sizeof(data->comm)); if (bufp != 0) ret = bpf_probe_read_user(&data->buf, buf_copy_size, (char *)*bufp); bpf_map_delete_elem(&bufs, &tid); bpf_map_delete_elem(&start_ns, &tid); if (!ret) data->buf_filled = 1; else buf_copy_size = 0; bpf_perf_event_output(ctx, &perf_SSL_events, BPF_F_CURRENT_CPU, data, EVENT_SIZE(buf_copy_size)); return 0;\n} 这里的 rw 参数标识是读还是写。0 代表读,1 代表写。 数据收集流程 获取当前进程和线程的 ID,以及当前用户的 ID。 通过 trace_allowed 判断是否允许追踪该进程。 获取起始时间,以计算函数的执行时间。 尝试从 bufs 和 start_ns maps 中查找相关的数据。 如果成功读取了数据,则创建或查找 probe_SSL_data_t 结构来填充数据。 将数据从用户空间复制到缓冲区,并确保不超过预定的大小。 最后,将数据发送到用户空间。 注意:我们使用了两个用户返回探针 uretprobe 来分别 hook SSL_read 和 SSL_write 的返回: SEC(\"uretprobe/SSL_read\")\nint BPF_URETPROBE(probe_SSL_read_exit) { return (SSL_exit(ctx, 0)); // 0 表示读操作\n} SEC(\"uretprobe/SSL_write\")\nint BPF_URETPROBE(probe_SSL_write_exit) { return (SSL_exit(ctx, 1)); // 1 表示写操作\n}","breadcrumbs":"使用 uprobe 捕获多种库的 SSL/TLS 明文数据 » Hook 函数","id":"156","title":"Hook 函数"},"157":{"body":"在 SSL/TLS 中,握手(handshake)是一个特殊的过程,用于在客户端和服务器之间建立安全的连接。为了分析此过程,我们 hook 到了 do_handshake 函数,以跟踪握手的开始和结束。 进入握手 我们使用 uprobe 为 do_handshake 设置一个 probe: SEC(\"uprobe/do_handshake\")\nint BPF_UPROBE(probe_SSL_do_handshake_enter, void *ssl) { u64 pid_tgid = bpf_get_current_pid_tgid(); u32 pid = pid_tgid >> 32; u32 tid = (u32)pid_tgid; u64 ts = bpf_ktime_get_ns(); u32 uid = bpf_get_current_uid_gid(); if (!trace_allowed(uid, pid)) { return 0; } /* store arg info for later lookup */ bpf_map_update_elem(&start_ns, &tid, &ts, BPF_ANY); return 0;\n} 这段代码的主要功能如下: 获取当前的 pid, tid, ts 和 uid。 使用 trace_allowed 检查进程是否被允许追踪。 将当前时间戳存储在 start_ns 映射中,用于稍后计算握手过程的持续时间。 退出握手 同样,我们为 do_handshake 的返回设置了一个 uretprobe: SEC(\"uretprobe/do_handshake\")\nint BPF_URETPROBE(probe_SSL_do_handshake_exit) { u32 zero = 0; u64 pid_tgid = bpf_get_current_pid_tgid(); u32 pid = pid_tgid >> 32; u32 tid = (u32)pid_tgid; u32 uid = bpf_get_current_uid_gid(); u64 ts = bpf_ktime_get_ns(); int ret = 0; /* use kernel terminology here for tgid/pid: */ u32 tgid = pid_tgid >> 32; /* store arg info for later lookup */ if (!trace_allowed(tgid, pid)) { return 0; } u64 *tsp = bpf_map_lookup_elem(&start_ns, &tid); if (tsp == 0) return 0; ret = PT_REGS_RC(ctx); if (ret <= 0) // handshake failed return 0; struct probe_SSL_data_t *data = bpf_map_lookup_elem(&ssl_data, &zero); if (!data) return 0; data->timestamp_ns = ts; data->delta_ns = ts - *tsp; data->pid = pid; data->tid = tid; data->uid = uid; data->len = ret; data->buf_filled = 0; data->rw = 2; data->is_handshake = true; bpf_get_current_comm(&data->comm, sizeof(data->comm)); bpf_map_delete_elem(&start_ns, &tid); bpf_perf_event_output(ctx, &perf_SSL_events, BPF_F_CURRENT_CPU, data, EVENT_SIZE(0)); return 0;\n} 此函数的逻辑如下: 获取当前的 pid, tid, ts 和 uid。 使用 trace_allowed 再次检查是否允许追踪。 查找 start_ns 映射中的时间戳,用于计算握手的持续时间。 使用 PT_REGS_RC(ctx) 获取 do_handshake 的返回值,判断握手是否成功。 查找或初始化与当前线程关联的 probe_SSL_data_t 数据结构。 更新数据结构的字段,包括时间戳、持续时间、进程信息等。 通过 bpf_perf_event_output 将数据发送到用户态。 我们的 eBPF 代码不仅跟踪了 ssl_read 和 ssl_write 的数据传输,还特别关注了 SSL/TLS 的握手过程。这些信息对于深入了解和优化安全连接的性能至关重要。 通过这些 hook 函数,我们可以获得关于握手成功与否、握手所需的时间以及相关的进程信息的数据。这为我们提供了关于系统 SSL/TLS 行为的深入见解,可以帮助我们在需要时进行更深入的分析和优化。","breadcrumbs":"使用 uprobe 捕获多种库的 SSL/TLS 明文数据 » Hook到握手过程","id":"157","title":"Hook到握手过程"},"158":{"body":"在 eBPF 的生态系统中,用户态和内核态代码经常协同工作。内核态代码负责数据的采集,而用户态代码则负责设置、管理和处理这些数据。在本节中,我们将解读上述用户态代码如何配合 eBPF 追踪 SSL/TLS 交互。","breadcrumbs":"使用 uprobe 捕获多种库的 SSL/TLS 明文数据 » 用户态辅助代码分析与解读","id":"158","title":"用户态辅助代码分析与解读"},"159":{"body":"上述代码片段中,根据环境变量 env 的设定,程序可以选择针对三种常见的加密库(OpenSSL、GnuTLS 和 NSS)进行挂载。这意味着我们可以在同一个工具中对多种库的调用进行追踪。 为了实现这一功能,首先利用 find_library_path 函数确定库的路径。然后,根据库的类型,调用对应的 attach_ 函数来将 eBPF 程序挂载到库函数上。 if (env.openssl) { char *openssl_path = find_library_path(\"libssl.so\"); printf(\"OpenSSL path: %s\\n\", openssl_path); attach_openssl(obj, \"/lib/x86_64-linux-gnu/libssl.so.3\"); } if (env.gnutls) { char *gnutls_path = find_library_path(\"libgnutls.so\"); printf(\"GnuTLS path: %s\\n\", gnutls_path); attach_gnutls(obj, gnutls_path); } if (env.nss) { char *nss_path = find_library_path(\"libnspr4.so\"); printf(\"NSS path: %s\\n\", nss_path); attach_nss(obj, nss_path); } 这里主要包含 OpenSSL、GnuTLS 和 NSS 三个库的挂载逻辑。NSS 是为组织设计的一套安全库,支持创建安全的客户端和服务器应用程序。它们最初是由 Netscape 开发的,现在由 Mozilla 维护。其他两个库前面已经介绍过了,这里不再赘述。","breadcrumbs":"使用 uprobe 捕获多种库的 SSL/TLS 明文数据 » 1. 支持的库挂载","id":"159","title":"1. 支持的库挂载"},"16":{"body":"要开发eBPF程序,您需要安装以下软件和工具: Linux 内核:由于eBPF是内核技术,因此您需要具备较新版本的Linux内核(推荐4.8及以上版本),以支持eBPF功能。 LLVM 和 Clang:这些工具用于编译eBPF程序。安装最新版本的LLVM和Clang可以确保您获得最佳的eBPF支持。 eBPF 程序主要由两部分构成:内核态部分和用户态部分。内核态部分包含 eBPF 程序的实际逻辑,用户态部分负责加载、运行和监控内核态程序。 当您选择了合适的开发框架后,如BCC(BPF Compiler Collection)、libbpf、cilium/ebpf或eunomia-bpf等,您可以开始进行用户态和内核态程序的开发。以BCC工具为例,我们将介绍eBPF程序的基本开发流程: 安装BCC工具:根据您的Linux发行版,按照BCC官方文档的指南安装BCC工具和相关依赖。 编写eBPF程序(C语言):使用C语言编写一个简单的eBPF程序,例如Hello World程序。该程序可以在内核空间执行并完成特定任务,如统计网络数据包数量。 编写用户态程序(Python或C等):使用Python、C等语言编写用户态程序,用于加载、运行eBPF程序以及与之交互。在这个程序中,您需要使用BCC提供的API来加载和操作内核态的eBPF程序。 编译eBPF程序:使用BCC工具,将C语言编写的eBPF程序编译成内核可以执行的字节码。BCC会在运行时动态从源码编译eBPF程序。 加载并运行eBPF程序:在用户态程序中,使用BCC提供的API加载编译好的eBPF程序到内核空间,然后运行该程序。 与eBPF程序交互:用户态程序通过BCC提供的API与eBPF程序交互,实现数据收集、分析和展示等功能。例如,您可以使用BCC API读取eBPF程序中的map数据,以获取网络数据包统计信息。 卸载eBPF程序:当不再需要eBPF程序时,用户态程序应使用BCC API将其从内核空间卸载。 调试与优化:使用 bpftool 等工具进行eBPF程序的调试和优化,提高程序性能和稳定性。 通过以上流程,您可以使用BCC工具开发、编译、运行和调试eBPF程序。请注意,其他框架(如libbpf、cilium/ebpf和eunomia-bpf)的开发流程大致相似但略有不同,因此在选择框架时,请参考相应的官方文档和示例。 通过这个过程,你可以开发出一个能够在内核中运行的 eBPF 程序。eunomia-bpf 是一个开源的 eBPF 动态加载运行时和开发工具链,它的目的是简化 eBPF 程序的开发、构建、分发、运行。它基于 libbpf 的 CO-RE 轻量级开发框架,支持通过用户态 WASM 虚拟机控制 eBPF 程序的加载和执行,并将预编译的 eBPF 程序打包为通用的 JSON 或 WASM 模块进行分发。我们会使用 eunomia-bpf 进行演示。","breadcrumbs":"eBPF Hello World,基本框架和开发流程 » 安装必要的软件和工具","id":"16","title":"安装必要的软件和工具"},"160":{"body":"具体的 attach 函数如下: #define __ATTACH_UPROBE(skel, binary_path, sym_name, prog_name, is_retprobe) \\ do { \\ LIBBPF_OPTS(bpf_uprobe_opts, uprobe_opts, .func_name = #sym_name, \\ .retprobe = is_retprobe); \\ skel->links.prog_name = bpf_program__attach_uprobe_opts( \\ skel->progs.prog_name, env.pid, binary_path, 0, &uprobe_opts); \\ } while (false) int attach_openssl(struct sslsniff_bpf *skel, const char *lib) { ATTACH_UPROBE_CHECKED(skel, lib, SSL_write, probe_SSL_rw_enter); ATTACH_URETPROBE_CHECKED(skel, lib, SSL_write, probe_SSL_write_exit); ATTACH_UPROBE_CHECKED(skel, lib, SSL_read, probe_SSL_rw_enter); ATTACH_URETPROBE_CHECKED(skel, lib, SSL_read, probe_SSL_read_exit); if (env.latency && env.handshake) { ATTACH_UPROBE_CHECKED(skel, lib, SSL_do_handshake, probe_SSL_do_handshake_enter); ATTACH_URETPROBE_CHECKED(skel, lib, SSL_do_handshake, probe_SSL_do_handshake_exit); } return 0;\n} int attach_gnutls(struct sslsniff_bpf *skel, const char *lib) { ATTACH_UPROBE_CHECKED(skel, lib, gnutls_record_send, probe_SSL_rw_enter); ATTACH_URETPROBE_CHECKED(skel, lib, gnutls_record_send, probe_SSL_write_exit); ATTACH_UPROBE_CHECKED(skel, lib, gnutls_record_recv, probe_SSL_rw_enter); ATTACH_URETPROBE_CHECKED(skel, lib, gnutls_record_recv, probe_SSL_read_exit); return 0;\n} int attach_nss(struct sslsniff_bpf *skel, const char *lib) { ATTACH_UPROBE_CHECKED(skel, lib, PR_Write, probe_SSL_rw_enter); ATTACH_URETPROBE_CHECKED(skel, lib, PR_Write, probe_SSL_write_exit); ATTACH_UPROBE_CHECKED(skel, lib, PR_Send, probe_SSL_rw_enter); ATTACH_URETPROBE_CHECKED(skel, lib, PR_Send, probe_SSL_write_exit); ATTACH_UPROBE_CHECKED(skel, lib, PR_Read, probe_SSL_rw_enter); ATTACH_URETPROBE_CHECKED(skel, lib, PR_Read, probe_SSL_read_exit); ATTACH_UPROBE_CHECKED(skel, lib, PR_Recv, probe_SSL_rw_enter); ATTACH_URETPROBE_CHECKED(skel, lib, PR_Recv, probe_SSL_read_exit); return 0;\n} 我们进一步观察 attach_ 函数,可以看到它们都使用了 ATTACH_UPROBE_CHECKED 和 ATTACH_URETPROBE_CHECKED 宏来实现具体的挂载逻辑。这两个宏分别用于设置 uprobe(函数入口)和 uretprobe(函数返回)。 考虑到不同的库有不同的 API 函数名称(例如,OpenSSL 使用 SSL_write,而 GnuTLS 使用 gnutls_record_send),所以我们需要为每个库写一个独立的 attach_ 函数。 例如,在 attach_openssl 函数中,我们为 SSL_write 和 SSL_read 设置了 probe。如果用户还希望追踪握手的延迟 (env.latency) 和握手过程 (env.handshake),那么我们还会为 SSL_do_handshake 设置 probe。 在eBPF生态系统中,perf_buffer是一个用于从内核态传输数据到用户态的高效机制。这对于内核态eBPF程序来说是十分有用的,因为它们不能直接与用户态进行交互。使用perf_buffer,我们可以在内核态eBPF程序中收集数据,然后在用户态异步地读取这些数据。我们使用 perf_buffer__poll 函数来读取内核态上报的数据,如下所示: while (!exiting) { err = perf_buffer__poll(pb, PERF_POLL_TIMEOUT_MS); if (err < 0 && err != -EINTR) { warn(\"error polling perf buffer: %s\\n\", strerror(-err)); goto cleanup; } err = 0; } 最后,在 print_event 函数中,我们将数据打印到标准输出: // Function to print the event from the perf buffer\nvoid print_event(struct probe_SSL_data_t *event, const char *evt) { ... if (buf_size != 0) { if (env.hexdump) { // 2 characters for each byte + null terminator char hex_data[MAX_BUF_SIZE * 2 + 1] = {0}; buf_to_hex((uint8_t *)buf, buf_size, hex_data); printf(\"\\n%s\\n\", s_mark); for (size_t i = 0; i < strlen(hex_data); i += 32) { printf(\"%.32s\\n\", hex_data + i); } printf(\"%s\\n\\n\", e_mark); } else { printf(\"\\n%s\\n%s\\n%s\\n\\n\", s_mark, buf, e_mark); } }\n} 完整的源代码可以在这里查看: https://github.com/eunomia-bpf/bpf-developer-tutorial/tree/main/src/30-sslsniff","breadcrumbs":"使用 uprobe 捕获多种库的 SSL/TLS 明文数据 » 2. 详细挂载逻辑","id":"160","title":"2. 详细挂载逻辑"},"161":{"body":"要开始使用 sslsniff,首先要进行编译: make 完成后,请按照以下步骤操作:","breadcrumbs":"使用 uprobe 捕获多种库的 SSL/TLS 明文数据 » 编译与运行","id":"161","title":"编译与运行"},"162":{"body":"在一个终端中,执行以下命令来启动 sslsniff: sudo ./sslsniff","breadcrumbs":"使用 uprobe 捕获多种库的 SSL/TLS 明文数据 » 启动 sslsniff","id":"162","title":"启动 sslsniff"},"163":{"body":"在另一个终端中,执行: curl https://example.com 正常情况下,你会看到类似以下的输出: Example Domain ... ...
","breadcrumbs":"使用 uprobe 捕获多种库的 SSL/TLS 明文数据 » 执行 CURL 命令","id":"163","title":"执行 CURL 命令"},"164":{"body":"当执行 curl 命令后,sslsniff 会显示以下内容: READ/RECV 0.132786160 curl 47458 1256 ----- DATA ----- ...
Example Domain
... ----- END DATA ----- 注意 :显示的 HTML 内容可能会因 example.com 页面的不同而有所不同。","breadcrumbs":"使用 uprobe 捕获多种库的 SSL/TLS 明文数据 » sslsniff 输出","id":"164","title":"sslsniff 输出"},"165":{"body":"要查看延迟和握手过程,请执行以下命令: $ sudo ./sslsniff -l --handshake\nOpenSSL path: /lib/x86_64-linux-gnu/libssl.so.3\nGnuTLS path: /lib/x86_64-linux-gnu/libgnutls.so.30\nNSS path: /lib/x86_64-linux-gnu/libnspr4.so\nFUNC TIME(s) COMM PID LEN LAT(ms)\nHANDSHAKE 0.000000000 curl 6460 1 1.384 WRITE/SEND 0.000115400 curl 6460 24 0.014","breadcrumbs":"使用 uprobe 捕获多种库的 SSL/TLS 明文数据 » 显示延迟和握手过程","id":"165","title":"显示延迟和握手过程"},"166":{"body":"要以16进制格式显示数据,请执行以下命令: $ sudo ./sslsniff --hexdump\nWRITE/SEND 0.000000000 curl 16104 24 ----- DATA -----\n505249202a20485454502f322e300d0a\n0d0a534d0d0a0d0a\n----- END DATA ----- ...","breadcrumbs":"使用 uprobe 捕获多种库的 SSL/TLS 明文数据 » 16进制输出","id":"166","title":"16进制输出"},"167":{"body":"eBPF 是一个非常强大的技术,它可以帮助我们深入了解系统的工作原理。本教程是一个简单的示例,展示了如何使用 eBPF 来监控 SSL/TLS 通信。如果您对 eBPF 技术感兴趣,并希望进一步了解和实践,可以访问我们的教程代码仓库 https://github.com/eunomia-bpf/bpf-developer-tutorial 和教程网站 https://eunomia.dev/zh/tutorials/。 参考资料: https://github.com/iovisor/bcc/pull/4706 https://github.com/openssl/openssl https://www.openssl.org/docs/man1.1.1/man3/SSL_read.html https://github.com/iovisor/bcc/blob/master/tools/sslsniff_example.txt https://en.wikipedia.org/wiki/Transport_Layer_Security","breadcrumbs":"使用 uprobe 捕获多种库的 SSL/TLS 明文数据 » 总结","id":"167","title":"总结"},"168":{"body":"","breadcrumbs":"使用 sockops 加速网络请求转发 » eBPF sockops 示例","id":"168","title":"eBPF sockops 示例"},"169":{"body":"网络连接本质上是 socket 之间的通讯,eBPF 提供了一个 bpf_msg_redirect_hash 函数,用来将应用发出的包直接转发到对端的 socket,可以极大地加速包在内核中的处理流程。 这里 sock_map 是记录 socket 规则的关键部分,即根据当前的数据包信息,从 sock_map 中挑选一个存在的 socket 连接来转发请求。所以需要先在 sockops 的 hook 处或者其它地方,将 socket 信息保存到 sock_map,并提供一个规则 (一般为四元组) 根据 key 查找到 socket。 Merbridge 项目就是这样实现了用 eBPF 代替 iptables 为 Istio 进行加速。在使用 Merbridge (eBPF) 优化之后,出入口流量会直接跳过很多内核模块,明显提高性能,如下图所示: merbridge","breadcrumbs":"使用 sockops 加速网络请求转发 » 利用 eBPF 的 sockops 进行性能优化","id":"169","title":"利用 eBPF 的 sockops 进行性能优化"},"17":{"body":"可以通过以下步骤下载和安装 eunomia-bpf: 下载 ecli 工具,用于运行 eBPF 程序: $ wget https://aka.pw/bpf-ecli -O ecli && chmod +x ./ecli\n$ ./ecli -h\nUsage: ecli [--help] [--version] [--json] [--no-cache] url-and-args 下载编译器工具链,用于将 eBPF 内核代码编译为 config 文件或 WASM 模块: $ wget https://github.com/eunomia-bpf/eunomia-bpf/releases/latest/download/ecc && chmod +x ./ecc\n$ ./ecc -h\neunomia-bpf compiler\nUsage: ecc [OPTIONS] [EXPORT_EVENT_HEADER]\n.... 也可以使用 docker 镜像进行编译: $ docker run -it -v `pwd`/:/src/ ghcr.io/eunomia-bpf/ecc-`uname -m`:latest # 使用 docker 进行编译。`pwd` 应该包含 *.bpf.c 文件和 *.h 文件。\nexport PATH=PATH:~/.eunomia/bin\nCompiling bpf object...\nPacking ebpf object and config into /src/package.json...","breadcrumbs":"eBPF Hello World,基本框架和开发流程 » 下载安装 eunomia-bpf 开发工具","id":"17","title":"下载安装 eunomia-bpf 开发工具"},"170":{"body":"此示例程序从发送者的套接字(出口)重定向流量至接收者的套接字(入口), 跳过 TCP/IP 内核网络栈 。在这个示例中,我们假定发送者和接收者都在 同一台 机器上运行。","breadcrumbs":"使用 sockops 加速网络请求转发 » 运行样例","id":"170","title":"运行样例"},"171":{"body":"# Compile the bpf_sockops program\nclang -O2 -g -Wall -target bpf -c bpf_sockops.c -o bpf_sockops.o\nclang -O2 -g -Wall -target bpf -c bpf_redir.c -o bpf_redir.o","breadcrumbs":"使用 sockops 加速网络请求转发 » 编译 eBPF 程序","id":"171","title":"编译 eBPF 程序"},"172":{"body":"sudo ./load.sh 您可以使用 bpftool utility 检查这两个 eBPF 程序是否已经加载。 $ sudo bpftool prog show\n63: sock_ops name bpf_sockmap tag 275467be1d69253d gpl loaded_at 2019-01-24T13:07:17+0200 uid 0 xlated 1232B jited 750B memlock 4096B map_ids 58\n64: sk_msg name bpf_redir tag bc78074aa9dd96f4 gpl loaded_at 2019-01-24T13:07:17+0200 uid 0 xlated 304B jited 233B memlock 4096B map_ids 58","breadcrumbs":"使用 sockops 加速网络请求转发 » 加载 eBPF 程序","id":"172","title":"加载 eBPF 程序"},"173":{"body":"iperf3 -s -p 10000","breadcrumbs":"使用 sockops 加速网络请求转发 » 运行 iperf3 服务器","id":"173","title":"运行 iperf3 服务器"},"174":{"body":"iperf3 -c 127.0.0.1 -t 10 -l 64k -p 10000","breadcrumbs":"使用 sockops 加速网络请求转发 » 运行 iperf3 客户端","id":"174","title":"运行 iperf3 客户端"},"175":{"body":"$ ./trace.sh\niperf3-9516 [001] .... 22500.634108: 0: <<< ipv4 op = 4, port 18583 --> 4135\niperf3-9516 [001] ..s1 22500.634137: 0: <<< ipv4 op = 5, port 4135 --> 18583\niperf3-9516 [001] .... 22500.634523: 0: <<< ipv4 op = 4, port 19095 --> 4135\niperf3-9516 [001] ..s1 22500.634536: 0: <<< ipv4 op = 5, port 4135 --> 19095 你应该可以看到 4 个用于套接字建立的事件。如果你没有看到任何事件,那么 eBPF 程序可能没有正确地附加上。","breadcrumbs":"使用 sockops 加速网络请求转发 » 收集追踪","id":"175","title":"收集追踪"},"176":{"body":"sudo ./unload.sh","breadcrumbs":"使用 sockops 加速网络请求转发 » 卸载 eBPF 程序","id":"176","title":"卸载 eBPF 程序"},"177":{"body":"https://github.com/zachidan/ebpf-sockops https://github.com/merbridge/merbridge","breadcrumbs":"使用 sockops 加速网络请求转发 » 参考资料","id":"177","title":"参考资料"},"178":{"body":"eBPF(扩展的伯克利数据包过滤器)是 Linux 内核中的一个强大功能,可以在无需更改内核源代码或重启内核的情况下,运行、加载和更新用户定义的代码。这种功能让 eBPF 在网络和系统性能分析、数据包过滤、安全策略等方面有了广泛的应用。 在本篇教程中,我们将展示如何利用 eBPF 来隐藏进程或文件信息,这是网络安全和防御领域中一种常见的技术。","breadcrumbs":"使用 eBPF 隐藏进程或文件信息 » eBPF 开发实践:使用 eBPF 隐藏进程或文件信息","id":"178","title":"eBPF 开发实践:使用 eBPF 隐藏进程或文件信息"},"179":{"body":"\"进程隐藏\" 能让特定的进程对操作系统的常规检测机制变得不可见。在黑客攻击或系统防御的场景中,这种技术都可能被应用。具体来说,Linux 系统中每个进程都在 /proc/ 目录下有一个以其进程 ID 命名的子文件夹,包含了该进程的各种信息。ps 命令就是通过查找这些文件夹来显示进程信息的。因此,如果我们能隐藏某个进程的 /proc/ 文件夹,就能让这个进程对 ps 命令等检测手段“隐身”。 要实现进程隐藏,关键在于操作 /proc/ 目录。在 Linux 中,getdents64 系统调用可以读取目录下的文件信息。我们可以通过挂接这个系统调用,修改它返回的结果,从而达到隐藏文件的目的。实现这个功能需要使用到 eBPF 的 bpf_probe_write_user 功能,它可以修改用户空间的内存,因此能用来修改 getdents64 返回的结果。 下面,我们会详细介绍如何在内核态和用户态编写 eBPF 程序来实现进程隐藏。","breadcrumbs":"使用 eBPF 隐藏进程或文件信息 » 背景知识与实现机制","id":"179","title":"背景知识与实现机制"},"18":{"body":"我们会先从一个简单的 eBPF 程序开始,它会在内核中打印一条消息。我们会使用 eunomia-bpf 的编译器工具链将其编译为 bpf 字节码文件,然后使用 ecli 工具加载并运行该程序。作为示例,我们可以暂时省略用户态程序的部分。 /* SPDX-License-Identifier: (LGPL-2.1 OR BSD-2-Clause) */\n#define BPF_NO_GLOBAL_DATA\n#include \n#include \n#include typedef unsigned int u32;\ntypedef int pid_t;\nconst pid_t pid_filter = 0; char LICENSE[] SEC(\"license\") = \"Dual BSD/GPL\"; SEC(\"tp/syscalls/sys_enter_write\")\nint handle_tp(void *ctx)\n{ pid_t pid = bpf_get_current_pid_tgid() >> 32; if (pid_filter && pid != pid_filter) return 0; bpf_printk(\"BPF triggered sys_enter_write from PID %d.\\n\", pid); return 0;\n} 这段程序通过定义一个 handle_tp 函数并使用 SEC 宏把它附加到 sys_enter_write tracepoint(即在进入 write 系统调用时执行)。该函数通过使用 bpf_get_current_pid_tgid 和 bpf_printk 函数获取调用 write 系统调用的进程 ID,并在内核日志中打印出来。 bpf_trace_printk(): 一种将信息输出到trace_pipe(/sys/kernel/debug/tracing/trace_pipe)简单机制。 在一些简单用例中这样使用没有问题, but它也有一些限制:最多3 参数; 第一个参数必须是%s(即字符串);同时trace_pipe在内核中全局共享,其他并行使用trace_pipe的程序有可能会将 trace_pipe 的输出扰乱。 一个更好的方式是通过 BPF_PERF_OUTPUT(), 稍后将会讲到。 void *ctx:ctx本来是具体类型的参数, 但是由于我们这里没有使用这个参数,因此就将其写成void *类型。 return 0;:必须这样,返回0 (如果要知道why, 参考 #139 https://github.com/iovisor/bcc/issues/139 )。 要编译和运行这段程序,可以使用 ecc 工具和 ecli 命令。首先在 Ubuntu/Debian 上,执行以下命令: sudo apt install clang llvm 使用 ecc 编译程序: $ ./ecc minimal.bpf.c\nCompiling bpf object...\nPacking ebpf object and config into package.json... 或使用 docker 镜像进行编译: docker run -it -v `pwd`/:/src/ ghcr.io/eunomia-bpf/ecc-`uname -m`:latest 然后使用 ecli 运行编译后的程序: $ sudo ./ecli run package.json\nRuning eBPF program... 运行这段程序后,可以通过查看 /sys/kernel/debug/tracing/trace_pipe 文件来查看 eBPF 程序的输出: $ sudo cat /sys/kernel/debug/tracing/trace_pipe | grep \"BPF triggered sys_enter_write\" <...>-3840345 [010] d... 3220701.101143: bpf_trace_printk: write system call from PID 3840345. <...>-3840345 [010] d... 3220701.101143: bpf_trace_printk: write system call from PID 3840345. 按 Ctrl+C 停止 ecli 进程之后,可以看到对应的输出也停止。 注意:如果正在使用的 Linux 发行版(例如 Ubuntu )默认情况下没有启用跟踪子系统可能看不到任何输出,使用以下指令打开这个功能: $ sudo su\n# echo 1 > /sys/kernel/debug/tracing/tracing_on","breadcrumbs":"eBPF Hello World,基本框架和开发流程 » Hello World - minimal eBPF program","id":"18","title":"Hello World - minimal eBPF program"},"180":{"body":"接下来,我们将详细介绍如何在内核态编写 eBPF 程序来实现进程隐藏。首先是 eBPF 程序的起始部分: // SPDX-License-Identifier: BSD-3-Clause\n#include \"vmlinux.h\"\n#include \n#include \n#include \n#include \"common.h\" char LICENSE[] SEC(\"license\") = \"Dual BSD/GPL\"; // Ringbuffer Map to pass messages from kernel to user\nstruct { __uint(type, BPF_MAP_TYPE_RINGBUF); __uint(max_entries, 256 * 1024);\n} rb SEC(\".maps\"); // Map to fold the dents buffer addresses\nstruct { __uint(type, BPF_MAP_TYPE_HASH); __uint(max_entries, 8192); __type(key, size_t); __type(value, long unsigned int);\n} map_buffs SEC(\".maps\"); // Map used to enable searching through the\n// data in a loop\nstruct { __uint(type, BPF_MAP_TYPE_HASH); __uint(max_entries, 8192); __type(key, size_t); __type(value, int);\n} map_bytes_read SEC(\".maps\"); // Map with address of actual\nstruct { __uint(type, BPF_MAP_TYPE_HASH); __uint(max_entries, 8192); __type(key, size_t); __type(value, long unsigned int);\n} map_to_patch SEC(\".maps\"); // Map to hold program tail calls\nstruct { __uint(type, BPF_MAP_TYPE_PROG_ARRAY); __uint(max_entries, 5); __type(key, __u32); __type(value, __u32);\n} map_prog_array SEC(\".maps\"); 我们首先需要理解这个 eBPF 程序的基本构成和使用到的几个重要组件。前几行引用了几个重要的头文件,如 \"vmlinux.h\"、\"bpf_helpers.h\"、\"bpf_tracing.h\" 和 \"bpf_core_read.h\"。这些文件提供了 eBPF 编程所需的基础设施和一些重要的函数或宏。 \"vmlinux.h\" 是一个包含了完整的内核数据结构的头文件,是从 vmlinux 内核二进制中提取的。使用这个头文件,eBPF 程序可以访问内核的数据结构。 \"bpf_helpers.h\" 头文件中定义了一系列的宏,这些宏是 eBPF 程序使用的 BPF 助手(helper)函数的封装。这些 BPF 助手函数是 eBPF 程序和内核交互的主要方式。 \"bpf_tracing.h\" 是用于跟踪事件的头文件,它包含了许多宏和函数,这些都是为了简化 eBPF 程序对跟踪点(tracepoint)的操作。 \"bpf_core_read.h\" 头文件提供了一组用于从内核读取数据的宏和函数。 程序中定义了一系列的 map 结构,这些 map 是 eBPF 程序中的主要数据结构,它们用于在内核态和用户态之间共享数据,或者在 eBPF 程序中存储和传递数据。 其中,\"rb\" 是一个 Ringbuffer 类型的 map,它用于从内核向用户态传递消息。Ringbuffer 是一种能在内核和用户态之间高效传递大量数据的数据结构。 \"map_buffs\" 是一个 Hash 类型的 map,它用于存储目录项(dentry)的缓冲区地址。 \"map_bytes_read\" 是另一个 Hash 类型的 map,它用于在数据循环中启用搜索。 \"map_to_patch\" 是另一个 Hash 类型的 map,存储了需要被修改的目录项(dentry)的地址。 \"map_prog_array\" 是一个 Prog Array 类型的 map,它用于保存程序的尾部调用。 程序中的 \"target_ppid\" 和 \"pid_to_hide_len\"、\"pid_to_hide\" 是几个重要的全局变量,它们分别存储了目标父进程的 PID、需要隐藏的 PID 的长度以及需要隐藏的 PID。 接下来的代码部分,程序定义了一个名为 \"linux_dirent64\" 的结构体,这个结构体代表一个 Linux 目录项。然后程序定义了两个函数,\"handle_getdents_enter\" 和 \"handle_getdents_exit\",这两个函数分别在 getdents64 系统调用的入口和出口被调用,用于实现对目录项的操作。 // Optional Target Parent PID\nconst volatile int target_ppid = 0; // These store the string represenation\n// of the PID to hide. This becomes the name\n// of the folder in /proc/\nconst volatile int pid_to_hide_len = 0;\nconst volatile char pid_to_hide[max_pid_len]; // struct linux_dirent64 {\n// u64 d_ino; /* 64-bit inode number */\n// u64 d_off; /* 64-bit offset to next structure */\n// unsigned short d_reclen; /* Size of this dirent */\n// unsigned char d_type; /* File type */\n// char d_name[]; /* Filename (null-terminated) */ }; // int getdents64(unsigned int fd, struct linux_dirent64 *dirp, unsigned int count);\nSEC(\"tp/syscalls/sys_enter_getdents64\")\nint handle_getdents_enter(struct trace_event_raw_sys_enter *ctx)\n{ size_t pid_tgid = bpf_get_current_pid_tgid(); // Check if we're a process thread of interest // if target_ppid is 0 then we target all pids if (target_ppid != 0) { struct task_struct *task = (struct task_struct *)bpf_get_current_task(); int ppid = BPF_CORE_READ(task, real_parent, tgid); if (ppid != target_ppid) { return 0; } } int pid = pid_tgid >> 32; unsigned int fd = ctx->args[0]; unsigned int buff_count = ctx->args[2]; // Store params in map for exit function struct linux_dirent64 *dirp = (struct linux_dirent64 *)ctx->args[1]; bpf_map_update_elem(&map_buffs, &pid_tgid, &dirp, BPF_ANY); return 0;\n} 在这部分代码中,我们可以看到 eBPF 程序的一部分具体实现,该程序负责在 getdents64 系统调用的入口处进行处理。 我们首先声明了几个全局的变量。其中 target_ppid 代表我们要关注的目标父进程的 PID。如果这个值为 0,那么我们将关注所有的进程。pid_to_hide_len 和 pid_to_hide 则分别用来存储我们要隐藏的进程的 PID 的长度和 PID 本身。这个 PID 会转化成 /proc/ 目录下的一个文件夹的名称,因此被隐藏的进程在 /proc/ 目录下将无法被看到。 接下来,我们声明了一个名为 linux_dirent64 的结构体。这个结构体代表一个 Linux 目录项,包含了一些元数据,如 inode 号、下一个目录项的偏移、当前目录项的长度、文件类型以及文件名。 然后是 getdents64 函数的原型。这个函数是 Linux 系统调用,用于读取一个目录的内容。我们的目标就是在这个函数执行的过程中,对目录项进行修改,以实现进程隐藏。 随后的部分是 eBPF 程序的具体实现。我们在 getdents64 系统调用的入口处定义了一个名为 handle_getdents_enter 的函数。这个函数首先获取了当前进程的 PID 和线程组 ID,然后检查这个进程是否是我们关注的进程。如果我们设置了 target_ppid,那么我们就只关注那些父进程的 PID 为 target_ppid 的进程。如果 target_ppid 为 0,我们就关注所有进程。 在确认了当前进程是我们关注的进程之后,我们将 getdents64 系统调用的参数保存到一个 map 中,以便在系统调用返回时使用。我们特别关注 getdents64 系统调用的第二个参数,它是一个指向 linux_dirent64 结构体的指针,代表了系统调用要读取的目录的内容。我们将这个指针以及当前的 PID 和线程组 ID 作为键值对保存到 map_buffs 这个 map 中。 至此,我们完成了 getdents64 系统调用入口处的处理。在系统调用返回时,我们将会在 handle_getdents_exit 函数中,对目录项进行修改,以实现进程隐藏。 在接下来的代码段中,我们将要实现在 getdents64 系统调用返回时的处理。我们主要的目标就是找到我们想要隐藏的进程,并且对目录项进行修改以实现隐藏。 我们首先定义了一个名为 handle_getdents_exit 的函数,它将在 getdents64 系统调用返回时被调用。 SEC(\"tp/syscalls/sys_exit_getdents64\")\nint handle_getdents_exit(struct trace_event_raw_sys_exit *ctx)\n{ size_t pid_tgid = bpf_get_current_pid_tgid(); int total_bytes_read = ctx->ret; // if bytes_read is 0, everything's been read if (total_bytes_read <= 0) { return 0; } // Check we stored the address of the buffer from the syscall entry long unsigned int* pbuff_addr = bpf_map_lookup_elem(&map_buffs, &pid_tgid); if (pbuff_addr == 0) { return 0; } // All of this is quite complex, but basically boils down to // Calling 'handle_getdents_exit' in a loop to iterate over the file listing // in chunks of 200, and seeing if a folder with the name of our pid is in there. // If we find it, use 'bpf_tail_call' to jump to handle_getdents_patch to do the actual // patching long unsigned int buff_addr = *pbuff_addr; struct linux_dirent64 *dirp = 0; int pid = pid_tgid >> 32; short unsigned int d_reclen = 0; char filename[max_pid_len]; unsigned int bpos = 0; unsigned int *pBPOS = bpf_map_lookup_elem(&map_bytes_read, &pid_tgid); if (pBPOS != 0) { bpos = *pBPOS; } for (int i = 0; i < 200; i ++) { if (bpos >= total_bytes_read) { break; } dirp = (struct linux_dirent64 *)(buff_addr+bpos); bpf_probe_read_user(&d_reclen, sizeof(d_reclen), &dirp->d_reclen); bpf_probe_read_user_str(&filename, pid_to_hide_len, dirp->d_name); int j = 0; for (j = 0; j < pid_to_hide_len; j++) { if (filename[j] != pid_to_hide[j]) { break; } } if (j == pid_to_hide_len) { // *********** // We've found the folder!!! // Jump to handle_getdents_patch so we can remove it! // *********** bpf_map_delete_elem(&map_bytes_read, &pid_tgid); bpf_map_delete_elem(&map_buffs, &pid_tgid); bpf_tail_call(ctx, &map_prog_array, PROG_02); } bpf_map_update_elem(&map_to_patch, &pid_tgid, &dirp, BPF_ANY); bpos += d_reclen; } // If we didn't find it, but there's still more to read, // jump back the start of this function and keep looking if (bpos < total_bytes_read) { bpf_map_update_elem(&map_bytes_read, &pid_tgid, &bpos, BPF_ANY); bpf_tail_call(ctx, &map_prog_array, PROG_01); } bpf_map_delete_elem(&map_bytes_read, &pid_tgid); bpf_map_delete_elem(&map_buffs, &pid_tgid); return 0;\n} 在这个函数中,我们首先获取了当前进程的 PID 和线程组 ID,然后检查系统调用是否读取到了目录的内容。如果没有读取到内容,我们就直接返回。 然后我们从 map_buffs 这个 map 中获取 getdents64 系统调用入口处保存的目录内容的地址。如果我们没有保存过这个地址,那么就没有必要进行进一步的处理。 接下来的部分有点复杂,我们用了一个循环来迭代读取目录的内容,并且检查是否有我们想要隐藏的进程的 PID。如果我们找到了,我们就用 bpf_tail_call 函数跳转到 handle_getdents_patch 函数,进行实际的隐藏操作。 SEC(\"tp/syscalls/sys_exit_getdents64\")\nint handle_getdents_patch(struct trace_event_raw_sys_exit *ctx)\n{ // Only patch if we've already checked and found our pid's folder to hide size_t pid_tgid = bpf_get_current_pid_tgid(); long unsigned int* pbuff_addr = bpf_map_lookup_elem(&map_to_patch, &pid_tgid); if (pbuff_addr == 0) { return 0; } // Unlink target, by reading in previous linux_dirent64 struct, // and setting it's d_reclen to cover itself and our target. // This will make the program skip over our folder. long unsigned int buff_addr = *pbuff_addr; struct linux_dirent64 *dirp_previous = (struct linux_dirent64 *)buff_addr; short unsigned int d_reclen_previous = 0; bpf_probe_read_user(&d_reclen_previous, sizeof(d_reclen_previous), &dirp_previous->d_reclen); struct linux_dirent64 *dirp = (struct linux_dirent64 *)(buff_addr+d_reclen_previous); short unsigned int d_reclen = 0; bpf_probe_read_user(&d_reclen, sizeof(d_reclen), &dirp->d_reclen); // Debug print char filename[max_pid_len]; bpf_probe_read_user_str(&filename, pid_to_hide_len, dirp_previous->d_name); filename[pid_to_hide_len-1] = 0x00; bpf_printk(\"[PID_HIDE] filename previous %s\\n\", filename); bpf_probe_read_user_str(&filename, pid_to_hide_len, dirp->d_name); filename[pid_to_hide_len-1] = 0x00; bpf_printk(\"[PID_HIDE] filename next one %s\\n\", filename); // Attempt to overwrite short unsigned int d_reclen_new = d_reclen_previous + d_reclen; long ret = bpf_probe_write_user(&dirp_previous->d_reclen, &d_reclen_new, sizeof(d_reclen_new)); // Send an event struct event *e; e = bpf_ringbuf_reserve(&rb, sizeof(*e), 0); if (e) { e->success = (ret == 0); e->pid = (pid_tgid >> 32); bpf_get_current_comm(&e->comm, sizeof(e->comm)); bpf_ringbuf_submit(e, 0); } bpf_map_delete_elem(&map_to_patch, &pid_tgid); return 0;\n} 在 handle_getdents_patch 函数中,我们首先检查我们是否已经找到了我们想要隐藏的进程的 PID。然后我们读取目录项的内容,并且修改 d_reclen 字段,让它覆盖下一个目录项,这样就可以隐藏我们的目标进程了。 在这个过程中,我们用到了 bpf_probe_read_user、bpf_probe_read_user_str、bpf_probe_write_user 这几个函数来读取和写入用户空间的数据。这是因为在内核空间,我们不能直接访问用户空间的数据,必须使用这些特殊的函数。 在我们完成隐藏操作后,我们会向一个名为 rb 的环形缓冲区发送一个事件,表示我们已经成功地隐藏了一个进程。我们用 bpf_ringbuf_reserve 函数来预留缓冲区空间,然后将事件的数据填充到这个空间,并最后用 bpf_ringbuf_submit 函数将事件提交到缓冲区。 最后,我们清理了之前保存在 map 中的数据,并返回。 这段代码是在 eBPF 环境下实现进程隐藏的一个很好的例子。通过这个例子,我们可以看到 eBPF 提供的丰富的功能,如系统调用跟踪、map 存储、用户空间数据访问、尾调用等。这些功能使得我们能够在内核空间实现复杂的逻辑,而不需要修改内核代码。","breadcrumbs":"使用 eBPF 隐藏进程或文件信息 » 内核态 eBPF 程序实现","id":"180","title":"内核态 eBPF 程序实现"},"181":{"body":"我们在用户态的 eBPF 程序中主要进行了以下几个操作: 打开 eBPF 程序。 设置我们想要隐藏的进程的 PID。 验证并加载 eBPF 程序。 等待并处理由 eBPF 程序发送的事件。 首先,我们打开了 eBPF 程序。这个过程是通过调用 pidhide_bpf__open 函数实现的。如果这个过程失败了,我们就直接返回。 skel = pidhide_bpf__open(); if (!skel) { fprintf(stderr, \"Failed to open BPF program: %s\\n\", strerror(errno)); return 1; } 接下来,我们设置了我们想要隐藏的进程的 PID。这个过程是通过将 PID 保存到 eBPF 程序的 rodata 区域实现的。默认情况下,我们隐藏的是当前进程。 char pid_to_hide[10]; if (env.pid_to_hide == 0) { env.pid_to_hide = getpid(); } sprintf(pid_to_hide, \"%d\", env.pid_to_hide); strncpy(skel->rodata->pid_to_hide, pid_to_hide, sizeof(skel->rodata->pid_to_hide)); skel->rodata->pid_to_hide_len = strlen(pid_to_hide) + 1; skel->rodata->target_ppid = env.target_ppid; 然后,我们验证并加载 eBPF 程序。这个过程是通过调用 pidhide_bpf__load 函数实现的。如果这个过程失败了,我们就进行清理操作。 err = pidhide_bpf__load(skel); if (err) { fprintf(stderr, \"Failed to load and verify BPF skeleton\\n\"); goto cleanup; } 最后,我们等待并处理由 eBPF 程序发送的事件。这个过程是通过调用 ring_buffer__poll 函数实现的。在这个过程中,我们每隔一段时间就检查一次环形缓冲区中是否有新的事件。如果有,我们就调用 handle_event 函数来处理这个事件。 printf(\"Successfully started!\\n\");\nprintf(\"Hiding PID %d\\n\", env.pid_to_hide);\nwhile (!exiting)\n{ err = ring_buffer__poll(rb, 100 /* timeout, ms */); /* Ctrl-C will cause -EINTR */ if (err == -EINTR) { err = 0; break; } if (err < 0) { printf(\"Error polling perf buffer: %d\\n\", err); break; }\n} handle_event 函数中,我们根据事件的内容打印了相应的消息。这个函数的参数包括一个上下文,事件的数据,以及数据的大小。我们首先将事件的数据转换为 event 结构体,然后根据 success 字段判断这个事件是否表示成功隐藏了一个进程,最后打 印相应的消息。 static int handle_event(void *ctx, void *data, size_t data_sz)\n{ const struct event *e = data; if (e->success) printf(\"Hid PID from program %d (%s)\\n\", e->pid, e->comm); else printf(\"Failed to hide PID from program %d (%s)\\n\", e->pid, e->comm); return 0;\n} 这段代码展示了如何在用户态使用 eBPF 程序来实现进程隐藏的功能。我们首先打开 eBPF 程序,然后设置我们想要隐藏的进程的 PID,再验证并加载 eBPF 程序,最后等待并处理由 eBPF 程序发送的事件。这个过程中,我们使用了 eBPF 提供的一些高级功能,如环形缓冲区和事件处理,这些功能使得我们能够在用户态方便地与内核态的 eBPF 程序进行交互。 完整源代码: https://github.com/eunomia-bpf/bpf-developer-tutorial/tree/main/src/24-hide 本文所示技术仅为概念验证,仅供学习使用,严禁用于不符合法律法规要求的场景。","breadcrumbs":"使用 eBPF 隐藏进程或文件信息 » 用户态 eBPF 程序实现","id":"181","title":"用户态 eBPF 程序实现"},"182":{"body":"首先,我们需要编译 eBPF 程序: make 然后,假设我们想要隐藏进程 ID 为 1534 的进程,可以运行如下命令: sudo ./pidhide --pid-to-hide 1534 这条命令将使所有尝试读取 /proc/ 目录的操作都无法看到 PID 为 1534 的进程。例如,我们可以选择一个进程进行隐藏: $ ps -aux | grep 1534\nyunwei 1534 0.0 0.0 244540 6848 ? Ssl 6月02 0:00 /usr/libexec/gvfs-mtp-volume-monitor\nyunwei 32065 0.0 0.0 17712 2580 pts/1 S+ 05:43 0:00 grep --color=auto 1534 此时通过 ps 命令可以看到进程 ID 为 1534 的进程。但是,如果我们运行 sudo ./pidhide --pid-to-hide 1534,再次运行 ps -aux | grep 1534,就会发现进程 ID 为 1534 的进程已经不见了。 $ sudo ./pidhide --pid-to-hide 1534\nHiding PID 1534\nHid PID from program 31529 (ps)\nHid PID from program 31551 (ps)\nHid PID from program 31560 (ps)\nHid PID from program 31582 (ps)\nHid PID from program 31582 (ps)\nHid PID from program 31585 (bash)\nHid PID from program 31585 (bash)\nHid PID from program 31609 (bash)\nHid PID from program 31640 (ps)\nHid PID from program 31649 (ps) 这个程序将匹配这个 pid 的进程隐藏,使得像 ps 这样的工具无法看到,我们可以通过 ps aux | grep 1534 来验证。 $ ps -aux | grep 1534\nroot 31523 0.1 0.0 22004 5616 pts/2 S+ 05:42 0:00 sudo ./pidhide -p 1534\nroot 31524 0.0 0.0 22004 812 pts/3 Ss 05:42 0:00 sudo ./pidhide -p 1534\nroot 31525 0.3 0.0 3808 2456 pts/3 S+ 05:42 0:00 ./pidhide -p 1534\nyunwei 31583 0.0 0.0 17712 2612 pts/1 S+ 05:42 0:00 grep --color=auto 1534","breadcrumbs":"使用 eBPF 隐藏进程或文件信息 » 编译运行,隐藏 PID","id":"182","title":"编译运行,隐藏 PID"},"183":{"body":"通过本篇 eBPF 入门实践教程,我们深入了解了如何使用 eBPF 来隐藏进程或文件信息。我们学习了如何编写和加载 eBPF 程序,如何通过 eBPF 拦截系统调用并修改它们的行为,以及如何将这些知识应用到实际的网络安全和防御工作中。此外,我们也了解了 eBPF 的强大性,尤其是它能在不需要修改内核源代码或重启内核的情况下,允许用户在内核中执行自定义代码的能力。 您还可以访问我们的教程代码仓库 https://github.com/eunomia-bpf/bpf-developer-tutorial 或网站 https://eunomia.dev/zh/tutorials/ 以获取更多示例和完整的教程。","breadcrumbs":"使用 eBPF 隐藏进程或文件信息 » 总结","id":"183","title":"总结"},"184":{"body":"eBPF (扩展的伯克利数据包过滤器) 是 Linux 内核的一种革命性技术,允许用户在内核空间执行自定义程序,而不需要修改内核源代码或加载任何内核模块。这使得开发人员可以非常灵活地对 Linux 系统进行观测、修改和控制。 本文介绍了如何使用 eBPF 的 bpf_send_signal 功能,向指定的进程发送信号进行干预。更多的教程文档,请参考 https://github.com/eunomia-bpf/bpf-developer-tutorial","breadcrumbs":"使用 bpf_send_signal 发送信号终止进程 » eBPF 入门实践教程:用 bpf_send_signal 发送信号终止恶意进程","id":"184","title":"eBPF 入门实践教程:用 bpf_send_signal 发送信号终止恶意进程"},"185":{"body":"1. 性能分析: 在现代软件生态系统中,优化应用程序的性能是开发人员和系统管理员的一个核心任务。当应用程序,如 hhvm,出现运行缓慢或资源利用率异常高时,它可能会对整个系统产生不利影响。因此,定位这些性能瓶颈并及时解决是至关重要的。 2. 异常检测与响应: 任何运行在生产环境中的系统都可能面临各种异常情况,从简单的资源泄露到复杂的恶意软件攻击。在这些情况下,系统需要能够迅速、准确地检测到这些异常,并采取适当的应对措施。 3. 动态系统管理: 随着云计算和微服务架构的普及,能够根据当前系统状态动态调整资源配置和应用行为已经成为了一个关键需求。例如,根据流量波动自动扩容或缩容,或者在检测到系统过热时降低 CPU 频率。","breadcrumbs":"使用 bpf_send_signal 发送信号终止进程 » 使用场景","id":"185","title":"使用场景"},"186":{"body":"为了满足上述使用场景的需求,传统的技术方法如下: 安装一个 bpf 程序,该程序会持续监视系统,同时对一个 map 进行轮询。 当某个事件触发了 bpf 程序中定义的特定条件时,它会将相关数据写入此 map。 接着,外部分析工具会从该 map 中读取数据,并根据读取到的信息向目标进程发送信号。 尽管这种方法在很多场景中都是可行的,但它存在一个主要的缺陷:从事件发生到外部工具响应的时间延迟可能相对较大。这种延迟可能会影响到事件的响应速度,从而使得性能分析的结果不准确或者在面对恶意活动时无法及时作出反应。","breadcrumbs":"使用 bpf_send_signal 发送信号终止进程 » 现有方案的不足","id":"186","title":"现有方案的不足"},"187":{"body":"为了克服传统方法的这些限制,Linux 内核提供了 bpf_send_signal 和 bpf_send_signal_thread 这两个 helper 函数。 这两个函数带来的主要优势包括: 1. 实时响应: 通过直接从内核空间发送信号,避免了用户空间的额外开销,这确保了信号能够在事件发生后立即被发送,大大减少了延迟。 2. 准确性: 得益于减少的延迟,现在我们可以获得更准确的系统状态快照,这对于性能分析和异常检测尤其重要。 3. 灵活性: 这些新的 helper 函数为开发人员提供了更多的灵活性,他们可以根据不同的使用场景和需求来自定义信号的发送逻辑,从而更精确地控制和管理系统行为。","breadcrumbs":"使用 bpf_send_signal 发送信号终止进程 » 新方案的优势","id":"187","title":"新方案的优势"},"188":{"body":"在现代操作系统中,一种常见的安全策略是监控和控制进程之间的交互。尤其在Linux系统中,ptrace 系统调用是一个强大的工具,它允许一个进程观察和控制另一个进程的执行,并修改其寄存器和内存。这使得它成为了调试和跟踪工具(如 strace 和 gdb)的主要机制。然而,恶意的 ptrace 使用也可能导致安全隐患。 这个程序的目标是在内核态监控 ptrace 的调用,当满足特定的条件时,它会发送一个 SIGKILL 信号终止调用进程。此外,为了调试或审计目的,该程序会记录这种干预并将相关信息发送到用户空间。","breadcrumbs":"使用 bpf_send_signal 发送信号终止进程 » 内核态代码分析","id":"188","title":"内核态代码分析"},"189":{"body":"","breadcrumbs":"使用 bpf_send_signal 发送信号终止进程 » 代码分析","id":"189","title":"代码分析"},"19":{"body":"如上所述, eBPF 程序的基本框架包括: 包含头文件:需要包含 和 等头文件。 定义许可证:需要定义许可证,通常使用 \"Dual BSD/GPL\"。 定义 BPF 函数:需要定义一个 BPF 函数,例如其名称为 handle_tp,其参数为 void *ctx,返回值为 int。通常用 C 语言编写。 使用 BPF 助手函数:在例如 BPF 函数中,可以使用 BPF 助手函数 bpf_get_current_pid_tgid() 和 bpf_printk()。 返回值","breadcrumbs":"eBPF Hello World,基本框架和开发流程 » eBPF 程序的基本框架","id":"19","title":"eBPF 程序的基本框架"},"190":{"body":"signal.h // Simple message structure to get events from eBPF Programs\n// in the kernel to user spcae\n#define TASK_COMM_LEN 16\nstruct event { int pid; char comm[TASK_COMM_LEN]; bool success;\n}; 这部分定义了一个简单的消息结构,用于从内核的 eBPF 程序传递事件到用户空间。结构包括进程ID、命令名和一个标记是否成功发送信号的布尔值。","breadcrumbs":"使用 bpf_send_signal 发送信号终止进程 » 1. 数据结构定义 (signal.h)","id":"190","title":"1. 数据结构定义 (signal.h)"},"191":{"body":"signal.bpf.c // SPDX-License-Identifier: BSD-3-Clause\n#include \"vmlinux.h\"\n#include \n#include \n#include \n#include \"common.h\" char LICENSE[] SEC(\"license\") = \"Dual BSD/GPL\"; // Ringbuffer Map to pass messages from kernel to user\nstruct { __uint(type, BPF_MAP_TYPE_RINGBUF); __uint(max_entries, 256 * 1024);\n} rb SEC(\".maps\"); // Optional Target Parent PID\nconst volatile int target_ppid = 0; SEC(\"tp/syscalls/sys_enter_ptrace\")\nint bpf_dos(struct trace_event_raw_sys_enter *ctx)\n{ long ret = 0; size_t pid_tgid = bpf_get_current_pid_tgid(); int pid = pid_tgid >> 32; // if target_ppid is 0 then we target all pids if (target_ppid != 0) { struct task_struct *task = (struct task_struct *)bpf_get_current_task(); int ppid = BPF_CORE_READ(task, real_parent, tgid); if (ppid != target_ppid) { return 0; } } // Send signal. 9 == SIGKILL ret = bpf_send_signal(9); // Log event struct event *e; e = bpf_ringbuf_reserve(&rb, sizeof(*e), 0); if (e) { e->success = (ret == 0); e->pid = pid; bpf_get_current_comm(&e->comm, sizeof(e->comm)); bpf_ringbuf_submit(e, 0); } return 0;\n} 许可证声明 声明了程序的许可证为 \"Dual BSD/GPL\",这是为了满足 Linux 内核对 eBPF 程序的许可要求。 Ringbuffer Map 这是一个 ring buffer 类型的 map,允许 eBPF 程序在内核空间产生的消息被用户空间程序高效地读取。 目标父进程ID target_ppid 是一个可选的父进程ID,用于限制哪些进程受到影响。如果它被设置为非零值,只有与其匹配的进程才会被目标。 主函数 bpf_dos 进程检查 程序首先获取当前进程的ID。如果设置了 target_ppid,它还会获取当前进程的父进程ID并进行比较。如果两者不匹配,则直接返回。 发送信号 使用 bpf_send_signal(9) 来发送 SIGKILL 信号。这将终止调用 ptrace 的进程。 记录事件 使用 ring buffer map 记录这个事件。这包括了是否成功发送信号、进程ID以及进程的命令名。 总结:这个 eBPF 程序提供了一个方法,允许系统管理员或安全团队在内核级别监控和干预 ptrace 调用,提供了一个对抗潜在恶意活动或误操作的额外层次。","breadcrumbs":"使用 bpf_send_signal 发送信号终止进程 » 2. eBPF 程序 (signal.bpf.c)","id":"191","title":"2. eBPF 程序 (signal.bpf.c)"},"192":{"body":"eunomia-bpf 是一个结合 Wasm 的开源 eBPF 动态加载运行时和开发工具链,它的目的是简化 eBPF 程序的开发、构建、分发、运行。可以参考 https://github.com/eunomia-bpf/eunomia-bpf 下载和安装 ecc 编译工具链和 ecli 运行时。我们使用 eunomia-bpf 编译运行这个例子。 编译: ./ecc signal.bpf.c signal.h 使用方式: $ sudo ./ecli package.json 这个程序会对任何试图使用 ptrace 系统调用的程序,例如 strace,发出 SIG_KILL 信号。 一旦 eBPF 程序开始运行,你可以通过运行以下命令进行测试: $ strace /bin/whoami\nKilled 原先的 console 中会输出: INFO [bpf_loader_lib::skeleton] Running ebpf program...\nTIME PID COMM SUCCESS 13:54:45 8857 strace true 完整的源代码可以参考: https://github.com/eunomia-bpf/bpf-developer-tutorial/tree/main/src/25-signal","breadcrumbs":"使用 bpf_send_signal 发送信号终止进程 » 编译运行","id":"192","title":"编译运行"},"193":{"body":"通过这个实例,我们深入了解了如何将 eBPF 程序与用户态程序相结合,实现对系统调用的监控和干预。eBPF 提供了一种在内核空间执行程序的机制,这种技术不仅限于监控,还可用于性能优化、安全防御、系统诊断等多种场景。对于开发者来说,这为Linux系统的性能调优和故障排查提供了一种强大且灵活的工具。 最后,如果您对 eBPF 技术感兴趣,并希望进一步了解和实践,可以访问我们的教程代码仓库 https://github.com/eunomia-bpf/bpf-developer-tutorial 和教程网站 https://eunomia.dev/zh/tutorials/。","breadcrumbs":"使用 bpf_send_signal 发送信号终止进程 » 总结","id":"193","title":"总结"},"194":{"body":"https://github.com/pathtofile/bad-bpf https://www.mail-archive.com/netdev@vger.kernel.org/msg296358.html","breadcrumbs":"使用 bpf_send_signal 发送信号终止进程 » 参考资料","id":"194","title":"参考资料"},"195":{"body":"编译: make 使用方式: sudo ./sudoadd --username lowpriv-user 这个程序允许一个通常权限较低的用户使用 sudo 成为 root。 它通过拦截 sudo 读取 /etc/sudoers 文件,并将第一行覆盖为 ALL=(ALL:ALL) NOPASSWD:ALL # 的方式工作。这欺骗了 sudo,使其认为用户被允许成为 root。其他程序如 cat 或 sudoedit 不受影响,所以对于这些程序来说,文件未改变,用户并没有这些权限。行尾的 # 确保行的其余部分被当作注释处理,因此不会破坏文件的逻辑。","breadcrumbs":"使用 eBPF 添加 sudo 用户 » 使用 eBPF 添加 sudo 用户","id":"195","title":"使用 eBPF 添加 sudo 用户"},"196":{"body":"https://github.com/pathtofile/bad-bpf","breadcrumbs":"使用 eBPF 添加 sudo 用户 » 参考资料","id":"196","title":"参考资料"},"197":{"body":"编译: make 使用方式: sudo ./replace --filename /path/to/file --input foo --replace bar 这个程序将文件中所有与 input 匹配的文本替换为 replace 文本。 这有很多用途,例如: 隐藏内核模块 joydev,避免被如 lsmod 这样的工具发现: ./replace -f /proc/modules -i 'joydev' -r 'cryptd' 伪造 eth0 接口的 MAC 地址: ./replace -f /sys/class/net/eth0/address -i '00:15:5d:01:ca:05' -r '00:00:00:00:00:00' 恶意软件进行反沙箱检查可能会检查 MAC 地址,寻找是否正在虚拟机或沙箱内运行,而不是在“真实”的机器上运行的迹象。 注意: input 和 replace 的长度必须相同,以避免在文本块的中间添加 NULL 字符。在 bash 提示符下输入换行符,使用 $'\\n',例如 --replace $'text\\n'。","breadcrumbs":"使用 eBPF 替换任意程序读取或写入的文本 » 使用 eBPF 替换任意程序读取或写入的文本","id":"197","title":"使用 eBPF 替换任意程序读取或写入的文本"},"198":{"body":"https://github.com/pathtofile/bad-bpf","breadcrumbs":"使用 eBPF 替换任意程序读取或写入的文本 » 参考资料","id":"198","title":"参考资料"},"199":{"body":"通过使用 detach 的方式运行 eBPF 程序,用户空间加载器可以退出,而不会停止 eBPF 程序。","breadcrumbs":"BPF的生命周期:使用 Detached 模式在用户态应用退出后持续运行 eBPF 程序 » 在用户态应用退出后运行 eBPF 程序:eBPF 程序的生命周期","id":"199","title":"在用户态应用退出后运行 eBPF 程序:eBPF 程序的生命周期"},"2":{"body":"eBPF 是一项革命性的技术,起源于 Linux 内核,可以在操作系统的内核中运行沙盒程序。它被用来安全和有效地扩展内核的功能,而不需要改变内核的源代码或加载内核模块。eBPF 通过允许在操作系统内运行沙盒程序,应用程序开发人员可以在运行时,可编程地向操作系统动态添加额外的功能。然后,操作系统保证安全和执行效率,就像在即时编译(JIT)编译器和验证引擎的帮助下进行本地编译一样。eBPF 程序在内核版本之间是可移植的,并且可以自动更新,从而避免了工作负载中断和节点重启。 今天,eBPF被广泛用于各类场景:在现代数据中心和云原生环境中,可以提供高性能的网络包处理和负载均衡;以非常低的资源开销,做到对多种细粒度指标的可观测性,帮助应用程序开发人员跟踪应用程序,为性能故障排除提供洞察力;保障应用程序和容器运行时的安全执行,等等。可能性是无穷的,而 eBPF 在操作系统内核中所释放的创新才刚刚开始[3]。","breadcrumbs":"介绍 eBPF 的基本概念、常见的开发工具 » 1. eBPF简介:安全和有效地扩展内核","id":"2","title":"1. eBPF简介:安全和有效地扩展内核"},"20":{"body":"跟踪点(tracepoints)是内核静态插桩技术,在技术上只是放置在内核源代码中的跟踪函数,实际上就是在源码中插入的一些带有控制条件的探测点,这些探测点允许事后再添加处理函数。比如在内核中,最常见的静态跟踪方法就是 printk,即输出日志。又比如:在系统调用、调度程序事件、文件系统操作和磁盘 I/O 的开始和结束时都有跟踪点。跟踪点于 2009 年在 Linux 2.6.32 版本中首次提供。跟踪点是一种稳定的 API,数量有限。","breadcrumbs":"eBPF Hello World,基本框架和开发流程 » tracepoints","id":"20","title":"tracepoints"},"200":{"body":"首先,我们需要了解一些关键的概念,如 BPF 对象(包括程序,地图和调试信息),文件描述符 (FD),引用计数(refcnt)等。在 eBPF 系统中,用户空间通过文件描述符访问 BPF 对象,而每个对象都有一个引用计数。当一个对象被创建时,其引用计数初始为1。如果该对象不再被使用(即没有其他程序或文件描述符引用它),它的引用计数将降至0,并在 RCU 宽限期后被内存清理。 接下来,我们需要了解 eBPF 程序的生命周期。首先,当你创建一个 BPF 程序,并将它连接到某个“钩子”(例如网络接口,系统调用等),它的引用计数会增加。然后,即使原始创建和加载该程序的用户空间进程退出,只要 BPF 程序的引用计数大于 0,它就会保持活动状态。然而,这个过程中有一个重要的点是:不是所有的钩子都是相等的。有些钩子是全局的,比如 XDP、tc's clsact 和 cgroup-based 钩子。这些全局钩子会一直保持 BPF 程序的活动状态,直到这些对象自身消失。而有些钩子是局部的,只在拥有它们的进程存活期间运行。 对于 BPF 对象(程序或映射)的生命周期管理,另一个关键的操作是“分离”(detach)。这个操作会阻止已附加程序的任何未来执行。然后,对于需要替换 BPF 程序的情况,你可以使用替换(replace)操作。这是一个复杂的过程,因为你需要确保在替换过程中,不会丢失正在处理的事件,而且新旧程序可能在不同的 CPU 上同时运行。 最后,除了通过文件描述符和引用计数来管理 BPF 对象的生命周期,还有一个叫做 BPFFS 的方法,也就是“BPF 文件系统”。用户空间进程可以在 BPFFS 中“固定”(pin)一个 BPF 程序或映射,这将增加对象的引用计数,使得即使 BPF 程序未附加到任何地方或 BPF 映射未被任何程序使用,该 BPF 对象也将保持活动状态。 所以,当我们谈论在后台运行 eBPF 程序时,我们需要清楚这个过程的含义。在某些情况下,即使用户空间进程已经退出,我们可能还希望 BPF 程序保持运行。这就需要我们正确地管理 BPF 对象的生命周期","breadcrumbs":"BPF的生命周期:使用 Detached 模式在用户态应用退出后持续运行 eBPF 程序 » eBPF 程序的生命周期","id":"200","title":"eBPF 程序的生命周期"},"201":{"body":"这里还是采用了上一个的字符串替换的应用,来体现对应可能的安全风险。通过使用 --detach 运行程序,用户空间加载器可以退出,而不会停止 eBPF 程序。 编译: make 在运行前,请首先确保 bpf 文件系统已经被挂载: sudo mount bpffs -t bpf /sys/fs/bpf\nmkdir /sys/fs/bpf/textreplace 然后,你可以分离运行 text-replace2: ./textreplace2 -f /proc/modules -i 'joydev' -r 'cryptd' -d 这将在 /sys/fs/bpf/textreplace 下创建一些 eBPF 链接文件。 一旦加载器成功运行,你可以通过运行以下命令检查日志: sudo cat /sys/kernel/debug/tracing/trace_pipe\n# 确认链接文件存在\nsudo ls -l /sys/fs/bpf/textreplace 然后,要停止,只需删除链接文件即可: sudo rm -r /sys/fs/bpf/textreplace","breadcrumbs":"BPF的生命周期:使用 Detached 模式在用户态应用退出后持续运行 eBPF 程序 » 运行","id":"201","title":"运行"},"202":{"body":"https://github.com/pathtofile/bad-bpf https://facebookmicrosites.github.io/bpf/blog/2018/08/31/object-lifetime.html","breadcrumbs":"BPF的生命周期:使用 Detached 模式在用户态应用退出后持续运行 eBPF 程序 » 参考资料","id":"202","title":"参考资料"},"203":{"body":"随着TLS在现代网络环境中的广泛应用,跟踪微服务RPC消息已经变得愈加棘手。传统的流量嗅探技术常常受限于只能获取到加密后的数据,导致无法真正观察到通信的原始内容。这种限制为系统的调试和分析带来了不小的障碍。 但现在,我们有了新的解决方案。使用 eBPF 技术,通过其能力在用户空间进行探测,提供了一种方法重新获得明文数据,使得我们可以直观地查看加密前的通信内容。然而,每个应用可能使用不同的库,每个库都有多个版本,这种多样性给跟踪带来了复杂性。 在本教程中,我们将带您了解一种跨多种用户态 SSL/TLS 库的 eBPF 追踪技术,它不仅可以同时跟踪 GnuTLS 和 OpenSSL 等用户态库,而且相比以往,大大降低了对新版本库的维护工作。","breadcrumbs":"使用 eBPF 用户态捕获多种库的 SSL/TLS 明文数据 » eBPF 实践教程:使用 uprobe 捕获多种库的 SSL/TLS 明文数据","id":"203","title":"eBPF 实践教程:使用 uprobe 捕获多种库的 SSL/TLS 明文数据"},"204":{"body":"在深入本教程的主题之前,我们需要理解一些核心概念,这些概念将为我们后面的讨论提供基础。","breadcrumbs":"使用 eBPF 用户态捕获多种库的 SSL/TLS 明文数据 » 背景知识","id":"204","title":"背景知识"},"205":{"body":"SSL (Secure Sockets Layer): 由 Netscape 在 1990 年代早期开发,为网络上的两台机器之间提供数据加密传输。然而,由于某些已知的安全问题,SSL的使用已被其后继者TLS所替代。 TLS (Transport Layer Security): 是 SSL 的继任者,旨在提供更强大和更安全的数据加密方式。TLS 工作通过一个握手过程,在这个过程中,客户端和服务器之间会选择一个加密算法和相应的密钥。一旦握手完成,数据传输开始,所有数据都使用选择的算法和密钥加密。","breadcrumbs":"使用 eBPF 用户态捕获多种库的 SSL/TLS 明文数据 » SSL 和 TLS","id":"205","title":"SSL 和 TLS"},"206":{"body":"Transport Layer Security (TLS) 是一个密码学协议,旨在为计算机网络上的通信提供安全性。它主要目标是通过密码学,例如证书的使用,为两个或更多通信的计算机应用程序提供安全性,包括隐私(机密性)、完整性和真实性。TLS 由两个子层组成:TLS 记录协议和TLS 握手协议。 握手过程 当客户端与启用了TLS的服务器连接并请求建立安全连接时,握手过程开始。握手允许客户端和服务器通过不对称密码来建立连接的安全性参数,完整流程如下: 初始握手 :客户端连接到启用了TLS的服务器,请求安全连接,并提供它支持的密码套件列表(加密算法和哈希函数)。 选择密码套件 :从提供的列表中,服务器选择它也支持的密码套件和哈希函数,并通知客户端已做出的决定。 提供数字证书 :通常,服务器接下来会提供形式为数字证书的身份验证。此证书包含服务器名称、信任的证书授权机构(为证书的真实性提供担保)以及服务器的公共加密密钥。 验证证书 :客户端在继续之前确认证书的有效性。 生成会话密钥 :为了生成用于安全连接的会话密钥,客户端有以下两种方法: 使用服务器的公钥加密一个随机数(PreMasterSecret)并将结果发送到服务器(只有服务器才能使用其私钥解密);双方然后使用该随机数生成一个独特的会话密钥,用于会话期间的数据加密和解密。 使用 Diffie-Hellman 密钥交换(或其变体椭圆曲线DH)来安全地生成一个随机且独特的会话密钥,用于加密和解密,该密钥具有前向保密的额外属性:即使在未来公开了服务器的私钥,也不能用它来解密当前的会话,即使第三方拦截并记录了会话。 一旦上述步骤成功完成,握手过程便结束,加密的连接开始。此连接使用会话密钥进行加密和解密,直到连接关闭。如果上述任何步骤失败,则TLS握手失败,连接将不会建立。 OSI模型中的TLS TLS 和 SSL 不完全适合 OSI 模型或 TCP/IP 模型的任何单一层次。TLS 在“某些可靠的传输协议(例如,TCP)之上运行”,这意味着它位于传输层之上。它为更高的层提供加密,这通常是表示层的功能。但是,使用TLS 的应用程序通常视其为传输层,即使使用TLS的应用程序必须积极控制启动 TLS 握手和交换的认证证书的处理。","breadcrumbs":"使用 eBPF 用户态捕获多种库的 SSL/TLS 明文数据 » TLS 的工作原理","id":"206","title":"TLS 的工作原理"},"207":{"body":"eBPF (Extended Berkeley Packet Filter): 是一种内核技术,允许用户在内核空间中运行预定义的程序,不需要修改内核源代码或重新加载模块。它创建了一个桥梁,使得用户空间和内核空间可以交互,从而为系统监控、性能分析和网络流量分析等任务提供了无前例的能力。 uprobes 是eBPF的一个重要特性,允许我们在用户空间应用程序中动态地插入探测点,特别适用于跟踪SSL/TLS库中的函数调用。","breadcrumbs":"使用 eBPF 用户态捕获多种库的 SSL/TLS 明文数据 » eBPF 和 uprobe","id":"207","title":"eBPF 和 uprobe"},"208":{"body":"SSL/TLS协议的实现主要依赖于用户态库。以下是一些常见的库: OpenSSL: 一个开源的、功能齐全的加密库,广泛应用于许多开源和商业项目中。 BoringSSL: 是Google维护的OpenSSL的一个分支,重点是简化和优化,适用于Google的需求。 GnuTLS: 是GNU项目的一部分,提供了SSL,TLS和DTLS协议的实现。与OpenSSL和BoringSSL相比,GnuTLS在API设计、模块结构和许可证上有所不同。","breadcrumbs":"使用 eBPF 用户态捕获多种库的 SSL/TLS 明文数据 » 用户态库","id":"208","title":"用户态库"},"209":{"body":"OpenSSL 是一个广泛应用的开源库,提供了 SSL 和 TLS 协议的完整实现,并广泛用于各种应用程序中以确保数据传输的安全性。其中,SSL_read() 和 SSL_write() 是两个核心的 API 函数,用于从 TLS/SSL 连接中读取和写入数据。本章节,我们将深入这两个函数,帮助你理解其工作机制。","breadcrumbs":"使用 eBPF 用户态捕获多种库的 SSL/TLS 明文数据 » OpenSSL API 分析","id":"209","title":"OpenSSL API 分析"},"21":{"body":"面对创建一个 eBPF 项目,您是否对如何开始搭建环境以及选择编程语言感到困惑?别担心,我们为您准备了一系列 GitHub 模板,以便您快速启动一个全新的eBPF项目。只需在GitHub上点击 Use this template 按钮,即可开始使用。 https://github.com/eunomia-bpf/libbpf-starter-template :基于C语言和 libbpf 框架的eBPF项目模板 https://github.com/eunomia-bpf/cilium-ebpf-starter-template :基于C语言和cilium/ebpf框架的eBPF项目模板 https://github.com/eunomia-bpf/libbpf-rs-starter-template :基于Rust语言和libbpf-rs框架的eBPF项目模板 https://github.com/eunomia-bpf/eunomia-template :基于C语言和eunomia-bpf框架的eBPF项目模板 这些启动模板包含以下功能: 一个 Makefile,让您可以一键构建项目 一个 Dockerfile,用于为您的 eBPF 项目自动创建一个容器化环境并发布到 Github Packages GitHub Actions,用于自动化构建、测试和发布流程 eBPF 开发所需的所有依赖项 通过将现有仓库设置为模板,您和其他人可以快速生成具有相同基础结构的新仓库,从而省去了手动创建和配置的繁琐过程。借助 GitHub 模板仓库,开发者可以专注于项目的核心功能和逻辑,而无需为基础设置和结构浪费时间。更多关于模板仓库的信息,请参阅官方文档: https://docs.github.com/en/repositories/creating-and-managing-repositories/creating-a-template-repository","breadcrumbs":"eBPF Hello World,基本框架和开发流程 » GitHub 模板:轻松构建 eBPF 项目和开发环境","id":"21","title":"GitHub 模板:轻松构建 eBPF 项目和开发环境"},"210":{"body":"当我们想从一个已建立的 SSL 连接中读取数据时,可以使用 SSL_read 或 SSL_read_ex 函数。函数原型如下: int SSL_read_ex(SSL *ssl, void *buf, size_t num, size_t *readbytes);\nint SSL_read(SSL *ssl, void *buf, int num); SSL_read 和 SSL_read_ex 试图从指定的 ssl 中读取最多 num 字节的数据到缓冲区 buf 中。成功时,SSL_read_ex 会在 *readbytes 中存储实际读取到的字节数。","breadcrumbs":"使用 eBPF 用户态捕获多种库的 SSL/TLS 明文数据 » 1. SSL_read 函数","id":"210","title":"1. SSL_read 函数"},"211":{"body":"当我们想往一个已建立的 SSL 连接中写入数据时,可以使用 SSL_write 或 SSL_write_ex 函数。 函数原型: int SSL_write_ex(SSL *s, const void *buf, size_t num, size_t *written);\nint SSL_write(SSL *ssl, const void *buf, int num); SSL_write 和 SSL_write_ex 会从缓冲区 buf 中将最多 num 字节的数据写入到指定的 ssl 连接中。成功时,SSL_write_ex 会在 *written 中存储实际写入的字节数。","breadcrumbs":"使用 eBPF 用户态捕获多种库的 SSL/TLS 明文数据 » 2. SSL_write 函数","id":"211","title":"2. SSL_write 函数"},"212":{"body":"在我们的例子中,我们使用 eBPF 来 hook ssl_read 和 ssl_write 函数,从而在数据读取或写入 SSL 连接时执行自定义操作。","breadcrumbs":"使用 eBPF 用户态捕获多种库的 SSL/TLS 明文数据 » eBPF 内核态代码编写","id":"212","title":"eBPF 内核态代码编写"},"213":{"body":"首先,我们定义了一个数据结构 probe_SSL_data_t 用于在内核态和用户态之间传输数据: #define MAX_BUF_SIZE 8192\n#define TASK_COMM_LEN 16 struct probe_SSL_data_t { __u64 timestamp_ns; // 时间戳(纳秒) __u64 delta_ns; // 函数执行时间 __u32 pid; // 进程 ID __u32 tid; // 线程 ID __u32 uid; // 用户 ID __u32 len; // 读/写数据的长度 int buf_filled; // 缓冲区是否填充完整 int rw; // 读或写(0为读,1为写) char comm[TASK_COMM_LEN]; // 进程名 __u8 buf[MAX_BUF_SIZE]; // 数据缓冲区 int is_handshake; // 是否是握手数据\n};","breadcrumbs":"使用 eBPF 用户态捕获多种库的 SSL/TLS 明文数据 » 数据结构","id":"213","title":"数据结构"},"214":{"body":"我们的目标是 hook 到 SSL_read 和 SSL_write 函数。我们定义了一个函数 SSL_exit 来处理这两个函数的返回值。该函数会根据当前进程和线程的 ID,确定是否需要追踪并收集数据。 static int SSL_exit(struct pt_regs *ctx, int rw) { int ret = 0; u32 zero = 0; u64 pid_tgid = bpf_get_current_pid_tgid(); u32 pid = pid_tgid >> 32; u32 tid = (u32)pid_tgid; u32 uid = bpf_get_current_uid_gid(); u64 ts = bpf_ktime_get_ns(); if (!trace_allowed(uid, pid)) { return 0; } /* store arg info for later lookup */ u64 *bufp = bpf_map_lookup_elem(&bufs, &tid); if (bufp == 0) return 0; u64 *tsp = bpf_map_lookup_elem(&start_ns, &tid); if (!tsp) return 0; u64 delta_ns = ts - *tsp; int len = PT_REGS_RC(ctx); if (len <= 0) // no data return 0; struct probe_SSL_data_t *data = bpf_map_lookup_elem(&ssl_data, &zero); if (!data) return 0; data->timestamp_ns = ts; data->delta_ns = delta_ns; data->pid = pid; data->tid = tid; data->uid = uid; data->len = (u32)len; data->buf_filled = 0; data->rw = rw; data->is_handshake = false; u32 buf_copy_size = min((size_t)MAX_BUF_SIZE, (size_t)len); bpf_get_current_comm(&data->comm, sizeof(data->comm)); if (bufp != 0) ret = bpf_probe_read_user(&data->buf, buf_copy_size, (char *)*bufp); bpf_map_delete_elem(&bufs, &tid); bpf_map_delete_elem(&start_ns, &tid); if (!ret) data->buf_filled = 1; else buf_copy_size = 0; bpf_perf_event_output(ctx, &perf_SSL_events, BPF_F_CURRENT_CPU, data, EVENT_SIZE(buf_copy_size)); return 0;\n} 这里的 rw 参数标识是读还是写。0 代表读,1 代表写。 数据收集流程 获取当前进程和线程的 ID,以及当前用户的 ID。 通过 trace_allowed 判断是否允许追踪该进程。 获取起始时间,以计算函数的执行时间。 尝试从 bufs 和 start_ns maps 中查找相关的数据。 如果成功读取了数据,则创建或查找 probe_SSL_data_t 结构来填充数据。 将数据从用户空间复制到缓冲区,并确保不超过预定的大小。 最后,将数据发送到用户空间。 注意:我们使用了两个用户返回探针 uretprobe 来分别 hook SSL_read 和 SSL_write 的返回: SEC(\"uretprobe/SSL_read\")\nint BPF_URETPROBE(probe_SSL_read_exit) { return (SSL_exit(ctx, 0)); // 0 表示读操作\n} SEC(\"uretprobe/SSL_write\")\nint BPF_URETPROBE(probe_SSL_write_exit) { return (SSL_exit(ctx, 1)); // 1 表示写操作\n}","breadcrumbs":"使用 eBPF 用户态捕获多种库的 SSL/TLS 明文数据 » Hook 函数","id":"214","title":"Hook 函数"},"215":{"body":"在 SSL/TLS 中,握手(handshake)是一个特殊的过程,用于在客户端和服务器之间建立安全的连接。为了分析此过程,我们 hook 到了 do_handshake 函数,以跟踪握手的开始和结束。 进入握手 我们使用 uprobe 为 do_handshake 设置一个 probe: SEC(\"uprobe/do_handshake\")\nint BPF_UPROBE(probe_SSL_do_handshake_enter, void *ssl) { u64 pid_tgid = bpf_get_current_pid_tgid(); u32 pid = pid_tgid >> 32; u32 tid = (u32)pid_tgid; u64 ts = bpf_ktime_get_ns(); u32 uid = bpf_get_current_uid_gid(); if (!trace_allowed(uid, pid)) { return 0; } /* store arg info for later lookup */ bpf_map_update_elem(&start_ns, &tid, &ts, BPF_ANY); return 0;\n} 这段代码的主要功能如下: 获取当前的 pid, tid, ts 和 uid。 使用 trace_allowed 检查进程是否被允许追踪。 将当前时间戳存储在 start_ns 映射中,用于稍后计算握手过程的持续时间。 退出握手 同样,我们为 do_handshake 的返回设置了一个 uretprobe: SEC(\"uretprobe/do_handshake\")\nint BPF_URETPROBE(probe_SSL_do_handshake_exit) { u32 zero = 0; u64 pid_tgid = bpf_get_current_pid_tgid(); u32 pid = pid_tgid >> 32; u32 tid = (u32)pid_tgid; u32 uid = bpf_get_current_uid_gid(); u64 ts = bpf_ktime_get_ns(); int ret = 0; /* use kernel terminology here for tgid/pid: */ u32 tgid = pid_tgid >> 32; /* store arg info for later lookup */ if (!trace_allowed(tgid, pid)) { return 0; } u64 *tsp = bpf_map_lookup_elem(&start_ns, &tid); if (tsp == 0) return 0; ret = PT_REGS_RC(ctx); if (ret <= 0) // handshake failed return 0; struct probe_SSL_data_t *data = bpf_map_lookup_elem(&ssl_data, &zero); if (!data) return 0; data->timestamp_ns = ts; data->delta_ns = ts - *tsp; data->pid = pid; data->tid = tid; data->uid = uid; data->len = ret; data->buf_filled = 0; data->rw = 2; data->is_handshake = true; bpf_get_current_comm(&data->comm, sizeof(data->comm)); bpf_map_delete_elem(&start_ns, &tid); bpf_perf_event_output(ctx, &perf_SSL_events, BPF_F_CURRENT_CPU, data, EVENT_SIZE(0)); return 0;\n} 此函数的逻辑如下: 获取当前的 pid, tid, ts 和 uid。 使用 trace_allowed 再次检查是否允许追踪。 查找 start_ns 映射中的时间戳,用于计算握手的持续时间。 使用 PT_REGS_RC(ctx) 获取 do_handshake 的返回值,判断握手是否成功。 查找或初始化与当前线程关联的 probe_SSL_data_t 数据结构。 更新数据结构的字段,包括时间戳、持续时间、进程信息等。 通过 bpf_perf_event_output 将数据发送到用户态。 我们的 eBPF 代码不仅跟踪了 ssl_read 和 ssl_write 的数据传输,还特别关注了 SSL/TLS 的握手过程。这些信息对于深入了解和优化安全连接的性能至关重要。 通过这些 hook 函数,我们可以获得关于握手成功与否、握手所需的时间以及相关的进程信息的数据。这为我们提供了关于系统 SSL/TLS 行为的深入见解,可以帮助我们在需要时进行更深入的分析和优化。","breadcrumbs":"使用 eBPF 用户态捕获多种库的 SSL/TLS 明文数据 » Hook到握手过程","id":"215","title":"Hook到握手过程"},"216":{"body":"在 eBPF 的生态系统中,用户态和内核态代码经常协同工作。内核态代码负责数据的采集,而用户态代码则负责设置、管理和处理这些数据。在本节中,我们将解读上述用户态代码如何配合 eBPF 追踪 SSL/TLS 交互。","breadcrumbs":"使用 eBPF 用户态捕获多种库的 SSL/TLS 明文数据 » 用户态辅助代码分析与解读","id":"216","title":"用户态辅助代码分析与解读"},"217":{"body":"上述代码片段中,根据环境变量 env 的设定,程序可以选择针对三种常见的加密库(OpenSSL、GnuTLS 和 NSS)进行挂载。这意味着我们可以在同一个工具中对多种库的调用进行追踪。 为了实现这一功能,首先利用 find_library_path 函数确定库的路径。然后,根据库的类型,调用对应的 attach_ 函数来将 eBPF 程序挂载到库函数上。 if (env.openssl) { char *openssl_path = find_library_path(\"libssl.so\"); printf(\"OpenSSL path: %s\\n\", openssl_path); attach_openssl(obj, \"/lib/x86_64-linux-gnu/libssl.so.3\"); } if (env.gnutls) { char *gnutls_path = find_library_path(\"libgnutls.so\"); printf(\"GnuTLS path: %s\\n\", gnutls_path); attach_gnutls(obj, gnutls_path); } if (env.nss) { char *nss_path = find_library_path(\"libnspr4.so\"); printf(\"NSS path: %s\\n\", nss_path); attach_nss(obj, nss_path); } 这里主要包含 OpenSSL、GnuTLS 和 NSS 三个库的挂载逻辑。NSS 是为组织设计的一套安全库,支持创建安全的客户端和服务器应用程序。它们最初是由 Netscape 开发的,现在由 Mozilla 维护。其他两个库前面已经介绍过了,这里不再赘述。","breadcrumbs":"使用 eBPF 用户态捕获多种库的 SSL/TLS 明文数据 » 1. 支持的库挂载","id":"217","title":"1. 支持的库挂载"},"218":{"body":"具体的 attach 函数如下: #define __ATTACH_UPROBE(skel, binary_path, sym_name, prog_name, is_retprobe) \\ do { \\ LIBBPF_OPTS(bpf_uprobe_opts, uprobe_opts, .func_name = #sym_name, \\ .retprobe = is_retprobe); \\ skel->links.prog_name = bpf_program__attach_uprobe_opts( \\ skel->progs.prog_name, env.pid, binary_path, 0, &uprobe_opts); \\ } while (false) int attach_openssl(struct sslsniff_bpf *skel, const char *lib) { ATTACH_UPROBE_CHECKED(skel, lib, SSL_write, probe_SSL_rw_enter); ATTACH_URETPROBE_CHECKED(skel, lib, SSL_write, probe_SSL_write_exit); ATTACH_UPROBE_CHECKED(skel, lib, SSL_read, probe_SSL_rw_enter); ATTACH_URETPROBE_CHECKED(skel, lib, SSL_read, probe_SSL_read_exit); if (env.latency && env.handshake) { ATTACH_UPROBE_CHECKED(skel, lib, SSL_do_handshake, probe_SSL_do_handshake_enter); ATTACH_URETPROBE_CHECKED(skel, lib, SSL_do_handshake, probe_SSL_do_handshake_exit); } return 0;\n} int attach_gnutls(struct sslsniff_bpf *skel, const char *lib) { ATTACH_UPROBE_CHECKED(skel, lib, gnutls_record_send, probe_SSL_rw_enter); ATTACH_URETPROBE_CHECKED(skel, lib, gnutls_record_send, probe_SSL_write_exit); ATTACH_UPROBE_CHECKED(skel, lib, gnutls_record_recv, probe_SSL_rw_enter); ATTACH_URETPROBE_CHECKED(skel, lib, gnutls_record_recv, probe_SSL_read_exit); return 0;\n} int attach_nss(struct sslsniff_bpf *skel, const char *lib) { ATTACH_UPROBE_CHECKED(skel, lib, PR_Write, probe_SSL_rw_enter); ATTACH_URETPROBE_CHECKED(skel, lib, PR_Write, probe_SSL_write_exit); ATTACH_UPROBE_CHECKED(skel, lib, PR_Send, probe_SSL_rw_enter); ATTACH_URETPROBE_CHECKED(skel, lib, PR_Send, probe_SSL_write_exit); ATTACH_UPROBE_CHECKED(skel, lib, PR_Read, probe_SSL_rw_enter); ATTACH_URETPROBE_CHECKED(skel, lib, PR_Read, probe_SSL_read_exit); ATTACH_UPROBE_CHECKED(skel, lib, PR_Recv, probe_SSL_rw_enter); ATTACH_URETPROBE_CHECKED(skel, lib, PR_Recv, probe_SSL_read_exit); return 0;\n} 我们进一步观察 attach_ 函数,可以看到它们都使用了 ATTACH_UPROBE_CHECKED 和 ATTACH_URETPROBE_CHECKED 宏来实现具体的挂载逻辑。这两个宏分别用于设置 uprobe(函数入口)和 uretprobe(函数返回)。 考虑到不同的库有不同的 API 函数名称(例如,OpenSSL 使用 SSL_write,而 GnuTLS 使用 gnutls_record_send),所以我们需要为每个库写一个独立的 attach_ 函数。 例如,在 attach_openssl 函数中,我们为 SSL_write 和 SSL_read 设置了 probe。如果用户还希望追踪握手的延迟 (env.latency) 和握手过程 (env.handshake),那么我们还会为 SSL_do_handshake 设置 probe。 在eBPF生态系统中,perf_buffer是一个用于从内核态传输数据到用户态的高效机制。这对于内核态eBPF程序来说是十分有用的,因为它们不能直接与用户态进行交互。使用perf_buffer,我们可以在内核态eBPF程序中收集数据,然后在用户态异步地读取这些数据。我们使用 perf_buffer__poll 函数来读取内核态上报的数据,如下所示: while (!exiting) { err = perf_buffer__poll(pb, PERF_POLL_TIMEOUT_MS); if (err < 0 && err != -EINTR) { warn(\"error polling perf buffer: %s\\n\", strerror(-err)); goto cleanup; } err = 0; } 最后,在 print_event 函数中,我们将数据打印到标准输出: // Function to print the event from the perf buffer\nvoid print_event(struct probe_SSL_data_t *event, const char *evt) { ... if (buf_size != 0) { if (env.hexdump) { // 2 characters for each byte + null terminator char hex_data[MAX_BUF_SIZE * 2 + 1] = {0}; buf_to_hex((uint8_t *)buf, buf_size, hex_data); printf(\"\\n%s\\n\", s_mark); for (size_t i = 0; i < strlen(hex_data); i += 32) { printf(\"%.32s\\n\", hex_data + i); } printf(\"%s\\n\\n\", e_mark); } else { printf(\"\\n%s\\n%s\\n%s\\n\\n\", s_mark, buf, e_mark); } }\n} 完整的源代码可以在这里查看: https://github.com/eunomia-bpf/bpf-developer-tutorial/tree/main/src/30-sslsniff","breadcrumbs":"使用 eBPF 用户态捕获多种库的 SSL/TLS 明文数据 » 2. 详细挂载逻辑","id":"218","title":"2. 详细挂载逻辑"},"219":{"body":"要开始使用 sslsniff,首先要进行编译: make 完成后,请按照以下步骤操作:","breadcrumbs":"使用 eBPF 用户态捕获多种库的 SSL/TLS 明文数据 » 编译与运行","id":"219","title":"编译与运行"},"22":{"body":"eBPF 程序的开发和使用流程可以概括为如下几个步骤: 定义 eBPF 程序的接口和类型:这包括定义 eBPF 程序的接口函数,定义和实现 eBPF 内核映射(maps)和共享内存(perf events),以及定义和使用 eBPF 内核帮助函数(helpers)。 编写 eBPF 程序的代码:这包括编写 eBPF 程序的主要逻辑,实现 eBPF 内核映射的读写操作,以及使用 eBPF 内核帮助函数。 编译 eBPF 程序:这包括使用 eBPF 编译器(例如 clang)将 eBPF 程序代码编译为 eBPF 字节码,并生成可执行的 eBPF 内核模块。ecc 本质上也是调用 clang 编译器来编译 eBPF 程序。 加载 eBPF 程序到内核:这包括将编译好的 eBPF 内核模块加载到 Linux 内核中,并将 eBPF 程序附加到指定的内核事件上。 使用 eBPF 程序:这包括监测 eBPF 程序的运行情况,并使用 eBPF 内核映射和共享内存进行数据交换和共享。 在实际开发中,还可能需要进行其他的步骤,例如配置编译和加载参数,管理 eBPF 内核模块和内核映射,以及使用其他高级功能等。 需要注意的是,BPF 程序的执行是在内核空间进行的,因此需要使用特殊的工具和技术来编写、编译和调试 BPF 程序。eunomia-bpf 是一个开源的 BPF 编译器和工具包,它可以帮助开发者快速和简单地编写和运行 BPF 程序。 您还可以访问我们的教程代码仓库 https://github.com/eunomia-bpf/bpf-developer-tutorial 以获取更多示例和完整的教程,全部内容均已开源。我们会继续分享更多有关 eBPF 开发实践的内容,帮助您更好地理解和掌握 eBPF 技术。","breadcrumbs":"eBPF Hello World,基本框架和开发流程 » 总结","id":"22","title":"总结"},"220":{"body":"在一个终端中,执行以下命令来启动 sslsniff: sudo ./sslsniff","breadcrumbs":"使用 eBPF 用户态捕获多种库的 SSL/TLS 明文数据 » 启动 sslsniff","id":"220","title":"启动 sslsniff"},"221":{"body":"在另一个终端中,执行: curl https://example.com 正常情况下,你会看到类似以下的输出: Example Domain ... ...
","breadcrumbs":"使用 eBPF 用户态捕获多种库的 SSL/TLS 明文数据 » 执行 CURL 命令","id":"221","title":"执行 CURL 命令"},"222":{"body":"当执行 curl 命令后,sslsniff 会显示以下内容: READ/RECV 0.132786160 curl 47458 1256 ----- DATA ----- ...
Example Domain
...