[TOC]

1. 前言

本文分析基于 5.4.0 内核,Ubuntu 20.04。

如果我们想了解 ip_rcv 被调用的堆栈路径,可以使用 BCC 中的 trace.py 工具获取;如果想了解 ip_rcv 函数内部调用的堆栈情况可以使用 funcgraph 工具查看。

BCC 安装参考 INSTALL.md

相关命令使用样例。

 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
# 获取调用 ip_rcv 的完整堆栈
$ ./trace -UK ip_rcv -M 100
0       0       swapper/8       ip_rcv
        ip_rcv+0x1 [kernel]
        __netif_receive_skb_core+0x729 [kernel]
        __netif_receive_skb+0x18 [kernel]
        process_backlog+0xae [kernel]
        net_rx_action+0x26f [kernel] # 进入网络数据接受软中断处理函数
        __do_softirq+0xf5 [kernel]
        call_softirq+0x1c [kernel]
        do_softirq+0x65 [kernel]    # 调用软中断
				...

# 获取网络接收软中断函数调用函数堆栈
$ ./funcgraph -d 1 -m 10 -P  net_rx_action
 10)    <idle>-0    |               |  net_rx_action() {
 10)    <idle>-0    |               |    process_backlog() {
 10)    <idle>-0    |   0.054 us    |      _raw_spin_lock();
 10)    <idle>-0    |               |      __netif_receive_skb() {
 10)    <idle>-0    |               |        __netif_receive_skb_core() {
 10)    <idle>-0    |               |          packet_rcv() {
 10)    <idle>-0    |   0.044 us    |            skb_push();
 10)    <idle>-0    |   0.067 us    |            consume_skb();
 10)    <idle>-0    |   1.427 us    |          }
 10)    <idle>-0    |               |          ip_rcv() {
 ...

2. Linux L2 层网络接收

2.1 软中断相关数据结构

删除了 RPS 和无关的成员变量。

struct softnet_data 为软中断处理结构,该结构为 per-CPU 变量,每个 CPU 具有一个单独的软中断数据结构。

 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
/*
 * Incoming packets are placed on per-CPU queues
 */
struct softnet_data {
  // 为 NAPI 设备添加的对应的 napi_struct 结构,非 NAPI 设备复用 backlog
	struct list_head	poll_list;
  
  // 非 NAPI 设备在软中断中处理 input_pkt_queue 中数据包时,先将 skb 链表转移到此列表;
  // 以尽快释放 input_pkt_queue 上的锁
	struct sk_buff_head	process_queue;

  // 非 NAPI 设备 ISR 收包时将 skb 放入该队列,等待软中断处理。
	struct sk_buff_head	input_pkt_queue; 
  
  // 所有非 NAPI 设备共用的 napi_struct 结构,对应的 poll 函数为 process_backlog
	struct napi_struct	backlog; 
  
  // 流量控制输出队列
  struct Qdisc		*output_queue;
	struct Qdisc		**output_queue_tailp;
  
  // 发包传输完成队列
	struct sk_buff		*completion_queue;

};

2.2 Linux L2 层网络接收流程图

linux_l2_network_rcv_pkt

L2 与 L3 层的函数入口通过 packet_type 结构定义,对于 IPv4 协议对应的处理函数为 ip_rcv

1
2
3
4
5
static struct packet_type ip_packet_type __read_mostly = {
	.type = cpu_to_be16(ETH_P_IP),
	.func = ip_rcv,
	.list_func = ip_list_rcv,
};

3. Linux L3 层网络接收流程图

3.1 L3 层函数调用流程图

linux_l3_pkg_rcv_flow

3.2 netfilter 相关结构

netfilter 协议和 hook 点对应的 hook 函数通过一个二维数据进行管理,在每个 Network Namespace 中都存在一个类似的二维数组。二维数组的纵轴为协议,横轴为 hook 点,两者交叉对应的数据为对应 hook 函数列表,按照优先级进行排列, hook 函数为各个 table 中设置的函数,初始化到对应的 hook 函数列表中。当初始化新的 network namespace 的时调用 iptable_nat_init 函数将对应的 table 初始化 (例如 iptable_nat_table_init),在 table 初始化函数中循环将 nf_xxx_ipv4_ops 结构中定义的 hook 函数进行注册。

netfilter_data_struct

hook 的函数定义为:

1
2
3
typedef unsigned int nf_hookfn(void *priv,
			       struct sk_buff *skb,
			       const struct nf_hook_state *state);

参考这里, 中文。表格代表了 iptables 的管理方式,按照 table 和 chain 的方式组织,纵轴代表的是 table 名,横轴是 chain 的名字,与 hook 点一一对应。纵轴的方向代表了在某个 chain 上调用的顺序,优先级自上而下。

Tables↓ /Chains→ PREROUTING INPUT FORWARD OUTPUT POSTROUTING
(routing decision)
raw
(connection tracking enabled)
mangle
nat (DNAT)
(routing decision)
filter
security
nat (SNAT)

3.3 netfilter hook 函数调用

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
int ip_rcv(struct sk_buff *skb, struct net_device *dev, struct packet_type *pt,
	   struct net_device *orig_dev)
{
	struct net *net = dev_net(dev);

	skb = ip_rcv_core(skb, net); // 对于 ip 数据进行校验
	if (skb == NULL)
		return NET_RX_DROP;

	return NF_HOOK(NFPROTO_IPV4, NF_INET_PRE_ROUTING,
		       net, NULL, skb, dev, NULL,
		       ip_rcv_finish);
}

NF_HOOK 宏在启用 netfilter 的条件编译下,会首先调用 nf_hook 函数,在该函数中会根据参入的协议和 hook 点,获取到对应的 hook 函数列表头(例如 IPv4 协议中的 net->nf.hooks_ipv4[hook] ),然后在 nf_hook_slow 中循环调用列表中的 hook 函数(hook 函数按照优先级组织),并基于 hook 函数返回的结果决定继续调用列表中后续的 hook 函数,还是直接返回。

netfilter 中 hook 函数的格式基本如下,直接调用 ipt_do_table 函数,最后的参数传入对应的 table 字段。

1
2
3
4
5
6
static unsigned int iptable_nat_do_chain(void *priv,
					 struct sk_buff *skb,
					 const struct nf_hook_state *state)
{
	return ipt_do_table(skb, state, state->net->ipv4.nat_table);
}

所以,如果我们想要获取到 netfilter 的结果,则需要跟踪 ipt_do_table 函数的入参和返回结果即可。

 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
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
static inline int
NF_HOOK(uint8_t pf, unsigned int hook, struct net *net, struct sock *sk, struct sk_buff *skb,
	struct net_device *in, struct net_device *out,
	int (*okfn)(struct net *, struct sock *, struct sk_buff *))
{
	int ret = nf_hook(pf, hook, net, sk, skb, in, out, okfn);
	if (ret == 1)
		ret = okfn(net, sk, skb);
	return ret;
}

/**
 *	nf_hook - call a netfilter hook
 *
 *	Returns 1 if the hook has allowed the packet to pass.  The function
 *	okfn must be invoked by the caller in this case.  Any other return
 *	value indicates the packet has been consumed by the hook.
 */
static inline int nf_hook(u_int8_t pf, unsigned int hook, struct net *net,
			  struct sock *sk, struct sk_buff *skb,
			  struct net_device *indev, struct net_device *outdev,
			  int (*okfn)(struct net *, struct sock *, struct sk_buff *))
{
	struct nf_hook_entries *hook_head = NULL;
	int ret = 1;

  // ...
  
  rcu_read_lock();
	switch (pf) {
	case NFPROTO_IPV4:
		hook_head = rcu_dereference(net->nf.hooks_ipv4[hook]);
		break;
		// ...
	default:
		WARN_ON_ONCE(1);
		break;
	}

	if (hook_head) {
		struct nf_hook_state state;

		nf_hook_state_init(&state, hook, pf, indev, outdev,
				   sk, net, okfn);

		ret = nf_hook_slow(skb, &state, hook_head, 0);
	}
	rcu_read_unlock();

	return ret;
}


/* Returns 1 if okfn() needs to be executed by the caller,
 * -EPERM for NF_DROP, 0 otherwise.  Caller must hold rcu_read_lock. */
int nf_hook_slow(struct sk_buff *skb, struct nf_hook_state *state,
		 const struct nf_hook_entries *e, unsigned int s)
{
	unsigned int verdict;
	int ret;

	for (; s < e->num_hook_entries; s++) {
		verdict = nf_hook_entry_hookfn(&e->hooks[s], skb, state);
		switch (verdict & NF_VERDICT_MASK) {
		case NF_ACCEPT:
			break;
		case NF_DROP:
			kfree_skb(skb);
			ret = NF_DROP_GETERR(verdict);
			if (ret == 0)
				ret = -EPERM;
			return ret;
		case NF_QUEUE:
			ret = nf_queue(skb, state, s, verdict);
			if (ret == 1)
				continue;
			return ret;
		default:
			/* Implicit handling for NF_STOLEN, as well as any other
			 * non conventional verdicts.
			 */
			return 0;
		}
	}

	return 1;
}
EXPORT_SYMBOL(nf_hook_slow);

参考

CO-RE Clang/LLVM 10+ Ubuntu 20.10 (LLVM 11)

CO-RE 环境依赖 (内核 + Clang/LLVM 10+ )

参考

  • HOWTO: BCC to libbpf conversion 如何将 bcc 的代码转成 CO-RE
  • 官方参考样例 libbpf-tools in BCC repo contain lots of real-world tools converted from BCC to BPF CO-RE. Consider converting some more to both contribute to the BPF community and gain some more experience with it.

代码编写

  • Makefile 文件

  • libbpf-bootstrap

监控网络连接 CO-RE 程序编写