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, |