eBPF流量捕获实践

前言

就目前而言,tcpdump是捕获生产中流量最常见的解决方案之一。但是缺点是它不允许应用程序级别的过滤(L7过滤),因此每当捕获相关的HTTP会话时,最终需要存储数百兆字节的流量通过。另一种解决方案是在源代码中添加一个算法来查找相关的HTTP会话,但这需要在生产中进行代码检测,并且无法达到非侵入式可观测性。

eBPF应运而生。eBPF是Linux应用程序在Linux内核空间执行代码的一种机制。使用eBPF实现流量捕获远远超出一些标准的解决方案(WireShark、Fiddler和tcpdump)

eBPF允许添加多个过滤层并直接从内核捕获流量,从而显著减少相关数据的输出量,并确保可以高吞吐量的处理应用程序流量。

实验环境

  • Ubuntu20.04
  • BCC v0.21.0
  • GO v1.19

请求流程分析

首先通过GO的Gin框架搭建一个简单的HTTP服务器。源码如下

server.go

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
package main

import (
"fmt"
"log"
"math/rand"
"net/http"
"os"
"time"

"github.com/gin-gonic/gin"
)

const (
defaultPort = "8080"
maxPayloadSize = 10 * 1024 * 1024 // 10 MB
)

var (
// source is a static, global rand object.
source *rand.Rand
letterBytes = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890~!@#$"
)

func randStringBytes(n int) string {
b := make([]byte, n)
for i := range b {
b[i] = letterBytes[source.Intn(len(letterBytes))]
}
return string(b)
}

func init() {
source = rand.New(rand.NewSource(time.Now().UnixNano()))
}

// customResponse holds the requested size for the response payload.
type customResponse struct {
Size int `json:"size"`
}

func postCustomResponse(context *gin.Context) {
var customResp customResponse
if err := context.BindJSON(&customResp); err != nil {
_ = context.AbortWithError(http.StatusBadRequest, err)
return
}

if customResp.Size > maxPayloadSize {
_ = context.AbortWithError(http.StatusBadRequest, fmt.Errorf("requested size %d is bigger than max allowed %d", customResp, maxPayloadSize))
return
}

context.JSON(http.StatusOK, map[string]string{"answer": randStringBytes(customResp.Size)})
}

func main() {
engine := gin.New()

engine.Use(gin.Recovery())
engine.POST("/customResponse", postCustomResponse)

port := os.Getenv("PORT")
if port == "" {
port = defaultPort
}

fmt.Printf("listening on 0.0.0.0:%s\n", port)
if err := engine.Run(fmt.Sprintf("0.0.0.0:%s", port)); err != nil {
log.Fatal(err)
}
}

执行代码

1
2
3
root@ubuntu:/home/caoyifan/eBPF-Sniffer# go run server.go
server.go:11:2: no required module provides package github.com/gin-gonic/gin; to add it:
go get github.com/gin-gonic/gin

如上因为没有安装gin框架,因此我们先安装gin框架。修改代理后使用go get命令

1
2
3
export GOPROXY="https://goproxy.cn"
go mod init xx
go mod tidy

下载完成后运行server服务

1
2
3
4
5
6
7
8
9
10
root@ubuntu:/home/caoyifan/eBPF-Sniffer# go run server.go
[GIN-debug] [WARNING] Running in "debug" mode. Switch to "release" mode in production.
- using env: export GIN_MODE=release
- using code: gin.SetMode(gin.ReleaseMode)

[GIN-debug] POST /customResponse --> main.postCustomResponse (2 handlers)
listening on 0.0.0.0:8080
[GIN-debug] [WARNING] You trusted all proxies, this is NOT safe. We recommend you to set a value.
Please check https://pkg.go.dev/github.com/gin-gonic/gin#readme-don-t-trust-all-proxies for details.
[GIN-debug] Listening and serving HTTP on 0.0.0.0:8080

本地发送POST请求,服务器接收到POST请求后会响应随机生成的Payload。

1
2
root@ubuntu:/home/caoyifan# curl -X POST http://localhost:8080/customResponse -d '{"size": 100}'
{"answer":"Gs#UjI7u2kiOmSyJDkw7JAi3Y~Z4fauPaeThxct14qoweIUdiwDsB#9PUuvPgayVGfXQcc$E9itK1tlo5twE$cuGRvge4S~H2cS6"}

如果我们需要通过eBPF去捕获完整的HTTP请求,第一步需要了解到本次请求使用了哪些系统调用,因此我们可以使用strace工具进行查看。

通过如下命令运行server服务

1
sudo strace -f -o syscalls_dump.txt go run server.go
  • -f:从服务器的线程中捕获系统调用
  • -o:将结果写入到文件中

接着再次运行上述POST请求

1
2
root@ubuntu:/home/caoyifan# curl -X POST http://localhost:8080/customResponse -d '{"size": 100}'
{"answer":"Gs#UjI7u2kiOmSyJDkw7JAi3Y~Z4fauPaeThxct14qoweIUdiwDsB#9PUuvPgayVGfXQcc$E9itK1tlo5twE$cuGRvge4S~H2cS6"}

查看syscalls_dump.txt文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
2507558 accept4(3,  <unfinished ...>
2507562 epoll_pwait(4, <unfinished ...>
2507559 nanosleep({tv_sec=0, tv_nsec=20000}, <unfinished ...>
2507558 <... accept4 resumed> 0xc00031fa28, [112], SOCK_CLOEXEC|SOCK_NONBLOCK) = -1 EAGAIN (Resource temporarily unavailable)
2507562 <... epoll_pwait resumed> [{EPOLLIN|EPOLLOUT, {u32=3021211368, u64=140633135384296}}], 128, 0, NULL, 0) = 1
2507562 epoll_pwait(4, <unfinished ...>
2507559 <... nanosleep resumed> NULL) = 0
2507559 nanosleep({tv_sec=0, tv_nsec=20000}, <unfinished ...>
2507558 read(7, "POST /customResponse HTTP/1.1\r\nH"..., 4096) = 175
2507558 write(7, "HTTP/1.1 200 OK\r\nContent-Type: a"..., 237 <unfinished ...>
2507559 <... nanosleep resumed> NULL) = 0
2507559 nanosleep({tv_sec=0, tv_nsec=20000}, NULL) = 0
2507558 futex(0xc000080148, FUTEX_WAKE_PRIVATE, 1 <unfinished ...>
2507559 nanosleep({tv_sec=0, tv_nsec=20000}, <unfinished ...>
2507558 <... futex resumed> ) = 1
2507561 <... futex resumed> ) = 0
2507559 <... nanosleep resumed> NULL) = 0
2507561 nanosleep({tv_sec=0, tv_nsec=3000}, <unfinished ...>
2507559 nanosleep({tv_sec=0, tv_nsec=20000}, <unfinished ...>
2507561 <... nanosleep resumed> NULL) = 0
2507559 <... nanosleep resumed> NULL) = 0
....
2507558 close(7 <unfinished ...>

通过上述文件可以看到。服务器首先使用accept4系统调用来接受一个新的连接。整个请求服务器的调用流程如下:

  • accept4:使用系统调用接受新连接
  • read:使用套接字文件描述符(fd)上的系统调用从套接字读取内容
  • write:使用套接字文件描述符(fd)上的系统调用将响应写入套接字
  • close:使用系统调用关闭文件描述符

内核代理eBPF实现

我们需要通过eBPF hook 8个钩子,分别是accept4、read、write、close的进入和退出钩子。程序通过C语言编写,我们会通过所有的钩子组合来执行完整的捕获过程。

在大多数情况下,eBPF程序由执行hook的内核代理和处理从内核发送的事件的用户代理组成。在一些其他用例中,可能只有内核代理(如阻止恶意流量的防火墙)

首先我们需要挂载accept4系统调用。在eBPF程序中,我们可以在每个系统调用的进入和退出放置钩子。这对于获取系统调用的输入参数很有用。

在如下代码片段中,我们声明结构体以将系统调用的输入参数保存在系统调用的入口中,并在accept4系统调用的出口处使用。

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
// 保存系统调用的addr参数结构
struct accept_args_t {
struct sockaddr_in* addr;
};

// map映射,在入口和返回钩子之间缓存接受系统调用的输入参数
BPF_HASH(active_accept_args_map, uint64_t, struct accept_args_t);

// 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) {
// 获取相关pid中线程唯一id,这样可以链接同一线程的不同调用。
uint64_t id = bpf_get_current_pid_tgid();

// 将addr保存在map中,方便accept4退出时使用
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();

// 从map中获取addr
struct accept_args_t* accept_args = active_accept_args_map.lookup(&id);
// 如果map映射中存在 id,我们将获得一个非空指针,该指针保存来自系统调用条目的输入地址参数。
if (accept_args != NULL) {
process_syscall_accept(ctx, id, accept_args);
}

// 最后清理map
active_accept_args_map.delete(&id);
return 0;
}

由于在系统调用进入期间我们无法知道系统调用是否会成功,并且在系统调用退出期间我们无法访问输入参数,因此我们需要存储参数。这里我们使用的逻辑是process_syscall_accept,该函数会检查系统调用是否成功完成,然后我们会将链接信息保存在全局map中,以便后续的系统调用(read、write、close)使用。

在下面的代码中,我们创建了accept4hook使用的函数。并在我们自己的map映射中注册到服务器上的任何新的连接。

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
// 一个结构体,表示由 pid、fd和结构体timestamp组成的唯一ID。
// descriptor and the creation time of the struct.
struct conn_id_t {
// 进程ID
uint32_t pid;
// 打开的网络连接的fd
int32_t fd;
// 结构体初始化时的时间戳
uint64_t tsid;
};

// 该结构包含建立连接时通过accept4() 系统调用收集的信息。
struct conn_info_t {
// 连接标识符
struct conn_id_t conn_id;

// 在此连接上写入/读取的字节数
int64_t wr_bytes;
int64_t rd_bytes;

// 指示我们将连接识别为HTTP的标志
bool is_http;
};

// 发送到用户代理的事件结构
struct socket_open_event_t {
// 事件发生的事件
uint64_t timestamp_ns;
// 连接的唯一ID
struct conn_id_t conn_id;
// 客户端地址
struct sockaddr_in addr;
};

// 活动连接的映射。map名字是conn_info_map
BPF_HASH(conn_info_map, uint64_t, struct conn_info_t, 131072);
// 一个性能缓冲区,允许我们将事件从内核发送到用户模式
// 该性能缓冲区专用于特殊类型的事件-打开事件
BPF_PERF_OUTPUT(socket_open_events);

// 检查系统调用是否成功完成以及是否将新连接保存在专用连接映射中的帮助函数
static __inline void process_syscall_accept(struct pt_regs* ctx, uint64_t id, const struct accept_args_t* args) {
// 提取返回码,检查是否失败,如果是,直接终止。
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;
// 将连接信息保存在全局map中,因此在其他系统调用(read、write和close)中,我们将能够知道已经看到了连接
conn_info_map.update(&pid_fd, &conn_info);

// 向用户模式发送一个打开事件,让用户模式知道我们已经识别了一个新的连接。
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));
}

到此,我们能够在内核侧识别新的连接并将信息发送至用户代理。接下来,我们将hook read系统调用

在下面的代码中,我们将hook read系统调用

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
// 一个辅助结构,用于缓存入口钩子和出口钩子之间读/写系统调用的输入参数。
struct data_args_t {
int32_t fd;
const char* buf;
};

// map 存储入口和出口挂钩之间的读取系统调用参数。
BPF_HASH(active_read_args_map, uint64_t, struct data_args_t);

// 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();

// 存储参数
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();

// 系统调用的返回码也是读取的字节数。
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 是一个枚举值,让 process_data函数知道输入缓冲区是传入还是传出。
process_data(ctx, id, kIngress, read_args, bytes_count);
}

// 最后清理map
active_read_args_map.delete(&id);
return 0;
}

在下面的代码中,我们创建了辅助函数来处理readwrite系统调用。

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
// 数据缓冲区消息大小,BPF最多可以将这个数量的数据提交到perf缓冲区。
//内核大小限制为32kb
#define MAX_MSG_SIZE 30720 // 30KiB

struct socket_data_event_t {

struct attr_t {
// 系统调用完成时的时间戳
uint64_t timestamp_ns;

// 连接标识符 (PID, FD, etc.).
struct conn_id_t conn_id;

// msg 字段编码的实际数据的类型,调用者使用它来确定如何解释数据。
enum traffic_direction_t direction;

// 原始消息的大小。我们使用它来截断 msg 字段以最小化正在传输的数据量。
uint32_t msg_size;

// 连接上此事件的从 0 开始的位置编号,以字节位置表示。
// 该位置是该消息的第一个字节。
uint64_t pos;
} attr;
char msg[MAX_MSG_SIZE];
};

// Perf缓冲区向用户模式代理发送数据事件
BPF_PERF_OUTPUT(socket_data_events);

...

// 处理读/写系统调用的辅助函数
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) {
// 在访问指针之前始终检查对指针的访问
if (args->buf == NULL) {
return;
}

// 对于 read 和 write 系统调用,返回码是写入或读取的字节数,因此零表示没有写入或读取任何内容,负数表示系统调用失败。
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) {
// 正在读/写的fd不代表IPV4套接字fd
return;
}

// 检查连接是否已经是HTTP或者检查是否是新连接,如果是HTTP,则返回true
if (is_http_connection(conn_info, args->buf, bytes_count)) {
// 分配新事件
uint32_t kZero = 0;
struct socket_data_event_t* event = socket_data_event_buffer_heap.lookup(&kZero);
if (event == NULL) {
return;
}

// 填充数据事件的元数据
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);
}

// 更新conn_info总写入/读取的字节数
switch (direction) {
case kEgress:
conn_info->wr_bytes += bytes_count;
break;
case kIngress:
conn_info->rd_bytes += bytes_count;
break;
}
}

由此可以看到,我们的辅助函数会检查read或者write系统调用是否成功完成。通过检查 读取(写入)字节,检查正在读取(或写入)的数据是否为HTTP。如果是,则发送它到用户模式代理并作为一个事件。

然后会快速转到write系统调用。

最后代码处理close事件。在下面的代码中,我们创建了辅助函数来处理close系统调用。

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
//发送到用户模式代理的关闭事件的结构
struct socket_close_event_t {
// close系统调用的时间戳
uint64_t timestamp_ns;
// 连接的唯一id
struct conn_id_t conn_id;
// 在该连接上写入的总字节数
int64_t wr_bytes;
// 在该连接上读取的总字节数
int64_t rd_bytes;
};

// Perf缓冲区向用户模式代理发送close事件
BPF_PERF_OUTPUT(socket_close_events);

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);
// 如果系统调用失败,直接return
if (ret_val < 0) {
return;
}

uint32_t pid = id >> 32;
uint64_t pid_fd = ((uint64_t)pid << 32) | (uint32_t)close_args->fd;
struct conn_info_t* conn_info = conn_info_map.lookup(&pid_fd);
if (conn_info == NULL) {
// 正在关闭的fd,并不代表IPV4套接字fd
return;
}

// 向用户模式代理发送一个指示连接已关闭的事件
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));

// 从映射中删除连接
conn_info_map.delete(&pid_fd);
}

用户代理Go实现

用户模式代理使用gobpf库编写。第一步是编译代码。

1
2
bpfModule := bcc.NewModule(string(bpfSourceCodeContent), nil)
defer bpfModule.Close()

然后创建一个连接工厂,负责保存所有连接实例并打印就绪连接并删除不活动或格式错误的连接。

1
2
3
4
5
6
7
8
9
// 创建连接工厂并将 1分钟设置为不活动阈值,这意味着在最后一分钟内未收到任何事件的连接将被关闭。
connectionFactory := connections.NewFactory(time.Minute)
// 启动一个goroutine。每 10 秒运行一次并打印就绪连接并删除非活动或格式错误的连接。
go func() {
for {
connectionFactory.HandleReadyConnections()
time.Sleep(10 * time.Second)
}
}()

加载 perf 缓冲区处理程序

1
2
3
if err := bpfwrapper.LaunchPerfBufferConsumers(bpfModule, connectionFactory); err != nil {
log.Panic(err)
}

以下是关于单用户模式perf缓冲区处理程序的说明。

每个处理程序通过管道(inputChan)获取事件,并且每个事件的类型为字节数组([]byte),对于每个事件,我们需要将其转换为go的数据结构表示。

1
2
3
4
5
6
7
8
9
10
11
// ConnID is a conversion of the following C-Struct into GO.
// struct conn_id_t {
// uint32_t pid;
// int32_t fd;
// uint64_t tsid;
// };.
type ConnID struct {
PID uint32
FD int32
TsID uint64
}

同时我们修复了事件的时间戳,因为内核模式返回单调时钟而不是实时时钟,最后,我们使用新事件更新连接对象字段。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
func socketCloseEventCallback(inputChan chan []byte, connectionFactory *connections.Factory) {
for data := range inputChan {
if data == nil {
return
}
var event structs.SocketCloseEvent
if err := binary.Read(bytes.NewReader(data), bpf.GetHostByteOrder(), &event); err != nil {
log.Printf("Failed to decode received data: %+v", err)
continue
}
event.TimestampNano += settings.GetRealTimeOffset()
connectionFactory.GetOrCreate(event.ConnID).AddCloseEvent(event)
}
}

最后一步是attach到钩子上。

1
2
3
if err := bpfwrapper.AttachKprobes(bpfModule); err != nil {
log.Panic(err)
}

实验

非docker环境

bcc工具安装

Ubuntu - Source

To build the toolchain from source, one needs:

  • LLVM 3.7.1 or newer, compiled with BPF support (default=on)
  • Clang, built from the same tree as LLVM
  • cmake (>=3.1), gcc (>=4.7), flex, bison
  • LuaJIT, if you want Lua support
1
2
3
# For Focal (20.04.1 LTS)
sudo apt install -y bison build-essential cmake flex git libedit-dev \
libllvm12 llvm-12-dev libclang-12-dev python zlib1g-dev libelf-dev libfl-dev python3-distutils
1
2
3
4
5
6
7
8
9
10
git clone https://github.com/iovisor/bcc.git
mkdir bcc/build; cd bcc/build
cmake ..
make
sudo make install
cmake -DPYTHON_CMD=python3 .. # build python3 binding
pushd src/python/
make
sudo make install
popd

执行报错

1
2
3
4
5
root@ubuntu:/home/caoyifan/ebpf-training/workshop1/capture-traffic# go run main.go ./sourcecode.c
# github.com/iovisor/gobpf/bcc
/root/go/pkg/mod/github.com/iovisor/gobpf@v0.2.0/bcc/module.go:230:132: not enough arguments in call to (_C2func_bcc_func_load)
have (unsafe.Pointer, _Ctype_int, *_Ctype_char, *_Ctype_struct_bpf_insn, _Ctype_int, *_Ctype_char, _Ctype_uint, _Ctype_int, *_Ctype_char, _Ctype_uint, nil)
want (unsafe.Pointer, _Ctype_int, *_Ctype_char, *_Ctype_struct_bpf_insn, _Ctype_int, *_Ctype_char, _Ctype_uint, _Ctype_int, *_Ctype_char, _Ctype_uint, *_Ctype_char, _Ctype_int)

最后发现是bcc版本的问题

1
git clone -c http.proxy="http://192.168.19.16:17890" -b v0.21.0 https://github.com/iovisor/bcc.git

启动ebpf sniffer

1
2
3
4
5
6
7
8
9
10
11
12
root@ubuntu:/home/caoyifan/ebpf-training/workshop1/capture-traffic# go run main.go ./sourcecode.c
2022/09/29 08:19:13 Loading "syscall__probe_entry_accept" for "accept" as 0
2022/09/29 08:19:13 Loading "syscall__probe_ret_accept" for "accept" as 1
2022/09/29 08:19:13 Loading "syscall__probe_entry_accept4" for "accept4" as 0
2022/09/29 08:19:13 Loading "syscall__probe_ret_accept4" for "accept4" as 1
2022/09/29 08:19:13 Loading "syscall__probe_entry_write" for "write" as 0
2022/09/29 08:19:13 Loading "syscall__probe_ret_write" for "write" as 1
2022/09/29 08:19:13 Loading "syscall__probe_entry_read" for "read" as 0
2022/09/29 08:19:13 Loading "syscall__probe_ret_read" for "read" as 1
2022/09/29 08:19:13 Loading "syscall__probe_entry_close" for "close" as 0
2022/09/29 08:19:13 Loading "syscall__probe_ret_close" for "close" as 1
2022/09/29 08:19:13 Sniffer is ready

启动服务端程序

1
2
3
4
5
6
7
8
9
10
root@ubuntu:/home/caoyifan/ebpf-training/workshop1/demo-server# go run main.go
[GIN-debug] [WARNING] Running in "debug" mode. Switch to "release" mode in production.
- using env: export GIN_MODE=release
- using code: gin.SetMode(gin.ReleaseMode)

[GIN-debug] POST /customResponse --> main.postCustomResponse (2 handlers)
listening on 0.0.0.0:8080
[GIN-debug] [WARNING] You trusted all proxies, this is NOT safe. We recommend you to set a value.
Please check https://pkg.go.dev/github.com/gin-gonic/gin#readme-don-t-trust-all-proxies for details.
[GIN-debug] Listening and serving HTTP on 0.0.0.0:8080

本地发送post请求

1
2
3
caoyifan@MacBookPro [16:19:17] [~]
-> % curl -X POST http://192.168.19.198:8080/customResponse -d '{"size": 100}'
{"answer":"9O6aa~!NL1@U66AIh8XLtUWgVynzcXER3hyu6dKFz@LXvQ#n2WkYrn40i5ee2$4@eu$fSwvV1Y4Hkg0zgrtM07BXEMopdzcmUna7"}%

sniffer查看获取到的信息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
========================>
Found HTTP payload
Request->
POST /customResponse HTTP/1.1
Host: 192.168.19.198:8080
User-Agent: curl/7.77.0
Accept: */*
Content-Length: 13
Content-Type: application/x-www-form-urlencoded

{"size": 100}

Response->
HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8
Date: Thu, 29 Sep 2022 08:19:21 GMT
Content-Length: 113

{"answer":"9O6aa~!NL1@U66AIh8XLtUWgVynzcXER3hyu6dKFz@LXvQ#n2WkYrn40i5ee2$4@eu$fSwvV1Y4Hkg0zgrtM07BXEMopdzcmUna7"}

<========================
docker环境

docker镜像打包,Dockfile如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
FROM golang:1.16-bullseye as builder

RUN apt-get update

# According to https://packages.debian.org/source/sid/bpfcc,
# BCC build dependencies:
RUN apt-get install -y arping bison clang-format cmake dh-python \
dpkg-dev pkg-kde-tools ethtool flex inetutils-ping iperf \
libbpf-dev libclang-dev libclang-cpp-dev libedit-dev libelf-dev \
libfl-dev libzip-dev linux-libc-dev llvm-dev libluajit-5.1-dev \
luajit python3-netaddr python3-pyroute2 python3-distutils python3 git
ENV http_proxy http://192.168.19.16:17890
ENV https_proxy http://192.168.19.16:17890
# Install and compile BCC
RUN git clone https://github.com/iovisor/bcc.git
ENV http_proxy ''
ENV https_proxy ''
RUN mkdir bcc/build
WORKDIR bcc/build
RUN cmake ..
RUN make
RUN make install
1
docker build -t sniffer:v1

docker启动,setup_docker.sh如下

1
2
3
4
5
#! /bin/bash

ROOT_DIR=$(dirname $(dirname $(realpath "${0}")))
docker run --privileged --net=host -v ${ROOT_DIR}:/src -w /src/workshop1/capture-traffic \
-v /sys:/sys -v /lib:/lib -v /usr/src:/usr/src -it --rm sniffer:v1