对于 socket 编程,在用户态看到的只能是 fd;而在内核态,大部分时候看到的是 sock。

fd 和 sock 之间的关系,是在内核态的 syscall 里建立的;所以,在 connectaccept 等 syscall 里,可以看到 fd 找 sock 的过程。

TL;DR 通过 hook connectaccept syscall,就可以将 fd 和 sock 关联起来。

connect syscall

先来看看 connect syscall 里是如何找到 sock 的。

 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
// net/socket.c

int __sys_connect_file(struct file *file, struct sockaddr_storage *address,
                       int addrlen, int file_flags)
{
    struct socket *sock;
    int err;

    sock = sock_from_file(file);
    // ...
}

int __sys_connect(int fd, struct sockaddr __user *uservaddr, int addrlen)
{
    int ret = -EBADF;
    struct fd f;

    f = fdget(fd);
    if (fd_file(f)) {
        struct sockaddr_storage address;

        ret = move_addr_to_kernel(uservaddr, addrlen, &address);
        if (!ret)
            ret = __sys_connect_file(fd_file(f), &address, addrlen, 0);
        fdput(f);
    }

    return ret;
}

SYSCALL_DEFINE3(connect, int, fd, struct sockaddr __user *, uservaddr,
                int, addrlen)
{
    return __sys_connect(fd, uservaddr, addrlen);
}

处理逻辑:

  1. 通过 fdget 获取 fd 对应的 struct fd
  2. 通过 fd_file 获取 struct file
  3. 通过 sock_from_file() 获取 struct socket

sock_from_file() 的实现如下:

1
2
3
4
5
6
7
8
9
// net/socket.c

struct socket *sock_from_file(struct file *file)
{
    if (file->f_op == &socket_file_ops)
        return file->private_data;  /* set in sock_alloc_file */

    return NULL;
}

accept syscall

再来看看 accept syscall 里是如何找到 sock 的。

 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
// net/socket.c

struct file *do_accept(struct file *file, struct proto_accept_arg *arg,
                       struct sockaddr __user *upeer_sockaddr,
                       int __user *upeer_addrlen, int flags)
{
    struct socket *sock, *newsock;
    struct file *newfile;
    int err, len;
    struct sockaddr_storage address;
    const struct proto_ops *ops;

    sock = sock_from_file(file);
    // ...
    newsock = sock_alloc();
    // ...
    newfile = sock_alloc_file(newsock, flags, sock->sk->sk_prot_creator->name);
    // ...
    arg->flags |= sock->file->f_flags;
    err = ops->accept(sock, newsock, arg);
    // ...

    /* File flags are not inherited via accept() unlike another OSes. */
    return newfile;
out_fd:
    fput(newfile);
    return ERR_PTR(err);
}

static int __sys_accept4_file(struct file *file, struct sockaddr __user *upeer_sockaddr,
                              int __user *upeer_addrlen, int flags)
{
    // ...

    newfd = get_unused_fd_flags(flags);
    // ...
    newfile = do_accept(file, &arg, upeer_sockaddr, upeer_addrlen,
                        flags);
    // ...
    fd_install(newfd, newfile);
    return newfd;
}

int __sys_accept4(int fd, struct sockaddr __user *upeer_sockaddr,
                  int __user *upeer_addrlen, int flags)
{
    int ret = -EBADF;
    struct fd f;

    f = fdget(fd);
    if (fd_file(f)) {
        ret = __sys_accept4_file(fd_file(f), upeer_sockaddr,
                     upeer_addrlen, flags);
        fdput(f);
    }

    return ret;
}

SYSCALL_DEFINE4(accept4, int, fd, struct sockaddr __user *, upeer_sockaddr,
                int __user *, upeer_addrlen, int, flags)
{
    return __sys_accept4(fd, upeer_sockaddr, upeer_addrlen, flags);
}

SYSCALL_DEFINE3(accept, int, fd, struct sockaddr __user *, upeer_sockaddr,
                int __user *, upeer_addrlen)
{
    return __sys_accept4(fd, upeer_sockaddr, upeer_addrlen, 0);
}

处理逻辑:

  1. 通过 fdget 获取 fd 对应的 struct fd
  2. 通过 fd_file 获取 struct file
  3. 通过 sock_from_file() 获取 struct socket
  4. 通过 sock_alloc() 创建新的 struct socket
  5. 通过 sock_alloc_file() 创建新的 struct file
  6. 通过 ops->accept() 等待新连接的建立;
  7. 通过 fd_install() 将新的 struct file 关联到 fd
  8. 最后返回新的 fd

hook connect syscal 拿到 fd 以及五元组信息

connect syscall 结束后,fd 对应的 sock 里便有了五元组信息。

 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
SEC("fentry/__sys_connect")
int BPF_PROG(fentry___sys_connect, int fd)
{
    struct tcp_fd_info fd_info = {};
    __u64 stack_id;

    stack_id = get_stack_id();
    bpf_map_update_elem(&fd_info_map, &stack_id, &fd_info, BPF_ANY);

    return BPF_OK;
}

SEC("fentry/__sys_connect_file")
int BPF_PROG(fentry___sys_connect_file, struct file *file)
{
    struct tcp_fd_info *fd_info;

    fd_info = find_fd_info();
    if (!fd_info)
        return BPF_OK;

    fd_info->file = (__u64)(void *) file;

    return BPF_OK;
}

SEC("fexit/__sys_connect")
int BPF_PROG(fexit___sys_connect, int fd, struct sockaddr *uservaddr, int addrlen,
             int retval)
{
    struct tcp_fd_info *fd_info;
    __u64 stack_id;

    stack_id = get_stack_id();

    fd_info = bpf_map_lookup_and_delete(&fd_info_map, &stack_id);
    if (!fd_info)
        return BPF_OK;

    struct sock *sk = sock_from_file(fd_info->file);
    if (BPF_CORE_READ(sk, __sk_common.skc_family) == AF_INET) {
        bpf_printk("fd-socket: connect fd=%d sock=0x%016llx retval=%d\n", fd,
                    (__u64) sock_from_file(fd_info->file), retval);
        bpf_printk("fd-socket: connect lport=%d rport=%d laddr=%pI4 raddr=%pI4\n",
                    BPF_CORE_READ(sk, __sk_common.skc_num),
                    bpf_ntohs(BPF_CORE_READ(sk, __sk_common.skc_dport)),
                    &sk->__sk_common.skc_rcv_saddr, &sk->__sk_common.skc_daddr);
    }

    return BPF_OK;
}
  1. fentry __sys_connect 里,向 fd_info_map 里插入一条记录。
  2. fentry __sys_connect_file 里,将 file 更新到 fd_info_map 的记录里。
  3. fexit __sys_connect 里,从 fd_info_map 里查找记录,然后打印五元组信息。

通过跟 inet_sock_set_state tracepoint 作对比,可以看到 __sys_connect 里的 sock 信息和 inet_sock_set_state 里的 sk 信息是一致的。

1
2
3
4
$ sudo cat /sys/kernel/debug/tracing/trace_pipe
            curl-72060   [005] ...11 141233.595402: bpf_trace_printk: fd-socket: connect fd=5 sock=0xffff91e7c4c6ae40 retval=-115
            curl-72060   [005] ...11 141233.595433: bpf_trace_printk: fd-socket: connect lport=58894 rport=8080 laddr=192.168.241.133 raddr=192.168.241.136
          <idle>-0       [006] ..s41 141233.595904: bpf_trace_printk: fd-socket: established sock=0xffff91e7c4c6ae40 lport=58894 rport=8080 laddr=192.168.241.133 raddr=192.168.241.136

hook accept syscall 拿到新连接的 fd 以及五元组信息

accept syscall 结束后,返回了新的 fd;而且这个 fd 对应的 sock 里保存了新连接的五元组信息。

 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
SEC("fentry/__sys_accept4")
int BPF_PROG(fentry___sys_accept4, int fd)
{
    struct tcp_fd_info fd_info = {};
    __u64 stack_id;

    stack_id = get_stack_id();
    bpf_map_update_elem(&fd_info_map, &stack_id, &fd_info, BPF_ANY);

    return BPF_OK;
}

SEC("fexit/do_accept")
int BPF_PROG(fexit_do_accept, struct file *file, struct proto_accept_arg *arg,
             struct sockaddr *upeer_sockaddr, int *upeer_addrlen, int flags,
             struct file *newfile)
{
    struct tcp_fd_info *fd_info;

    fd_info = find_fd_info();
    if (!fd_info)
        return BPF_OK;

    fd_info->file = (__u64)(void *) file;
    if (!IS_ERR_VALUE(newfile))
        fd_info->newfile = (__u64)(void *) newfile;

    return BPF_OK;
}

SEC("fexit/__sys_accept4")
int BPF_PROG(fexit___sys_accept4, int fd, struct sockaddr *uservaddr, int addrlen,
             int newfd)
{
    struct tcp_fd_info *fd_info;
    __u64 stack_id;

    stack_id = get_stack_id();

    fd_info = bpf_map_lookup_and_delete(&fd_info_map, &stack_id);
    if (!fd_info)
        return BPF_OK;

    bpf_printk("fd-socket: accept fd=%d sock=0x%016llx retval=%d\n", fd,
               (__u64) sock_from_file(fd_info->file), newfd);
    if (newfd < 0)
        return BPF_OK;

    struct sock *sk = sock_from_file(fd_info->newfile);
    if (BPF_CORE_READ(sk, __sk_common.skc_family) == AF_INET) {
        bpf_printk("fd-socket: accept newfd=%d sock=0x%016llx\n", newfd, (__u64) sk);
        bpf_printk("fd-socket: accept lport=%d rport=%d laddr=%pI4 raddr=%pI4\n",
                   BPF_CORE_READ(sk, __sk_common.skc_num),
                   bpf_ntohs(BPF_CORE_READ(sk, __sk_common.skc_dport)),
                   &sk->__sk_common.skc_rcv_saddr, &sk->__sk_common.skc_daddr);
    }


    return BPF_OK;
}
  1. fentry __sys_accept4 里,向 fd_info_map 里插入一条记录。
  2. fexit do_accept 里,将 filenewfile 更新到 fd_info_map 的记录里。
  3. fexit __sys_accept4 里,从 fd_info_map 里查找记录,然后打印五元组信息。

通过跟 inet_sock_set_state tracepoint 作对比,可以看到 __sys_accept4 里的 sock 信息和 inet_sock_set_state 里的 sk 信息是一致的。

1
2
3
4
5
$ sudo cat /sys/kernel/debug/tracing/trace_pipe
          <idle>-0       [006] ..s41 141500.612920: bpf_trace_printk: fd-socket: established sock=0xffff91e7cacc6f00 lport=8080 rport=58745 laddr=192.168.241.133 raddr=192.168.241.1
           <...>-72081   [001] ...11 141500.613126: bpf_trace_printk: fd-socket: accept fd=3 sock=0xffff91e7c83b8940 retval=524288
           <...>-72081   [001] ...11 141500.613161: bpf_trace_printk: fd-socket: accept newfd=524288 sock=0xffff91e7cacc6f00
           <...>-72081   [001] ...11 141500.613163: bpf_trace_printk: fd-socket: accept lport=8080 rport=58745 laddr=192.168.241.133 raddr=192.168.241.1

总结

通过 hook connectaccept syscall,可以将 fd 和 sock 关联起来;然后通过 fd 对应的 sock,就可以拿到五元组信息。

P.S. 本文的代码在 learn-by-example fd-socket