在使用 curl
、telnet
等工具排查问题的时候,如果想要知道当前的连接信息,在现有的工具集里,是没有办法直接获取的。
于是,我写了一个小工具 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 connect
和 accept
函数
tcpw
的实现并不复杂,主要使用 fexit
来 trace 各个协议的 connect
和 accept
函数。
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 代码里写死具体的 connect
和 accept
函数,而是留给 Go 代码来决定。
在 Go 代码里,通过遍历内核 BTF 信息,找到 connect
和 accept
函数的签名:
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)
}
|
通过确认函数参数的类型来判断是否是 connect
和 accept
函数。
之后,便可以将 bpf 程序 attach 到这些函数上。
trace fork
系统调用
在 trace connect
和 accept
函数的时候,并不是为了 trace 所有的 socket,而是为了 trace curl
、telnet
等工具的 socket。
而在 tcpw
里,会通过 exec.Command
来执行 curl
、telnet
等工具,因此,需要 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
是一个小工具,可以帮助我们在排查问题的时候,更方便地获取到 curl
、telnet
等工具的五元组信息。
同时,tcpw
也是一个学习 eBPF 的好例子,可以学习到如何 trace connect
和 accept
函数,如何 trace fork
系统调用。
tcpw
的源码在 tcpw。