犹如 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_sendmsgunix_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_sendmsgunix_dgram_sendmsg,抓取 Unix Socket 的报文。这两个 hook 的实现非常类似,都是先匹配 --sock 参数,然后抓取报文。

这里需要注意的是,使用 bpf_skc_to_unix_sock()struct socket *sock 转换成 struct unix_sock *usk,然后再获取 usk 里的 sock_path