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