Keep learning, keep living...

0%

Docker容器数据包转发路径分析

这些年总结过多次容器网络数据包路径相关的文章, 如:

但每次过段时间后,总会忘记细节。分析容器网络异常是否是由于某些基于netfilter的驱动影响时,总是要重新梳理。这次再从内核实现的角度来分析一次容器网络数据包的转发路径。

还是以外部访问bridge模式docker容器的场景进行分析。

入包

外部主机访问docker容器的数据包到达网卡后, 由内核函数:netif_receive_skb处理进入协议栈处理。对于IP数据包,会调用到函数ip_rcv

1
2
3
return NF_HOOK(NFPROTO_IPV4, NF_INET_PRE_ROUTING, NULL, skb,
dev, NULL,
ip_rcv_finish);

在这里会进行netfilterPRE_ROUTING阶段处理。入包在PRE_ROUTING阶段会由dockeriptables规则完成DNAT操作,数据包目的IP变更为docker容器的IP。

之后调用到函数ip_rcv_finiship_rcv_finish会进行路由查找,接着通过dst_input函数,调用路由中指定的函数。对于发送到本机的数据包,会调用ip_local_deliver交由传输层处理。对于访问docker容器这个场景,路由指向docker0, 调用ip_forward处理:

1
2
return NF_HOOK(NFPROTO_IPV4, NF_INET_FORWARD, NULL, skb,
skb->dev, rt->dst.dev, ip_forward_finish);

这里进行netfilterFORWARD阶段处理, 之后调用到函数:ip_forward_finiship_forward_finish最终会调用dst_output_sk, 它调用到ip_output:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
int ip_output(struct sock *sk, struct sk_buff *skb)
{
struct net_device *dev = skb_dst(skb)->dev;

IP_UPD_PO_STATS(dev_net(dev), IPSTATS_MIB_OUT, skb->len);

skb->dev = dev;
skb->protocol = htons(ETH_P_IP);

return NF_HOOK_COND(NFPROTO_IPV4, NF_INET_POST_ROUTING, sk, skb,
NULL, dev,
ip_finish_output,
!(IPCB(skb)->flags & IPSKB_REROUTED));
}

这里进行netfilterPOST_ROUTING阶段处理。最终由ip_finish_output调用到dev_queue_xmitdocker0设备发出。

dev_queue_xmit函数中发送会调用net_device设备的net_device_ops中的ndo_start_xmit指针。docker0bridge设备接口,它的ndo_start_xmit指向br_dev_xmit。于是函数调用到br_dev_xmit,数据包从路由转发进入到网桥上进行二层转发。

br_dev_xmit函数会根据目标MAC地址,决策由哪个端口转发,调用br_forward转发数据包。它会再调用__br_forward函数:

1
2
3
NF_HOOK(NFPROTO_BRIDGE, br_hook,
NULL, skb, indev, skb->dev,
br_forward_finish);

这里进行netfilter的二层过滤的NF_BR_LOCAL_OUT阶段的处理。它表示数据包来自主机的上层协议栈。

最后调用br_forward_finish, 在这里进行netfilter的二层过滤的NF_BR_POST_ROUTING阶段处理:

1
2
3
4
5
6
7
int br_forward_finish(struct sock *sk, struct sk_buff *skb)
{
return NF_HOOK(NFPROTO_BRIDGE, NF_BR_POST_ROUTING, sk, skb,
NULL, skb->dev,
br_dev_queue_push_xmit);

}

br_netfilter函数在NF_BR_POST_ROUTING注册了函数br_nf_post_routing:

1
2
3
4
5
NF_HOOK(pf, NF_INET_POST_ROUTING, state->sk, skb,
NULL, realoutdev,
br_nf_dev_queue_xmit);

return NF_STOLEN;

当参数net.bridge.bridge-nf-call-iptables开启时,会将二层数据包送到三层进行过滤。

br_dev_queue_push_xmit调用dev_queue_xmit从网桥上的端口发出。这时目标端口是veth pairveth pair设备的ndo_start_xmit指针指向veth_xmit函数, 它会调用dev_forward_skb将数据包由对端接收:

1
2
3
4
5
int dev_forward_skb(struct net_device *dev, struct sk_buff *skb)
{
return __dev_forward_skb(dev, skb) ?: netif_rx_internal(skb);
}
EXPORT_SYMBOL_GPL(dev_forward_skb);

这会调用netif_rx_internal函数,这时数据就进入到容器network namespace, 开始在容器内收包流程。容器network namespace中的路由表判断是发送给自己,于是上交到传输层处理,最终被应用层的socket接收。

整体的调用链如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
netif_receive_skb
--ip_rcv
----NF_INET_PRE_ROUTING
----ip_rcv_finish
------dst_input
--------ip_forward
----------NF_INET_FORWARD
----------ip_forward_finish
------------dst_output_sk
--------------ip_output
----------------NF_INET_POST_ROUTING
----------------ip_finish_output
------------------dev_queue_xmit
--------------------br_dev_xmit-----------------------进入桥接层
----------------------__br_forward
------------------------NF_BR_LOCAL_OUT
------------------------br_forward_finish
--------------------------NF_BR_POST_ROUTING
----------------------------br_nf_post_routing---------三层过滤
--------------------------br_dev_queue_push_xmit
----------------------------dev_queue_xmit
------------------------------veth_xmit
--------------------------------dev_forward_skb
----------------------------------net_if_rx_internal------------进入容器

回包:

回包从容器network namespace的网卡由dev_queue_xmit发送,容器内网卡为veth pair设备,由veth_xmit调用触发主机上的veth pair设备的收包逻辑, 调用到函数__netif_receive_skb_core
。此时,数据包进入到宿主机network namespace

由于veth pair设备挂接到bridge设备,它的net_device->rx_handler会被设置为br_handle_frame__netif_receive_skb_core于是调用到br_handle_frame函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
rx_handler = rcu_dereference(skb->dev->rx_handler);
if (rx_handler) {
if (pt_prev) {
ret = deliver_skb(skb, pt_prev, orig_dev);
pt_prev = NULL;
}
switch (rx_handler(&skb)) {
case RX_HANDLER_CONSUMED:
ret = NET_RX_SUCCESS;
goto out;
case RX_HANDLER_ANOTHER:
goto another_round;
case RX_HANDLER_EXACT:
deliver_exact = true;
case RX_HANDLER_PASS:
break;
default:
BUG();
}
}

br_handle_frame根据目的MAC地址决策目的端口转发。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
forward:
switch (p->state) {
case BR_STATE_FORWARDING:
rhook = rcu_dereference(br_should_route_hook);
if (rhook) {
if ((*rhook)(skb)) {
*pskb = skb;
return RX_HANDLER_PASS;
}
dest = eth_hdr(skb)->h_dest;
}
/* fall through */
case BR_STATE_LEARNING:
if (ether_addr_equal(p->br->dev->dev_addr, dest))
skb->pkt_type = PACKET_HOST;

NF_HOOK(NFPROTO_BRIDGE, NF_BR_PRE_ROUTING, NULL, skb,
skb->dev, NULL,
br_handle_frame_finish);
break;
default:
drop:
kfree_skb(skb);
}

这里进行netfilter的二层过滤的NF_BR_PRE_ROUTING阶段的处理。

br_netfilter内核模块在这个阶段注册了函数: br_nf_pre_routing, 当参数net.bridge.bridge-nf-call-iptables开启时,会将二层数据包送到三层过滤:

1
2
3
NF_HOOK(NFPROTO_IPV4, NF_INET_PRE_ROUTING, state->sk, skb,
skb->dev, NULL,
br_nf_pre_routing_finish);

br_handle_frame最终调用br_handle_frame_finish进行转发决策。此时回包MACdocker0, 是网桥自身的三层接口,这种情况下,并不会由其他端口转发,而是调用函数br_pass_frame_up上送协议栈处理:

1
2
3
return NF_HOOK(NFPROTO_BRIDGE, NF_BR_LOCAL_IN, NULL, skb,
indev, NULL,
br_netif_receive_skb);

这里进行NF_BR_LOCAL_IN阶段的处理。表示数据由二层进入三层协议栈。

br_netif_receive_skb调用netif_receive_skb进入宿主机的收包阶段:

1
2
3
4
5
static int br_netif_receive_skb(struct sock *sk, struct sk_buff *skb)
{
br_drop_fake_rtable(skb);
return netif_receive_skb(skb);
}

接着同入包流程类似,会调用到ip_rcv函数,进入宿主机上netfilterPRE_ROUTING阶段处理。接着根据路由结果由ip_forward进行转发,进行netfilterFORWARD阶段的处理。最终由ip_forward_finish调用ip_output, 在这里进行POST_ROUTING阶段的处理。容器回包在这里进行SNAT操作,将源IP修改为宿主机IP,然后由物理网卡发出。

整体调用链为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
dev_queue_xmit
--veth_xmit
----__netif_receive_skb_core
------br_handle_frame
--------NF_BR_PRE_ROUTING
--------br_handle_frame_finish
----------br_pass_frame_up
------------NF_BR_LOCAL_IN
------------br_netif_receive_skb
--------------netif_receive_skb
----------------ip_rcv
------------------NF_INET_PRE_ROUTING
------------------ip_rcv_finish
--------------------dst_input
----------------------ip_forward
------------------------NF_INET_FORWARD
------------------------ip_forward_finish
--------------------------dst_output_sk
----------------------------ip_output
------------------------------NF_INET_POST_ROUTING
------------------------------ip_finish_output
--------------------------------dev_queue_xmit

容器之间访问的数据包会由函数__br_forward处理,这时会进行netfilter的二层过滤NF_BR_FORWARD阶段处理。如果开启参数 net.bridge.bridge-nf-call-iptables, 会将数据包送到三层进行过滤。