vxlan 内核实现
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
|
0xffffffff8156efa0 : __napi_schedule+0x0/0x50 [kernel] //触发软中断
|
软中断出发时候net_rx_action 会处理调用gro_cell_poll从gcells中取出skb进行消耗最终调用__netif_receive_skb_core下的ovs_vport_receive将数据包送给openvswitch流程
1
|
0xffffffffa043ea40 : ovs_vport_receive+0x0/0xd0 [openvswitch]
|
数据包送给openvswitch流程在openvswitch内部处理过程和无差别,因为此时数据包已经是解过封装了。所以该数据包会发给namespace left
该数据包会呗放入到CPU队列中等待left namespace协议栈读取消耗
1
|
0xffffffff8156f130 : enqueue_to_backlog+0x0/0x170 [kernel]
|
namespace left协议栈收到该数包发现是发给本机接口的数据包,直接回复icmp reply
1
|
0xffffffff815e8040 : icmp_rcv+0x0/0x380 [kernel]
|
vxlan发包过程
因为最终数据包从openvswitch侧发给了vxlan口,vxlan口会调用dev_hard_start_xmit将数据包发送出去,因为是vxlan口所以需要对数据包进行封装,很显然封装的过程具体实现细节
发生在udp_tunnel_xmit_skb 和 iptunnel_xmit函数中,最后调用ip_local_out_sk将封装好的数据包当成本机数据包发出去,当然此时二层、三次转发查找路由的过程,都是借用的本机发包的流程了,这里就不再详细说明了
1
|
0xffffffff815fbfc0 : iptunnel_xmit+0x0/0x1a0 [kernel]
|
vlxan数据包UDP端口的选择
从代码实现来看,应该是根据vxlan封装前的源目的ip和端口进行hash获取的UDP发送端口,细节后续再研究
1
|
static inline __be16 udp_flow_src_port(struct net *net, struct sk_buff *skb,
|

浙公网安备 33010602011771号