首页 > 技术文章 > vxlan 内核实现

dream397 2021-03-09 16:27 原文

 

 

 

vxlan_tnl_send
根据vxlan tunnel的ip查找路由。
调用vxlan_xmit_skb封装发送报文。
vxlan_xmit_skb
计算封装vxlan需要的最小空间,并且扩展头部空间。
添加vxlan头。
如果有BGP的头,也添加。
udp_tunnel_xmit_skb添加协议头发送。
udp_tunnel_xmit_skb
添加UDP协议头。
iptunnel_xmit继续添加协议头,并且发送。
iptunnel_xmit
添加ip协议头。
ip_local_out_sk–>__ip_local_out–>__ip_local_out_sk继续添加协议头,并且发送。
__ip_local_out_sk
过netfilter的LOCAL_OUT。
调用dst_output_sk–>ip_output。
ip_output
过netfilter的POST_ROUTING。
调用ip_finish_output
ip_finish_output
如果报文支持gso,调用ip_finish_output_gso进行分片。
如果报文大于mtu,调用ip_fragment进行分片。
调用ip_finish_output2进行报文发送。
ip_finish_output2
__ipv4_neigh_lookup_noref查找邻居子系统。
调用dst_neigh_output–>neigh_hh_output进行报文发送。
neigh_hh_output
封装2层协议头。
调用dev_queue_xmit进行报文发送

 

 

Linux 内核支持 GSO for UDP tunnels

  • 需要在 skb 发到 UDP 协议栈之前,添加一个新的 option:inner_protocol,可以使用方法 skb_set_inner_ipproto 或者 skb_set_inner_protocol 来设置。vxlan driver 中的相关代码为 skb_set_inner_protocol(skb, htons(ETH_P_TEB));
  • 函数
    skb_udp_tunnel_segment 会检查该 option 再处理分段。
  • 支持多种类型的封装,包括 SKB_GSO_UDP_TUNNEL{_CSUM}

 

 

  其驱动设置了 net_device_ops结构体变量, 其中定义了操作 net_device 的重要函数,vxlan在驱动程序中根据需要的操作要填充这些函数,其中主要是 packets 的接收和发送处理函数。

 

复制代码
static const struct net_device_ops vxlan_netdev_ops = {
    .ndo_init        = vxlan_init,      
    .ndo_uninit        = vxlan_uninit,
    .ndo_open        = vxlan_open,
    .ndo_stop        = vxlan_stop,
    .ndo_start_xmit        = vxlan_xmit,  #向 vxlan interface 发送 packet
    ...
};
复制代码

来看看代码实现:

(1)首先看 static netdev_tx_t vxlan_xmit(struct sk_buff *skb, struct net_device *dev) 方法,它的输入就是要传输的 packets  所对应的 sk_buff 以及要经过的 vxlan interface dev:

它的主要逻辑是获取 vxlan dev,然后为 sk_buff 中的每一个 skb 调用 vxlan_xmit_skb 方法。

#该方法主要逻辑是,计算 tos,ttl,df,src_port,dst_port,md 以及 flags等,然后调用 vxlan_xmit_skb 方法。
err = vxlan_xmit_skb(rt, sk, skb, fl4.saddr, dst->sin.sin_addr.s_addr, tos, ttl, df, src_port, dst_port, htonl(vni << 8), md, !net_eq(vxlan->net, dev_net(vxlan->dev)), flags);

(2)vxlan_xmit_skb 函数修改了 skb,添加了 VxLAN Header,以及设置 GSO 参数。

复制代码
static int vxlan_xmit_skb(struct rtable *rt, struct sock *sk, struct sk_buff *skb,
              __be32 src, __be32 dst, __u8 tos, __u8 ttl, __be16 df,
              __be16 src_port, __be16 dst_port, __be32 vni,
              struct vxlan_metadata *md, bool xnet, u32 vxflags)
{
    ...int type = udp_sum ? SKB_GSO_UDP_TUNNEL_CSUM : SKB_GSO_UDP_TUNNEL; #计算 GSO UDP 相关的 offload type,使得能够利用内核 GSO for UDP Tunnel
    u16 hdrlen = sizeof(struct vxlanhdr); #计算 vxlan header 的长度
    ...
#计算 skb 新的 headroom,其中包含了 VXLAN Header 的长度 min_headroom = LL_RESERVED_SPACE(rt->dst.dev) + rt->dst.header_len + VXLAN_HLEN + sizeof(struct iphdr) + (skb_vlan_tag_present(skb) ? VLAN_HLEN : 0); /* Need space for new headers (invalidates iph ptr) */ err = skb_cow_head(skb, min_headroom); #使得 skb head 可写 ... skb = vlan_hwaccel_push_inside(skb); #处理 vlan 相关事情 ... skb = iptunnel_handle_offloads(skb, udp_sum, type); #设置 checksum 和 type ... vxh = (struct vxlanhdr *) __skb_push(skb, sizeof(*vxh)); #扩展 skb data area,来容纳 vxlan header vxh->vx_flags = htonl(VXLAN_HF_VNI); vxh->vx_vni = vni; ... if (vxflags & VXLAN_F_GBP) vxlan_build_gbp_hdr(vxh, vxflags, md); skb_set_inner_protocol(skb, htons(ETH_P_TEB)); #设置 Ethernet protocol,这是 GSO 在 UDP tunnel 中必须要的 udp_tunnel_xmit_skb(rt, sk, skb, src, dst, tos, ttl, df, #调用 linux 网络栈接口,将 skb 传给 udp tunnel 协议栈继续处理 src_port, dst_port, xnet, !(vxflags & VXLAN_F_UDP_CSUM)); return 0; }
复制代码

 

 (3)接下来就进入了 Linux TCP/IP 协议栈,从 UDP 进入,然后再到 IP 层。如果硬件支持,则由硬件调用 linux 内核中的 UDP GSO 函数;如果硬件不支持,则在进入 device driver queue 之前由 linux 内核调用 UDP GSO 分片函数。然后再一直往下到网卡。

最终在这个函数 ip_finish_output_gso 里面,先调用 GSO分段函数,如果需要的话,再进行 IP 分片:


static
int ip_finish_output(struct sock *sk, struct sk_buff *skb) { #if defined(CONFIG_NETFILTER) && defined(CONFIG_XFRM) /* Policy lookup after SNAT yielded a new policy */ if (skb_dst(skb)->xfrm) { //仅经过ip_forward流程处理的报文携带该对象 IPCB(skb)->flags |= IPSKB_REROUTED; //该flag会影响后续报文的GSO处理 return dst_output_sk(sk, skb); //由于SNAT等策略处理,需要再次调用xfrm4_output函数来发包 } #endif if (skb_is_gso(skb)) return ip_finish_output_gso(sk, skb); //如果是gso报文 if (skb->len > ip_skb_dst_mtu(skb)) //非gso报文,报文大小超过设备MTU值,则需要进行IP分片 return ip_fragment(sk, skb, ip_finish_output2); return ip_finish_output2(sk, skb); //直接发送报文 }

 

static int ip_finish_output_gso(struct net *net, struct sock *sk,
                                 struct sk_buff *skb, unsigned int mtu)
 {
         netdev_features_t features;
         struct sk_buff *segs;
         int ret = 0;
 
         /* Slowpath -  GSO segment length is exceeding the dst MTU.
          *
          * This can happen in two cases:
          * 1) TCP GRO packet, DF bit not set
          * 2) skb arrived via virtio-net, we thus get TSO/GSO skbs directly
          * from host network stack.
          */
         features = netif_skb_features(skb);
         segs = skb_gso_segment(skb, features & ~NETIF_F_GSO_MASK); #这里最终会调用到 UDP 的 gso_segment 回调函数进行 UDP GSO 分段
         if (IS_ERR_OR_NULL(segs)) {
                 kfree_skb(skb);
                 return -ENOMEM;
         }
 
         consume_skb(skb);
 
         do {
                 struct sk_buff *nskb = segs->next;
                 int err;
 
                 segs->next = NULL;
                 err = ip_fragment(net, sk, segs, mtu, ip_finish_output2); #需要的话,再进行 IP 分片,因为 UDP GSO 是按照 MSS 进行,MSS 还是有可能超过 IP 分段所使用的宿主机物理网卡 MTU 的 
                 if (err && ret == 0)
                         ret = err;
                 segs = nskb;
         } while (segs);
 
         return ret;
 }
复制代码
  • 在函数 static int ip_finish_output_gso(struct net *net, struct sock *sk,  struct sk_buff *skb, unsigned int mtu) 中能看到,首先按照 MSS 做 GSO,然后在调用 ip_fragment 做 IP 分片。可见,在通常情况下(虚机 TCP MSS 要比物理网卡 MTU 小),只做 UDP GSO 分段,IP 分片是不需要做的;只有在特殊情况下 (虚机 TCP MSS 超过了宿主机物理网卡 MTU),IP 分片才会做。 

这是 UDP 层所注册的 gso 回调函数:

复制代码
static const struct net_offload udpv4_offload = {
    .callbacks = {
        .gso_segment = udp4_ufo_fragment,
        .gro_receive  =    udp4_gro_receive,
        .gro_complete =    udp4_gro_complete,
    },
};
复制代码

它的实现在这里:

复制代码
static struct sk_buff *__skb_udp_tunnel_segment(struct sk_buff *skb, netdev_features_t features,
    struct sk_buff *(*gso_inner_segment)(struct sk_buff *skb, netdev_features_t features), __be16 new_protocol)
{
    .../* segment inner packet. */ #先调用内层的 分段函数进行分段
    enc_features = skb->dev->hw_enc_features & netif_skb_features(skb);
    segs = gso_inner_segment(skb, enc_features);
    ...
    skb = segs;
    do { #执行 UDP GSO 分段
        struct udphdr *uh;
        int len;

        skb_reset_inner_headers(skb);
        skb->encapsulation = 1;

        skb->mac_len = mac_len;

        skb_push(skb, outer_hlen);
        skb_reset_mac_header(skb);
        skb_set_network_header(skb, mac_len);
        skb_set_transport_header(skb, udp_offset);
        len = skb->len - udp_offset;
        uh = udp_hdr(skb);
        uh->len = htons(len);
        ...
        skb->protocol = protocol;
    } while ((skb = skb->next));
out:
    return segs;
}

struct sk_buff *skb_udp_tunnel_segment(struct sk_buff *skb, netdev_features_t features, bool is_ipv6)
{
    ...switch (skb->inner_protocol_type) { #计算内层的分片方法
    case ENCAP_TYPE_ETHER: #感觉 vxlan 的 GSO 应该是走这个分支,相当于是将 VXLAN 所封装的二层帧当做 payload 来分段,而不是将包含 VXLAN Header 的部分来分
        protocol = skb->inner_protocol;
        gso_inner_segment = skb_mac_gso_segment;
        break;
    case ENCAP_TYPE_IPPROTO:
        offloads = is_ipv6 ? inet6_offloads : inet_offloads;
        ops = rcu_dereference(offloads[skb->inner_ipproto]);
        if (!ops || !ops->callbacks.gso_segment)
            goto out_unlock;
        gso_inner_segment = ops->callbacks.gso_segment;
        break;
    default:
        goto out_unlock;
    }

    segs = __skb_udp_tunnel_segment(skb, features, gso_inner_segment,
                    protocol);
    ...
    return segs; #返回分片好的seg list
}
复制代码

这里比较有疑问的是,VXLAN 没有定义 gso_segment 回调函数,这导致有可能在 UDP GSO 分段里面没有完整的 VXLAN Header。这需要进一步研究。原因可能是在 inner segment 那里,分段是将 UDP 所封装的二层帧当做 payload 来分段,因此,VXLAN Header 就会保持在每个分段中。

(4)可见,在整个过程中,有客户机上 TCP 协议层设置的 skb_shinfo(skb)->gso_size 始终保持不变为 MSS,因此,在网卡中最终所做的针对 UDP GSO 数据报的 GSO 分片所依据的分片的长度还是根据 skb_shinfo(skb)->gso_size 的值即 TCP MSS。

 

 

vxlan收包处理过程

openvswitch vxlan收包过程如下

默认情况下发给4789端口的udp数据包,会在内核态呗截取,交给vxlan_rcv处理,vxlan_rcv该函数负责解封装然后将数据包挂入gcells

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
0xffffffff8156efa0 : __napi_schedule+0x0/0x50 [kernel]  //触发软中断

0xffffffffa045d67b : vxlan_rcv+0x99b/0xb00 [vxlan]

0xffffffff815e2818 : udp_queue_rcv_skb+0x1f8/0x4f0 [kernel]

0xffffffff815e355a : __udp4_lib_rcv+0x54a/0x880 [kernel]

0xffffffff815e3dfa : udp_rcv+0x1a/0x20 [kernel]

0xffffffff815b1584 : ip_local_deliver_finish+0xb4/0x1f0 [kernel]

0xffffffff815b1869 : ip_local_deliver+0x59/0xd0 [kernel]

0xffffffff815b120a : ip_rcv_finish+0x8a/0x350 [kernel]

0xffffffff815b1b96 : ip_rcv+0x2b6/0x410 [kernel]

0xffffffff81570062 : __netif_receive_skb_core+0x582/0x800 [kernel]

0xffffffff815702f8 : __netif_receive_skb+0x18/0x60 [kernel]

0xffffffff81570380 : netif_receive_skb_internal+0x40/0xc0 [kernel]

0xffffffff81571498 : napi_gro_receive+0xd8/0x130 [kernel]

0xffffffffa00472fc : e1000_clean_rx_irq+0x2ac/0x4f0 [e1000]

0xffffffffa0047d31 : e1000_clean+0x281/0x8f0 [e1000]

0xffffffff81570b20 : net_rx_action+0x170/0x380 [kernel]

0xffffffff8108f63f : __do_softirq+0xef/0x280 [kernel]

0xffffffff8169919c : call_softirq+0x1c/0x30 [kernel]

0xffffffff8102d365 : do_softirq+0x65/0xa0 [kernel]

0xffffffff8108f9d5 : irq_exit+0x115/0x120 [kernel]

 

软中断出发时候net_rx_action 会处理调用gro_cell_poll从gcells中取出skb进行消耗最终调用__netif_receive_skb_core下的ovs_vport_receive将数据包送给openvswitch流程

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
0xffffffffa043ea40 : ovs_vport_receive+0x0/0xd0 [openvswitch]

0xffffffffa043fc8e : netdev_frame_hook+0xde/0x160 [openvswitch]

0xffffffff8156fcc2 : __netif_receive_skb_core+0x1e2/0x800 [kernel]

0xffffffff815702f8 : __netif_receive_skb+0x18/0x60 [kernel]

0xffffffff81570380 : netif_receive_skb_internal+0x40/0xc0 [kernel]

0xffffffff81571498 : napi_gro_receive+0xd8/0x130 [kernel]

0xffffffffa045a30a : gro_cell_poll+0x7a/0xc0 [vxlan]

0xffffffff81570b20 : net_rx_action+0x170/0x380 [kernel]

0xffffffff8108f63f : __do_softirq+0xef/0x280 [kernel]

0xffffffff8169919c : call_softirq+0x1c/0x30 [kernel]

0xffffffff8102d365 : do_softirq+0x65/0xa0 [kernel]

0xffffffff8108f9d5 : irq_exit+0x115/0x120 [kernel]

0xffffffff81699d38 : do_IRQ+0x58/0xf0 [kernel]

0xffffffff8168eded : ret_from_intr+0x0/0x15 [kernel]

数据包送给openvswitch流程在openvswitch内部处理过程和无差别,因为此时数据包已经是解过封装了。所以该数据包会发给namespace left

该数据包会呗放入到CPU队列中等待left namespace协议栈读取消耗

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
0xffffffff8156f130 : enqueue_to_backlog+0x0/0x170 [kernel]

0xffffffff8156f2e5 : netif_rx_internal+0x45/0x120 [kernel]

0xffffffff8156f3de : dev_forward_skb+0x1e/0x30 [kernel]

0xffffffffa03a34ba : veth_xmit+0x2a/0x60 [veth]

0xffffffff8156f8a1 : dev_hard_start_xmit+0x171/0x3b0 [kernel]

0xffffffff81572656 : __dev_queue_xmit+0x466/0x570 [kernel]

0xffffffff81572770 : dev_queue_xmit+0x10/0x20 [kernel]

0xffffffffa03881d4 : ovs_vport_send+0x44/0xb0 [openvswitch]

0xffffffffa037a300 : do_output.isra.31+0x40/0x150 [openvswitch]

0xffffffffa037b74d : do_execute_actions+0x73d/0x890 [openvswitch]

0xffffffffa037b8e1 : ovs_execute_actions+0x41/0x130 [openvswitch]

0xffffffffa037e929 : ovs_packet_cmd_execute+0x2c9/0x2f0 [openvswitch]

0xffffffff815a6d5a : genl_family_rcv_msg+0x20a/0x430 [kernel]

0xffffffff815a7011 : genl_rcv_msg+0x91/0xd0 [kernel]

0xffffffff815a4f89 : netlink_rcv_skb+0xa9/0xc0 [kernel]

0xffffffff815a54b8 : genl_rcv+0x28/0x40 [kernel]

0xffffffff815a467d : netlink_unicast+0xed/0x1b0 [kernel]

0xffffffff815a4a5e : netlink_sendmsg+0x31e/0x690 [kernel]

0xffffffff81555ef0 : sock_sendmsg+0xb0/0xf0 [kernel]

0xffffffff81556799 : ___sys_sendmsg+0x3a9/0x3c0 [kernel]

 

namespace left协议栈收到该数包发现是发给本机接口的数据包,直接回复icmp reply

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
0xffffffff815e8040 : icmp_rcv+0x0/0x380 [kernel]

0xffffffff815b1584 : ip_local_deliver_finish+0xb4/0x1f0 [kernel]

0xffffffff815b1869 : ip_local_deliver+0x59/0xd0 [kernel]

0xffffffff815b120a : ip_rcv_finish+0x8a/0x350 [kernel]

0xffffffff815b1b96 : ip_rcv+0x2b6/0x410 [kernel]

0xffffffff81570062 : __netif_receive_skb_core+0x582/0x800 [kernel]

0xffffffff815702f8 : __netif_receive_skb+0x18/0x60 [kernel]

0xffffffff8157159e : process_backlog+0xae/0x170 [kernel]

0xffffffff81570b20 : net_rx_action+0x170/0x380 [kernel]

0xffffffff8108f63f : __do_softirq+0xef/0x280 [kernel]

0xffffffff8169919c : call_softirq+0x1c/0x30 [kernel]

0xffffffff8102d365 : do_softirq+0x65/0xa0 [kernel]

0xffffffff8108e894 : local_bh_enable+0x94/0xa0 [kernel]

0xffffffffa037e930 : ovs_packet_cmd_execute+0x2d0/0x2f0 [openvswitch]

0xffffffff815a6d5a : genl_family_rcv_msg+0x20a/0x430 [kernel]

0xffffffff815a7011 : genl_rcv_msg+0x91/0xd0 [kernel]

0xffffffff815a4f89 : netlink_rcv_skb+0xa9/0xc0 [kernel]

0xffffffff815a54b8 : genl_rcv+0x28/0x40 [kernel]

0xffffffff815a467d : netlink_unicast+0xed/0x1b0 [kernel]

0xffffffff815a4a5e : netlink_sendmsg+0x31e/0x690 [kernel]

 

vxlan发包过程

因为最终数据包从openvswitch侧发给了vxlan口,vxlan口会调用dev_hard_start_xmit将数据包发送出去,因为是vxlan口所以需要对数据包进行封装,很显然封装的过程具体实现细节

发生在udp_tunnel_xmit_skb 和 iptunnel_xmit函数中,最后调用ip_local_out_sk将封装好的数据包当成本机数据包发出去,当然此时二层、三次转发查找路由的过程,都是借用的本机发包的流程了,这里就不再详细说明了

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
0xffffffff815fbfc0 : iptunnel_xmit+0x0/0x1a0 [kernel]

0xffffffffa02b12b3 : udp_tunnel_xmit_skb+0xe3/0x100 [udp_tunnel]

0xffffffffa039a253 : vxlan_xmit_one+0x7e3/0xb60 [vxlan]

0xffffffffa039b81f : vxlan_xmit+0x41f/0xce0 [vxlan]

0xffffffff8156f8a1 : dev_hard_start_xmit+0x171/0x3b0 [kernel]

0xffffffff81572656 : __dev_queue_xmit+0x466/0x570 [kernel]

0xffffffff81572770 : dev_queue_xmit+0x10/0x20 [kernel]

0xffffffffa03881d4 : ovs_vport_send+0x44/0xb0 [openvswitch]

0xffffffffa037a300 : do_output.isra.31+0x40/0x150 [openvswitch]

0xffffffffa037b74d : do_execute_actions+0x73d/0x890 [openvswitch]

0xffffffffa037b8e1 : ovs_execute_actions+0x41/0x130 [openvswitch]

0xffffffffa037f445 : ovs_dp_process_packet+0x85/0x110 [openvswitch]

0xffffffffa0387aac : ovs_vport_receive+0x6c/0xd0 [openvswitch]

0xffffffffa0388c8e : netdev_frame_hook+0xde/0x160 [openvswitch]

0xffffffff8156fcc2 : __netif_receive_skb_core+0x1e2/0x800 [kernel]

0xffffffff815702f8 : __netif_receive_skb+0x18/0x60 [kernel]

0xffffffff8157159e : process_backlog+0xae/0x170 [kernel]

0xffffffff81570b20 : net_rx_action+0x170/0x380 [kernel]

0xffffffff8108f63f : __do_softirq+0xef/0x280 [kernel]

0xffffffff8169919c : call_softirq+0x1c/0x30 [kernel]

 

vlxan数据包UDP端口的选择

从代码实现来看,应该是根据vxlan封装前的源目的ip和端口进行hash获取的UDP发送端口,细节后续再研究

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
static inline __be16 udp_flow_src_port(struct net *net, struct sk_buff *skb,
int min, int max, bool use_eth)
{
u32 hash;

if (min >= max) {
/* Use default range */
inet_get_local_port_range(net, &min, &max);
}

hash = skb_get_hash(skb);
if (unlikely(!hash) && use_eth) {
/* Can't find a normal hash, caller has indicated an Ethernet
* packet so use that to compute a hash.
*/
hash = jhash(skb->data, 2 * ETH_ALEN,
(__force u32) skb->protocol);
}

/* Since this is being sent on the wire obfuscate hash a bit
* to minimize possbility that any useful information to an
* attacker is leaked. Only upper 16 bits are relevant in the
* computation for 16 bit port value.
*/
hash ^= hash << 16;

return htons((((u64) hash * (max - min)) >> 32) + min);
}

推荐阅读