前文采用 tracepoint 和 kprobe 等追踪手段画出了 tcp 状态转移的时序图,细心的读者可能注意到,文中的时序似乎有点问题:

ts:2220445792791:client:CLOSE:SYN_SENT
ts:2220446761789:client:SYN_SENT:ESTABLISHED
ts:2220447626787:server:LISTEN:SYN_RECV
ts:2220448026786:server:SYN_RECV:ESTABLISHED
ts:2220454118771:client:ESTABLISHED:FIN_WAIT1
ts:2220455075769:server:ESTABLISHED:CLOSE_WAIT
ts:2220455593768:server:CLOSE_WAIT:LAST_ACK
ts:2220456264766:client:FIN_WAIT1:FIN_WAIT2
ts:2220456525766:client:FIN_WAIT2:TIME_WAIT
ts:2220456623765:client:FIN_WAIT2:CLOSE
ts:2220456768765:server:LAST_ACK:CLOSE
ts:2282464966825:client:TIME_WAIT:CLOSE

为什么 clientESTABLISHEDserverSYN_RECV 前面?

事实上,我的测试内核版本是 6.1.11,它包含了这个commit [https://github.com/torvalds/linux/commit/10feb428a5045d5eb18a5d755fbb8f0cc9645626], 及其系列 [https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linu...]。
所以测试内核中 TCP_SYN_RECV 已经不是原始 TCP 协议中 server 收到第一个 syn 包的状态了,取而代之的是 TCP_NEW_SYN_RECVTCP_SYN_RECV 本身主要被用于支持 fastopen 特性了。

既然这样,我们怎么还原协议中的状态呢?经过一番检索,发现了这个方法:inet_reqsk_alloc,该方法是内核收到第一个 syn 包后,为连接分配 struct request_sock 这个轻量级数据结构表征的地方,于是我们可以进行追踪:

SEC("fexit/inet_reqsk_alloc")
int BPF_PROG(inet_reqsk_alloc,const struct request_sock_ops *ops,
                      struct sock *sk_listener,
                      bool attach_listener,struct request_sock *req ) {
    __u64 ts = bpf_ktime_get_boot_ns();
    const struct sock_common skc1 = BPF_CORE_READ(sk_listener,__sk_common);
    const struct sock_common skc = BPF_CORE_READ(req,__req_common);
    const int family = BPF_CORE_READ(&skc,skc_family);
    if(family != AF_INET){
        return 0;
    }
    const char oldstate = (BPF_CORE_READ(&skc1,skc_state));
    const char newstate = (BPF_CORE_READ(&skc,skc_state));

    __u32 dip = (BPF_CORE_READ(&skc1,skc_daddr));  
    __u16 dport = (BPF_CORE_READ(&skc1,skc_dport)); 
    __u32 sip = (BPF_CORE_READ(&skc1,skc_rcv_saddr));
    __u16 sport = bpf_htons(BPF_CORE_READ(&skc1,skc_num));
    return judge_side(ctx,ts,(long long)req,dip, dport, sip, sport,oldstate,newstate);
}

后面三步握手完成,为连接建立 struct sock 重量级数据结构表征的地方:tcp_v4_syn_recv_sock->tcp_create_openreq_child->inet_csk_clone_lock
通过这个方法,我们可以将 inet_reqsk_alloc 中的状态转移和前文sock 关联起来:

SEC("fexit/inet_csk_clone_lock")
int BPF_PROG(inet_csk_clone_lock,const struct sock *sk,
                 const struct request_sock *req,
                 const gfp_t priority,struct sock * newsk) {
    bpf_printk("csk_clone,lskaddr:%u,reqaddr:%u,skaddr:%u",
        sk,req,newsk);
    return 0;
}

这样就完整了。可以得到下图,是不是就符合我们预期了:

注:内核中不支持修改 timewait 时间,图中将其缩小才方便展示,其余状态如实展示。


MageekChiu
4.4k 声望1.7k 粉丝

T