HTTP

代码参考这里:
https://github.com/feiskyer/ebpf-apps/tree/main/bpf-apps

生成内核头文件:

1
sudo bpftool btf dump file /sys/kernel/btf/vmlinux format c > vmlinux.h

bpftool 下载连接:

http_trace.h 头文件

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
#ifndef __HTTP_TRACE_H
#define __HTTP_TRACE_H

#define MAX_LENGTH 100

struct event_t {
	__u32 saddr;
	__u32 daddr;
	__u16 sport;
	__u16 dport;
	__u32 payload_length;
	__u8 payload[MAX_LENGTH];
};

#endif				/* __HTTP_TRACE_H */

内核态跟踪 http_trace.bpf.c

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
#include "vmlinux.h"
#include <bpf/bpf_helpers.h>
#include <bpf/bpf_endian.h>
#include "http_trace.h"

#define ETH_HLEN 14
#define ETH_P_IP 0x0800		/* Internet Protocol packet     */
#define IP_MF 0x2000		/* More Fragments */
#define IP_OFFSET 0x1FFF	/* Mask for fragmenting bits */

struct {
	__uint(type, BPF_MAP_TYPE_RINGBUF);
	__uint(max_entries, 256 * 1024);
} events SEC(".maps");

static inline int ip_is_fragment(struct __sk_buff *skb)
{
	__u16 frag_off;
	bpf_skb_load_bytes(skb, ETH_HLEN + offsetof(struct iphdr, frag_off),
			   &frag_off, sizeof(frag_off));
	frag_off = bpf_ntohs(frag_off);
	return frag_off & (IP_MF | IP_OFFSET);
}

SEC("socket")
int http_trace(struct __sk_buff *skb)
{
	struct event_t *event;
	__u8 ip_proto;
	__u16 h_proto;

	// 只跟踪 IP 协议的数据包
	bpf_skb_load_bytes(skb, offsetof(struct ethhdr, h_proto), &h_proto, 2);
	if (h_proto != bpf_htons(ETH_P_IP)) {
		return 0;
	}

	// 如果是分片包则不跟踪
	if (ip_is_fragment(skb)) {
		return 0;
	}

	// 只跟踪 TCP 协议的数据包
	bpf_skb_load_bytes(skb, ETH_HLEN + offsetof(struct iphdr, protocol),
			   &ip_proto, 1);
	if (ip_proto != IPPROTO_TCP) {
		return 0;
	}

	// 计算IP头部长度(ihl单位为4字节,所以需要乘以4)
	struct iphdr iph;
	bpf_skb_load_bytes(skb, ETH_HLEN, &iph, sizeof(iph));
	__u32 ip_total_length = iph.tot_len;
	__u32 iph_len = iph.ihl;
	iph_len = iph_len << 2;

	// 根据TCP数据偏移(doff)计算TCP头部长度(doff单位为4字节,所以需要乘以4)
	struct tcphdr tcph;
	bpf_skb_load_bytes(skb, ETH_HLEN + sizeof(iph), &tcph, sizeof(tcph));
	__u32 tcp_hlen = tcph.doff;
	tcp_hlen = tcp_hlen << 2;

	// 只跟踪 TCP 80 端口的数据包
	if (tcph.source != bpf_htons(80) && tcph.dest != bpf_htons(80)) {
		return 0;
	}

	// 计算HTTP payload的偏移和长度
	__u32 payload_offset = ETH_HLEN + iph_len + tcp_hlen;
	__u32 payload_length = bpf_ntohs(ip_total_length) - iph_len - tcp_hlen;
	// HTTP 报文最短为7个字节
	if (payload_length < 7) {
		return 0;
	}

	// 只跟踪 GET、POST、PUT、DELETE 方法的数据包
	// HTTP 开头的数据包是服务器端的响应
	char start_buffer[7] = { };
	bpf_skb_load_bytes(skb, payload_offset, start_buffer, 7);
	if (bpf_strncmp(start_buffer, 3, "GET") != 0 &&
	    bpf_strncmp(start_buffer, 4, "POST") != 0 &&
	    bpf_strncmp(start_buffer, 3, "PUT") != 0 &&
	    bpf_strncmp(start_buffer, 6, "DELETE") != 0 &&
	    bpf_strncmp(start_buffer, 4, "HTTP") != 0) {
		return 0;
	}

	// 读取HTTP信息并将其提交到环形缓冲区
	event = bpf_ringbuf_reserve(&events, sizeof(*event), 0);
	if (!event) {
		return 0;
	}
	event->sport = bpf_ntohs(tcph.source);
	event->dport = bpf_ntohs(tcph.dest);
	event->payload_length = payload_length;
	bpf_skb_load_bytes(skb, payload_offset, event->payload, sizeof(event->payload));
	bpf_skb_load_bytes(skb, ETH_HLEN + offsetof(struct iphdr, saddr),
			   &event->saddr, 4);
	bpf_skb_load_bytes(skb, ETH_HLEN + offsetof(struct iphdr, daddr),
			   &event->daddr, 4);
	bpf_ringbuf_submit(event, 0);

	return 0;
}

char _license[] SEC("license") = "Dual BSD/GPL";

用户态代码 http_trace.c

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <signal.h>
#include <arpa/inet.h>
#include <linux/if_ether.h>
#include <linux/if_packet.h>
#include <net/if.h>
#include <arpa/inet.h>
#include <sys/resource.h>
#include <bpf/libbpf.h>
#include <sys/socket.h>
#include <time.h>

#include "http_trace.h"
#include "http_trace.skel.h"

// 用于打印调试信息
static int libbpf_print_fn(enum libbpf_print_level level, const char *format,
			   va_list args)
{
#ifdef DEBUGBPF
	return vfprintf(stderr, format, args);
#else
	return 0;
#endif
}

static volatile bool exiting = false;

static void sig_handler(int signo)
{
	exiting = true;
}

// 创建原始套接字
static inline 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) {
		printf("cannot 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) {
		printf("bind to %s: %s\n", name, strerror(errno));
		close(sock);
		return -1;
	}

	return sock;
}

// 输出 HTTP 请求和响应信息(注意:长度截断至MAX_LENGTH)
static int handle_event(void *ctx, void *data, size_t data_sz)
{
	const struct event_t *e = data;
	char saddr[16] = { }, daddr[16] = { };

	inet_ntop(AF_INET, &e->saddr, saddr, sizeof(saddr));
	inet_ntop(AF_INET, &e->daddr, daddr, sizeof(daddr));
	printf("%s:%d -> %s:%d (length: %d)\n%s\n\n", saddr, e->sport, daddr,
	       e->dport, e->payload_length, e->payload);
	return 0;
}

// 提升RLIMIT_MEMLOCK以允许BPF子系统执行任何需要的操作
static void bump_memlock_rlimit(void)
{
	struct rlimit rlim_new = {
		.rlim_cur = RLIM_INFINITY,
		.rlim_max = RLIM_INFINITY,
	};

	if (setrlimit(RLIMIT_MEMLOCK, &rlim_new)) {
		fprintf(stderr, "Failed to increase RLIMIT_MEMLOCK limit!\n");
		exit(1);
	}
}

int main(int argc, char **argv)
{
	struct http_trace_bpf *skel;
	struct ring_buffer *rb = NULL;
	int err = 0;

	// 设置libbpf的错误和调试信息回调
	libbpf_set_print(libbpf_print_fn);

	// 注册信号处理程序
	signal(SIGINT, sig_handler);
	signal(SIGTERM, sig_handler);

	// 提升RLIMIT_MEMLOCK以允许BPF子系统执行任何需要的操作
	bump_memlock_rlimit();

	// 加载BPF程序
	skel = http_trace_bpf__open_and_load();
	if (!skel) {
		fprintf(stderr, "Failed to open and load BPF skeleton\n");
		return 1;
	}
	// 创建ring buffer并绑定事件处理回调
	rb = ring_buffer__new(bpf_map__fd(skel->maps.events), handle_event,
			      NULL, NULL);
	if (!rb) {
		fprintf(stderr, "Failed to create ring buffer\n");
		goto cleanup;
	}
	// 将eBPF程序挂载到原始套接字
	int sock = open_raw_sock("eth0");
	if (sock < 0) {
		fprintf(stderr, "Failed to open raw socket\n");
		goto cleanup;
	}
	int prog_fd = bpf_program__fd(skel->progs.http_trace);
	if (setsockopt
	    (sock, SOL_SOCKET, SO_ATTACH_BPF, &prog_fd, sizeof(prog_fd))) {
		fprintf(stderr, "Failed to attach eBPF prog\n");
		goto cleanup;
	}
	// 从ring buffer中读取数据
	printf("Tracing HTTP traffic... Hit Ctrl-C to end.\n");
	while (!exiting) {
		err = ring_buffer__poll(rb, 100);
		if (err == -EINTR) {
			err = 0;
			break;
		}
		if (err < 0) {
			fprintf(stderr, "Error polling ring buffer: %d\n", err);
			break;
		}
	}

 cleanup:
	// 释放资源
	ring_buffer__free(rb);
	http_trace_bpf__destroy(skel);
	return -err;
}

编译,生成骨架文件

1
2
clang -g -O2 -target bpf -c http_trace.bpf.c -D__TARGET_ARCH_x86_64 -I/usr/include/x86_64-linux-gnu -I. -o http_trace.bpf.o
bpftool gen skeleton http_trace.bpf.o > http_trace.skel.h

用户态编译:

1
clang -g -O2 -Wall -I . -c http_trace.c -o http_trace.o

生成可执行文件

1
clang -Wall -O2 -g  http_trace.o  -lbpf -lelf -lz -o http_trace

测试效果

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
./http_trace
Tracing HTTP traffic... Hit Ctrl-C to end.
192.168.223.135:46100 -> 220.181.7.203:80 (length: 72)


220.181.7.203:80 -> 192.168.223.135:46100 (length: 195)
HTTP/1.1 200 OK
Content-Length: 81
Content-Type: text/html
Server: bfe
Date: Sun, 31 Aug 2025 09

HTTPS

头文件 https_trace.h

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
#ifndef __HTTPS_TRACE_H
#define __HTTPS_TRACE_H

// #include <linux/types.h>  // Added for __u32, __u8
#include "vmlinux.h"

#define COMM_LEN 32
#define MAX_BUF_LENGTH 8192

struct event_t {
    __u32 pid;
    __u32 uid;
    __u8 buf[MAX_BUF_LENGTH];
    char comm[COMM_LEN];
    __u32 len;
    __u8 rw;  // 0: read, 1: write
};

#endif  // __HTTPS_TRACE_H

内核态 https_trace.bpf.c

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
#include "vmlinux.h"
#include <bpf/bpf_helpers.h>
#include <bpf/bpf_endian.h>
#include <bpf/bpf_tracing.h>
#include "https_trace.h"


struct {
        __uint(type, BPF_MAP_TYPE_PERF_EVENT_ARRAY);
        __uint(key_size, sizeof(__u32));
        __uint(value_size, sizeof(__u32));
        __uint(max_entries, 1024);
}
events SEC(".maps");


struct {
        __uint(type, BPF_MAP_TYPE_PERCPU_ARRAY);
        __type(key, __u32);
        __type(value, struct event_t);
         __uint(max_entries, 1);
} data_buffer_heap SEC(".maps");


struct {
        __uint(type, BPF_MAP_TYPE_HASH);
        __uint(max_entries, 1024);
        __type(key, __u32);
        __type(value, __u64);
} bufs SEC(".maps");


static int SSL_rw_entry(struct pt_regs *ctx, void *ssl, void *buf, int num)
{
        u64 pid_tgid = bpf_get_current_pid_tgid();
        u32 tid = (u32) pid_tgid;
        bpf_map_update_elem(&bufs, &tid, (u64 *) & buf, BPF_ANY);
        return 0;
}

static int SSL_rw_exit(struct pt_regs *ctx, int rw)
{
        u64 pid_tgid = bpf_get_current_pid_tgid();
        u32 pid = pid_tgid >> 32;
        u32 tid = (u32) pid_tgid;


        u64 *bufp = bpf_map_lookup_elem(&bufs, &tid);
        if (!bufp) {
                return 0;
        }

        int len = PT_REGS_RC(ctx);
        if (len <= 0) {
                return 0;
        }

        __u32 zero = 0;
        struct event_t *event = bpf_map_lookup_elem(&data_buffer_heap, &zero);
        if (!event) {
                return 0;
        }

        event->rw = rw;
        event->pid = pid;
        event->uid = bpf_get_current_uid_gid();
        bpf_get_current_comm(&event->comm, sizeof(event->comm));

        event->len =
            (size_t)MAX_BUF_LENGTH <
            (size_t)len ? (size_t)MAX_BUF_LENGTH : (size_t)len;
        if (bufp != NULL) {
                bpf_probe_read_user(event->buf, event->len,
                                    (const char *)*bufp);
        }
        bpf_map_delete_elem(&bufs, &tid);
        bpf_perf_event_output(ctx, &events, BPF_F_CURRENT_CPU, event,
                              sizeof(struct event_t));
        return 0;
}

SEC("uprobe/SSL_read")
int BPF_UPROBE(probe_SSL_read_entry, void *ssl, void *buf, int num)
{
        return SSL_rw_entry(ctx, ssl, buf, num);
}

SEC("uretprobe/SSL_read")
int BPF_URETPROBE(probe_SSL_read_exit)
{
        return SSL_rw_exit(ctx, 0);
}

SEC("uprobe/SSL_write")
int BPF_UPROBE(probe_SSL_write_entry, void *ssl, void *buf, int num)
{
        return SSL_rw_entry(ctx, ssl, buf, num);
}

SEC("uretprobe/SSL_write")
int BPF_URETPROBE(probe_SSL_write_exit)
{
        return SSL_rw_exit(ctx, 1);
}

char _license[] SEC("license") = "Dual BSD/GPL";

用户态 https_trace.c

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <signal.h>
#include <arpa/inet.h>
#include <linux/if_ether.h>
#include <linux/if_packet.h>
#include <net/if.h>
#include <arpa/inet.h>
#include <sys/resource.h>
#include <bpf/libbpf.h>
#include <sys/socket.h>
#include <time.h>

#include "https_trace.h"
#include "https_trace.skel.h"

// 用于打印调试信息
static int libbpf_print_fn(enum libbpf_print_level level, const char *format,
                           va_list args)
{
#ifdef DEBUGBPF
        return vfprintf(stderr, format, args);
#else
        return 0;
#endif
}

static volatile bool exiting = false;

static void sig_handler(int signo)
{
        exiting = true;
}

// 以可读格式打印字符数组
void print_chars(const char *array, size_t size)
{
        size_t trimmed_size = size;
        for (trimmed_size = size; trimmed_size > 0; trimmed_size--) {
                if (array[trimmed_size - 1] != 0) {
                        break;
                }
        }
        for (size_t i = 0; i < trimmed_size; i++) {
                printf("%c", (unsigned char)array[i]);
        }
        printf("\n\n");
}

// 输出 HTTPS 请求和响应信息(注意:长度截断至MAX_BUF_LENGTH)
static void handle_event(void *ctx, int cpu, void *data, __u32 data_size)
{
        const struct event_t *e = data;
        if (e->len > 0) {
                printf("====================================\n");
                char buf[MAX_BUF_LENGTH + 1] = { 0 };
                memcpy(buf, e->buf, e->len);
                printf("%s\t%s\n\n", e->comm, e->rw ? "write" : "read");
                print_chars(buf, sizeof(buf));
        }
}

// 提升RLIMIT_MEMLOCK以允许BPF子系统执行任何需要的操作
static void bump_memlock_rlimit(void)
{
        struct rlimit rlim_new = {
                .rlim_cur = RLIM_INFINITY,
                .rlim_max = RLIM_INFINITY,
        };

        if (setrlimit(RLIMIT_MEMLOCK, &rlim_new)) {
                fprintf(stderr, "Failed to increase RLIMIT_MEMLOCK limit!\n");
                exit(1);
        }
}

// 查找库的路径
char *find_library_path(const char *libname)
{
        char cmd[256];
        static char path[256];
        snprintf(cmd, sizeof(cmd), "ldconfig -p | grep %s", libname);
        FILE *fp = popen(cmd, "r");
        if (!fp) {
                fprintf(stderr, "Failed to run command: %s\n", cmd);
                return NULL;
        }
        // 格式: libssl3.so (libc6,x86-64) => /lib/x86_64-linux-gnu/libssl3.so
        if (fgets(path, sizeof(path) - 1, fp) != NULL) {
                char *p = strrchr(path, '>');
                if (p && *(p + 1) == ' ') {
                        memmove(path, p + 2, strlen(p + 2) + 1);
                        char *end = strchr(path, '\n');
                        if (end) {
                                *end = '\0';
                        }
                        pclose(fp);
                        return path;
                }
        }

        pclose(fp);
        return NULL;
}

int main(int argc, char **argv)
{
        struct https_trace_bpf *skel;
        struct perf_buffer *pb = NULL;
        int err = 0;

        // 设置libbpf的错误和调试信息回调
        libbpf_set_print(libbpf_print_fn);

        // 注册信号处理程序
        signal(SIGINT, sig_handler);
        signal(SIGTERM, sig_handler);

        // 提升RLIMIT_MEMLOCK以允许BPF子系统执行任何需要的操作
        bump_memlock_rlimit();

        // 查找OpenSSL库的路径
        char *libssl_path = find_library_path("libssl.so");
        if (!libssl_path) {
                fprintf(stderr, "Failed to find libssl.so\n");
                return 1;
        }
        // 加载BPF程序
        skel = https_trace_bpf__open_and_load();
        if (!skel) {
                fprintf(stderr, "Failed to open and load BPF skeleton\n");
                return 1;
        }
        // 创建buffer并绑定事件处理回调
        pb = perf_buffer__new(bpf_map__fd(skel->maps.events), 16,
                              handle_event, NULL, NULL, NULL);
        if (!pb) {
                fprintf(stderr, "Failed to create perf buffer\n");
                goto cleanup;
        }
        // 挂载uprobe到OpenSSL库
        printf("Attaching uprobe to %s\n", libssl_path);
        // SSL_read
        LIBBPF_OPTS(bpf_uprobe_opts, uprobe_ropts,.func_name = "SSL_read");
        skel->links.probe_SSL_read_entry =
            bpf_program__attach_uprobe_opts(skel->progs.probe_SSL_read_entry,
                                            -1, libssl_path, 0, &uprobe_ropts);
        LIBBPF_OPTS(bpf_uprobe_opts, uprobe_ropts_ret,.func_name =
                    "SSL_read",.retprobe = true);
        skel->links.probe_SSL_read_exit =
            bpf_program__attach_uprobe_opts(skel->progs.probe_SSL_read_exit, -1,
                                            libssl_path, 0, &uprobe_ropts_ret);
        // SSL_write
        LIBBPF_OPTS(bpf_uprobe_opts, uprobe_wopts,.func_name = "SSL_write");
        skel->links.probe_SSL_write_entry =
            bpf_program__attach_uprobe_opts(skel->progs.probe_SSL_write_entry,
                                            -1, libssl_path, 0, &uprobe_wopts);
        LIBBPF_OPTS(bpf_uprobe_opts, uprobe_wopts_ret,.func_name =
                    "SSL_write",.retprobe = true);
        skel->links.probe_SSL_write_exit =
            bpf_program__attach_uprobe_opts(skel->progs.probe_SSL_write_exit,
                                            -1, libssl_path, 0,
                                            &uprobe_wopts_ret);

        // 从Buffer中读取数据
        printf("Tracing HTTPS traffic... Hit Ctrl-C to end.\n");
        while (!exiting) {
                err = perf_buffer__poll(pb, 100);
                if (err == -EINTR) {
                        err = 0;
                        break;
                }
                if (err < 0) {
                        fprintf(stderr, "Error polling perf buffer: %d\n", err);
                        break;
                }
        }

 cleanup:
        // 释放资源
        perf_buffer__free(pb);
        https_trace_bpf__destroy(skel);
        return -err;
}

编译

1
2
3
clang -g -O2 -target bpf -c https_trace.bpf.c    -D__TARGET_ARCH_x86   -I/usr/include/x86_64-linux-gnu  -I. -o https_trace.bpf.o

bpftool gen skeleton https_trace.bpf.o > https_trace.skel.h

参考