犹如 2 年前抄狄老师的 skbtracer
一样,当看到 sockdump
时,又抄了一回作业。原 sockdump
也是基于 bcc 的项目,我把它改造成了基于 Go+eBPF CO-RE 的项目,下载二进制文件即可立马用起来。
我并不了解 Unix Socket 的实现,所以就纯粹的抄作业了。
sockdump
用法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
# ./sockdump -h
Usage of ./sockdump:
--format string output format (string, hex, hexstring, pcap) (default "hex")
--output string output file, default stdout
--pid uint pid of the process to trace
--seg-size uint max segment size, increase this number if packet size is longer than captured size (default 51200)
--segs-in-buffer uint max number of segs in perf event buffer, increate this number if message is dropped (default 100)
--segs-per-msg uint max number of iovec segments (default 10)
--sock string unix socket path.
Matches all sockets starting with the given path.
Note that the path must be the same string used in the application, instead of the actual file path.
If the application used a relative path, the same relative path should be used here.
If the application runs inside a container, the path inside the container should be used here.
pflag: help requested
|
其中,--sock
参数是可选的;如果不指定,会抓取所有的 Unix Socket 的报文;如果指定,则使用前缀匹配的方式抓取匹配到的 Unix Socket 的报文。
sockdump
效果
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
|
# echo "go run ./cmd/sockdump-example/main.go" in another terminal
# ./sockdump --format string
2023/12/08 12:21:37 Attached fentry to unix_stream_sendmsg
2023/12/08 12:21:37 Attached fentry to unix_dgram_sendmsg
2023/12/08 12:21:37 Read data from perf event...
2023-12-08 12:21:39 >>> process sockdump-exampl [48125 -> 48125] path /tmp/sockdump.sock len 86(86)
GET // HTTP/1.1
Host: unix
User-Agent: Go-http-client/1.1
Accept-Encoding: gzip
2023-12-08 12:21:39 >>> process sockdump-exampl [48125 -> 48125] path /tmp/sockdump.sock len 944(944)
HTTP/1.1 200 OK
Content-Type: text/html; charset=utf-8
Last-Modified: Fri, 08 Dec 2023 12:21:39 GMT
Date: Fri, 08 Dec 2023 12:21:39 GMT
Content-Length: 781
<pre>
<a href=".ICE-unix/">.ICE-unix/</a>
<a href=".X11-unix/">.X11-unix/</a>
<a href=".XIM-unix/">.XIM-unix/</a>
<a href=".font-unix/">.font-unix/</a>
<a href="snap-private-tmp/">snap-private-tmp/</a>
<a href="systemd-private-9dda0a49b49843b994907c3790385fd7-systemd-logind.service-atspfy/">systemd-private-9dda0a49b49843b994907c3790385fd7-systemd-logind.service-atspfy/</a>
<a href="systemd-private-9dda0a49b49843b994907c3790385fd7-systemd-resolved.service-0mw7GZ/">systemd-private-9dda0a49b49843b994907c3790385fd7-systemd-resolved.service-0mw7GZ/</a>
<a href="systemd-private-9dda0a49b49843b994907c3790385fd7-systemd-timesyncd.service-n6Px1k/">systemd-private-9dda0a49b49843b994907c3790385fd7-systemd-timesyncd.service-n6Px1k/</a>
<a href="sockdump.sock">sockdump.sock</a>
</pre>
^C
2023/12/08 12:21:40 Captured 2 packets
|
除了命令行输出,还可以使用 --format pcap
输出 pcap 文件,然后使用 WireShark 打开。
sockdump
实现
sockdump
通过 hook unix_stream_sendmsg
和 unix_dgram_sendmsg
,抓取 Unix Socket 的报文。这两个 hook 的实现非常类似,都是先匹配 --sock
参数,然后抓取报文。
这 2 个函数的函数原型如下:
1
2
3
4
5
|
// ${KERNEL}/net/unix/af_unix.c
static int unix_stream_sendmsg(struct socket *sock, struct msghdr *msg, size_t len);
static int unix_dgram_sendmsg(struct socket *sock, struct msghdr *msg, size_t len);
|
可以看到,它们的参数是一致的,所以可以抽象出一个函数,同时 hook 这两个函数。
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
|
static __noinline int
__usk_sendmsg(void *ctx, struct socket *sock, struct msghdr *msg, size_t len)
{
// filter --pid
// filter --sock
// dump packet from iovec
return 0;
}
static __always_inline int
__kprobe_unix_sendmsg(struct pt_regs *ctx)
{
struct socket *sock = (void *) PT_REGS_PARM1(ctx);
struct msghdr *msg = (void *) PT_REGS_PARM2(ctx);
size_t len = PT_REGS_PARM3(ctx);
return __usk_sendmsg(ctx, sock, msg, len);
}
SEC("kprobe/unix_stream_sendmsg")
int kprobe__unix_stream_sendmsg(struct pt_regs *ctx)
{
return __kprobe_unix_sendmsg(ctx);
}
SEC("kprobe/unix_dgram_sendmsg")
int kprobe__unix_dgram_sendmsg(struct pt_regs *ctx)
{
return __kprobe_unix_sendmsg(ctx);
}
SEC("fentry/unix_stream_sendmsg")
int BPF_PROG(fentry__unix_stream_sendmsg, struct socket *sock, struct msghdr *msg, size_t len)
{
return __usk_sendmsg((void *) (long) ctx, sock, msg, len);
}
SEC("fentry/unix_dgram_sendmsg")
int BPF_PROG(fentry__unix_dgram_sendmsg, struct socket *sock, struct msghdr *msg, size_t len)
{
return __usk_sendmsg((void *) (long) ctx, sock, msg, len);
}
|
如果当前内核支持 fentry
,则使用 fentry
hook;否则使用 kprobe
hook。
filter –pid
sockdump
通过 --pid
参数,过滤指定的进程。这里使用 bpf_get_current_pid_tgid()
获取当前进程的 pid,然后与 --pid
参数进行比较。
1
2
3
|
pid = bpf_get_current_pid_tgid() >> 32;
if (cfg->pid && cfg->pid != pid)
return 0;
|
filter –sock
sockdump
通过 --sock
参数,过滤指定的 Unix Socket;既要匹配本端的 sock_path
,也要匹配对端的 sock_path
。
不过,这里需要注意:使用 bpf_skc_to_unix_sock()
将 struct socket *sock
转换成 struct unix_sock *usk
,然后再获取 usk
里的 sock_path
。
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
|
#define SOCK_PATH_OFFSET \
(offsetof(struct unix_address, name) + offsetof(struct sockaddr_un, sun_path))
static __always_inline bool
__is_str_prefix(const char *str, const char *prefix, int siz)
{
for (int i = 0; i < siz && prefix[i]; i++)
if (str[i] != prefix[i])
return false;
return true;
}
static __noinline bool
__is_path_matched(__u64 *path)
{
__u64 *sock_path = (__u64 *) cfg->sock_path;
int i;
// 1. 使用 __u64 减少遍历次数
// 2. 再使用 __u8 判断是否匹配前缀
for (i = 0; i < UNIX_PATH_MAX / 8 && sock_path[i]; i++)
if (path[i] != sock_path[i])
return __is_str_prefix((const char *) &path[i],
(const char *) &sock_path[i], 8);
if (i == UNIX_PATH_MAX / 8)
return __is_str_prefix((const char *) &path[i],
(const char *) &sock_path[i], 4);
return true;
}
static __always_inline bool
match_path_of_usk(struct unix_sock *usk, __u64 *path)
{
struct unix_address *addr;
__u8 one_byte = 0;
char *sock_path;
// addr->len == 0 则跳过本次抓包
addr = BPF_CORE_READ(usk, addr);
if (!BPF_CORE_READ(addr, len))
return false;
// 1. 通过偏移读取 sock_path
// 2. 如果第一个字节是 0(@/path/to/unix.sock),则多偏移一个字节再读取 sock_path
sock_path = (char *) addr + SOCK_PATH_OFFSET;
bpf_probe_read_kernel(&one_byte, 1, sock_path);
if (one_byte == 0)
bpf_probe_read_kernel_str(path, UNIX_PATH_MAX, sock_path + 1);
else
bpf_probe_read_kernel_str(path, UNIX_PATH_MAX, sock_path);
return __is_path_matched(path);
}
static __always_inline bool
__is_sock_path_matched(struct unix_sock *usk, __u64 *path)
{
return usk && match_path_of_usk(usk, path);
}
static __noinline int
__usk_sendmsg(void *ctx, struct socket *sock, struct msghdr *msg, size_t len)
{
// ...
usk = bpf_skc_to_unix_sock(sock->sk);
peer = usk ? bpf_skc_to_unix_sock(usk->peer) : NULL;
if (!__is_sock_path_matched(usk, path) &&
!__is_sock_path_matched(peer, path))
return 0;
// ...
return 0;
}
|
dump packet from iovec
从 iovec
中读取报文:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
static __always_inline void
collect_data(void *ctx, struct packet *pkt, char *buf, __u32 len)
{
__u32 seg_size = cfg->seg_size, n;
pkt->flags = 0;
pkt->len = len;
// 此处需要 if 判断 n 的上限,否则 verifier 会报错
n = len > seg_size ? seg_size : len;
if (n < SS_MAX_SEG_SIZE)
bpf_probe_read(&pkt->data, n, buf);
else
bpf_probe_read(&pkt->data, SS_MAX_SEG_SIZE, buf);
pkt->data[n < SS_MAX_SEG_SIZE ? n : SS_MAX_SEG_SIZE - 1] = '\0';
bpf_perf_event_output(ctx, &events, BPF_F_CURRENT_CPU, pkt, sizeof(*pkt));
}
|
此处跳过 Go 代码讲解。。。
sockdump
小结
sockdump
通过 hook unix_stream_sendmsg
和 unix_dgram_sendmsg
,抓取 Unix Socket 的报文。这两个 hook 的实现非常类似,都是先匹配 --sock
参数,然后抓取报文。
这里需要注意的是,使用 bpf_skc_to_unix_sock()
将 struct socket *sock
转换成 struct unix_sock *usk
,然后再获取 usk
里的 sock_path
。