在使用 curltelnet 等工具排查问题的时候,如果想要知道当前的连接信息,在现有的工具集里,是没有办法直接获取的。

于是,我写了一个小工具 tcpw,可以在 socket 层面获取到 curl 等工具的五元组信息。

tcpw 的使用

1
2
3
4
5
6
$ ./tcpw -h
Usage: tcpw [options] <command args...>
Options:
  --udp, -U       Trace UDP sockets
  --unix, -X      Trace Unix domain sockets
  --help, -h      Print this help message

tcpw 默认只 trace TCP socket,如果需要 trace UDP socket,可以使用 --udp/-U 选项;如果需要 trace Unix domain socket,可以使用 --unix/-X 选项。

trace TCP socket 的例子:

1
2
3
4
5
6
7
8
$ ./tcpw curl https://google.com
2024/12/21 14:42:15 tcpw: pid=97849 comm=curl af=AF_INET proto=TCP 192.168.241.133:46182 -> 142.251.10.101:443
<HTML><HEAD><meta http-equiv="content-type" content="text/html;charset=utf-8">
<TITLE>301 Moved</TITLE></HEAD><BODY>
<H1>301 Moved</H1>
The document has moved
<A HREF="https://www.google.com/">here</A>.
</BODY></HTML>

trace UDP socket 的例子:

1
2
3
4
5
6
7
$ ./tcpw -U nslookup google.com
2024/12/21 14:44:36 tcpw: pid=98464 comm=isc-net-0000 af=AF_INET proto=UDP 127.0.0.1:37324 -- 127.0.0.53:53
Server:     127.0.0.53
Address:    127.0.0.53#53

Non-authoritative answer:
...

trace Unix domain socket 的例子:

1
2
3
4
5
$ ./tcpw -X ../sockdump/sockdump-example
2024/12/21 14:45:24 serving
2024/12/21 14:45:24 tcpw: pid=98505 comm=sockdump-exampl af=AF_UNIX proto=UNIX-STREAM path=/tmp/uskdump.sock
2024/12/21 14:45:24 got response
...

同时,tcpw 也支持 IPv6 和 TCP socket 的方向。

trace connectaccept 函数

tcpw 的实现并不复杂,主要使用 fexit 来 trace 各个协议的 connectaccept 函数。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
SEC("fexit/connect")
int BPF_PROG(fexit_connect, struct socket *sock, struct sockaddr *uaddr,
             int addr_len, int flags, int retval)
{
    return trace_sock(sock, true);
}

SEC("fexit/accept")
int BPF_PROG(fexit_accept, struct socket *sock, struct socket *newsock,
             struct proto_accept_arg *arg, int retval)
{
    return trace_sock(newsock, false);
}

如上代码片段,并未在 bpf 代码里写死具体的 connectaccept 函数,而是留给 Go 代码来决定。

在 Go 代码里,通过遍历内核 BTF 信息,找到 connectaccept 函数的签名:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
func isConnectFunc(fn *btf.Func) bool {
    fnProto := fn.Type.(*btf.FuncProto)
    if len(fnProto.Params) != 4 {
        return false
    }

    return mybtf.IsStructPointer(fnProto.Params[0].Type, "socket") &&
        mybtf.IsStructPointer(fnProto.Params[1].Type, "sockaddr") &&
        isInt(fnProto.Params[2].Type) &&
        isInt(fnProto.Params[3].Type)
}

func isAcceptFunc(fn *btf.Func) bool {
    fnProto := fn.Type.(*btf.FuncProto)
    if len(fnProto.Params) != 4 {
        return false
    }

    return mybtf.IsStructPointer(fnProto.Params[0].Type, "socket") &&
        mybtf.IsStructPointer(fnProto.Params[1].Type, "socket") &&
        isInt(fnProto.Params[2].Type) &&
        isBool(fnProto.Params[3].Type)
}

通过确认函数参数的类型来判断是否是 connectaccept 函数。

之后,便可以将 bpf 程序 attach 到这些函数上。

trace fork 系统调用

在 trace connectaccept 函数的时候,并不是为了 trace 所有的 socket,而是为了 trace curltelnet 等工具的 socket

而在 tcpw 里,会通过 exec.Command 来执行 curltelnet 等工具,因此,需要 trace fork 系统调用,为了记录子进程的 pid。

通过翻阅 fork 系统调用的内核源代码,发现 tracepoint sched_process_fork 可以满足需求:

1
2
3
4
5
6
$ sudo bpftrace -lv 'tracepoint:*sched_process_fork'
tracepoint:sched:sched_process_fork
    char parent_comm[16]
    pid_t parent_pid
    char child_comm[16]
    pid_t child_pid

该 tracepoint 的 bpf 代码如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
SEC("tp/sched/sched_process_fork")
int tp_sched_process_fork(struct trace_event_raw_sched_process_fork *ctx)
{
    __u32 parent_pid = ctx->parent_pid;
    __u32 child_pid = ctx->child_pid;
    __u32 pid;

    pid = bpf_get_current_pid_tgid() >> 32;
    if (bpf_map_lookup_elem(&tcpw_pids, &pid)) {
        bpf_map_update_elem(&tcpw_pids, &parent_pid, &pid, BPF_ANY);
        bpf_map_update_elem(&tcpw_pids, &child_pid, &parent_pid, BPF_ANY);
    }

    return BPF_OK;
}

其中 parent_pid 并不一定是当前的 pid,因此需要通过 bpf_get_current_pid_tgid() 来获取当前的 pid。

而且,在 attach 该 tracepoint 之前,就事先添加一条记录当前进程 pid 的记录:

1
2
3
    pid := os.Getpid()
    err = pids.Put(uint32(pid), uint32(pid))
    assert.NoErr(err, "Failed to put pid: %v")

因此,tcpw 进程树下的所有进程都会被 trace;譬如:./tcpw bash -c "for i in {1..10}; do curl -s https://google.com -o /dev/null; echo done \$i; done"

总结

tcpw 是一个小工具,可以帮助我们在排查问题的时候,更方便地获取到 curltelnet 等工具的五元组信息。

同时,tcpw 也是一个学习 eBPF 的好例子,可以学习到如何 trace connectaccept 函数,如何 trace fork 系统调用。

tcpw 的源码在 tcpw