谈谈OpenStack中端口在内核中的收发流程

在进入正题之前,先来聊一个问题。看过openstack文档的都清楚,社区为了解决安全组的问题,而硬生生的在br-int之前插入了一个linux bridge。原话是这么说的:

可是,为什么说iptables和ovs 端口不兼容呢?估计很多人没有仔细想过,下面就来简单聊聊这个问题。

首先,看一个普通的物理网卡收包流程。网卡驱动这部分略过不提,从内核函数__netif_receive_skb_core开始,每一个报文最终都会走到这里来。在这里,根据在网卡驱动中就已经获取到的协议类型(通过函数eth_type_trans)比如0x0800以及预先注册的接口比如

static struct packet_type ip_packet_type __read_mostly = {

.type = cpu_to_be16(ETH_P_IP),

.func = ip_rcv,

};

最终调用到了IP层的接收函数。在该函数的最后,有一个调用:

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

没错,这里的NF_HOOK就是netfilter的钩子函数,最后会走到nf_hook_slow,这里就是netfilter的总入口。当该过滤器执行完毕报文还没有被丢掉,就会接下来执行ip_rcv_finish,然后解析IP header,接着调用4层的接收函数,比如tcp_rcv。

普通的报文接收流程说完了,看起来很简单,不是嘛?接着来看看被attach到ovs bridge上的TUN/TAP端口(就是虚拟机在宿主机上对应的端口)。该端口是用ip tuntap add命令create出来的,在ovs中属于system类型(简单理解,就是系统本身的,而不是ovs创建的)。同样的,从这种类型端口接收的报文最终也会到达函数__netif_receive_skb_core。请注意,答案马上揭晓。在调用ip_rcv之前,有一处代码:

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 unlock;

     …

}

这里有一个钩子函数叫rx_handler,所有加入ovs bridge的system类型端口,都会注册一个钩子函数。这个在函数vport-netdev.c:netdev_create()可以清楚看到:

netdev_rx_handler_register(netdev_vport->dev, netdev_frame_hook, vport);

再看看这个函数netdev_frame_hook,最终调用到了接口ovs_vport_receive,接下来就是走ovs的flow match和execute flow action的流程。rx_handler函数执行完毕后,直接就goto unlock,跳到了__netif_receive_skb_core的末尾,也就是说,把ip_rcv这块的流程被ovs接管了,不执行了,那么自然,钩子函数NF_HOOK也无法执行到。

Ovs bridge上的端口和netfilter不兼容的问题应该算大概解释清楚了,发送报文方向的可以自己去尝试去分析一下。不过,还有个问题,那为什么attach到linux bridge上就可以了呢?也很简单嘛,linux bridge虽然也注册了一个钩子函数br_handle_frame,但是人家最后是调用了NF_HOOK的。

篇首提出来的问题已经简单解释完毕,接下来把ovs internal端口的收发和vxlan端口的收发简单说一下。前面已经提到,通过TUN/TAP端口收上来的报文,最后去执行ovs的flow match了,ovs_execute_actions()->do_execute_actions()->do_output()->ovs_vport_send,如果出端口是system类型的端口,最后会通过netdev_send函数直接调用dev_queue_xmit,这个函数除了做一些qos队列调度的事情,接下来就是直接发送了dev_hard_start_xmit。

同一个宿主机上的两个ovs internal端口又是如何通信的呢?很多人会说,2层交换啊。没错,但是到底怎么实现交换的?且看下面的内核执行流程:

发送: __dev_queue_xmit()->dev_hard_start_xmit(through ops->ndo_start_xmit)->internal_dev_xmit()->ovs_dp_process_received_packet()

接收: ovs_execute_actions()->do_execute_actions()->do_output()->ovs_vport_send(through ops->send)->internal_dev_recv()->netif_rx()

绕来绕去,其实就是在执行ovs的flow match和execute action。有兴趣的可以自行去搭个环境尝试一下,比如2个namespace,每个namespace里一个ovs internal port,然后把这两个端口配置成同一个subnet的不同IP地址,最后两段互ping一下,通过命令ovs-dpctl dump-flows就能看到两条流表,分别代表两个方向。

最后来解释一下vxlan端口报文的收发。我们目前的私有云平台两个位于不同宿主机上的虚拟机间通信就是通过vxlan tunnel来实现的。细心的小伙伴可能已经发现了,我们现在的配置下,不管你加了多少个多少vxlan端口,linux内核中都无法看到该端口(通过命令ifconfig或者ip link show),ovs内核中最终也只能看到一个vxlan_sys_4789(通过命令ovs-dpctl show)。看起来似乎无法走和ovs internal port或ovs system port一样的流程来实现报文收发了,那到底怎么样才能接收和发送报文呢? 谜底在于使用udp socket。VXLAN是建立在UDP之上的一种隧道,source port随意,默认的destination port就是4789。当我们通过ovs-vsctl 增加一个vxlan类型的端口时,内核中会创建udp socket:

vxlan_sock_add(net, htons(dst_port), vxlan_rcv, vport, true, false);

函数中定义了接收接口vxlan_rcv。因此,报文接收的前半段和普通的udp报文没什么区别,就是走ip_rcv()->udp_rcv()->vxlan_udp_encap_recv(),该函数最后调用

vs->rcv(vs, skb, vxh->vx_vni);

此时vxlan_rcv出场,然后调用到了ovs_vport_receive,接下来就是愉快的ovs flow之旅。 如果是通过执行ovs流表,最后的出端口是vxlan_sys_4789呢?也简单,通过一串的执行

ovs_execute_actions()->do_execute_actions()->do_output()->ovs_vport_send(through ops->send)

到达vxlan_tnl_send->vxlan_xmit_skb->iptunnel_xmit, 后续的就纯粹是路由执行过程了。


网易云新用户大礼包:https://www.163yun.com/gift

本文来自网易实践者社区,经作者陈跃芳授权发布。