问题
通过 kprobe 跟踪 __netif_receive_skb 并尝试从参数 struct sk_buff *skb 获取 socket 信息,为什么不可行 ?
测试
我们通过一个简单的程序来验证一下:
#!/usr/bin/env bpftrace
kprobe:__netif_receive_skb {
$skb = (struct sk_buff *)arg0;
$saddr = $skb->sk->__sk_common.skc_rcv_saddr;
$daddr = $skb->sk->__sk_common.skc_daddr;
$sport = $skb->sk->__sk_common.skc_num;
$dport = $skb->sk->__sk_common.skc_dport;
$dport = ($dport >> 8) | (($dport << 8) & 0x00FF00);
printf("%s:%d -> %s:%d\n", ntop($saddr), $sport, ntop($daddr), $dport);
}
结果如下:
$ sudo bpftrace ./test0.bt
Attaching 1 probe...
0.0.0.0:0
0.0.0.0:0
0.0.0.0:0
0.0.0.0:0
0.0.0.0:0
0.0.0.0:0
0.0.0.0:0
分析
我们先来看看 __netif_receive_skb 函数的调用栈:
netif_receive_skb
netif_receive_skb_internal
__netif_receive_skb
__netif_receive_skb_one_core
__netif_receive_skb_core
do_xdp_generic
sch_handle_ingress
ip_rcv
ip_rcv_finish
ip_rcv_finish_core
tcp_v4_early_demux
udp_v4_early_demux
走读一下代码我们就会发现,__netif_receive_skb 其实是在网络栈收包路径的最开始阶段:
/**
* netif_receive_skb - process receive buffer from network
* @skb: buffer to process
*
* netif_receive_skb() is the main receive data processing function.
* It always succeeds. The buffer may be dropped during processing
* for congestion control or by the protocol layers.
*
* This function may only be called from softirq context and interrupts
* should be enabled.
*
* Return values (usually ignored):
* NET_RX_SUCCESS: no congestion
* NET_RX_DROP: packet was dropped
*/
调用到 __netif_receive_skb_core 时,才是 generic XDP 和 TC 入口,而这早于协议栈的处理,包括 iptables PREROUTING 和 nftables ingress hooks。
一路往下看,调用到 ip_rcv 才是网络层的收包逻辑,根据系统配置及 IP 层的载荷,我们最终会走到 tcp_v4_early_demux 和 udp_v4_early_demux,以 tcp_v4_early_demux 为例:
int tcp_v4_early_demux(struct sk_buff *skb)
{
const struct iphdr *iph;
const struct tcphdr *th;
struct sock *sk;
if (skb->pkt_type != PACKET_HOST)
return 0;
if (!pskb_may_pull(skb, skb_transport_offset(skb) + sizeof(struct tcphdr)))
return 0;
iph = ip_hdr(skb);
th = tcp_hdr(skb);
if (th->doff < sizeof(struct tcphdr) / 4)
return 0;
sk = __inet_lookup_established(dev_net(skb->dev), &tcp_hashinfo,
iph->saddr, th->source,
iph->daddr, ntohs(th->dest),
skb->skb_iif, inet_sdif(skb));
if (sk) {
skb->sk = sk;
根据网络层和传输层的头部信息,从已建立的连接的哈希表中查找出对应的 struct sock * 对象,最终赋值给 skb->sk 字段。
所以使用 kprobe 跟踪 __netif_receive_skb 时,skb->sk 字段还未赋值,所以拿不到对应的 socket 信息。
验证
根据以上分析,我们知道在 __netif_receive_skb 调用返回时,如果是已建立连接的 TCP/UDP 数据包,我们应该能通过 skb->sk 打印出对应 socket 的信息:
#!/usr/bin/env bpftrace
kprobe:__netif_receive_skb {
@[tid] = arg0;
}
kretprobe:__netif_receive_skb {
$skb = (struct sk_buff *)@[tid];
delete(@[tid]);
$saddr = $skb->sk->__sk_common.skc_rcv_saddr;
$daddr = $skb->sk->__sk_common.skc_daddr;
$sport = $skb->sk->__sk_common.skc_num;
$dport = $skb->sk->__sk_common.skc_dport;
$dport = ($dport >> 8) | (($dport << 8) & 0x00FF00);
printf("%s:%d -> %s:%d\n", ntop($saddr), $sport, ntop($daddr), $dport);
}
结果如下:
$ sudo bpftrace ./test1.bt
Attaching 2 probes...
0.0.0.0:0
127.0.0.1:34033
0.0.0.0:0
127.0.0.1:34033
0.0.0.0:0
0.0.0.0:0
127.0.0.1:34033
0.0.0.0:0
0.0.0.0:0
127.0.0.1:34033
0.0.0.0:0
0.0.0.0:0
127.0.0.1:34033
我们可以看到,在 kretprobe:__netif_receive_skb 中,我们已经可以打印出 socket 信息。当然还有一些漏网之鱼,暂未深究 :)