TCP雖然能保證傳輸的可靠性,但其繁瑣的狀態機以及複雜的擁塞控制機制讓它難以做爲隧道報文的外層封裝,詳見TCP-in-TCP。linux
相對而言,UDP就沒這個困擾了,丟包的事情交給應用層處理就行。於是,很多隧道協議都是將UDP做爲外層報文的方案。天然而然,與網絡發展聯繫緊密的Linux內核也開始支持這些隧道協議,較新的內核已經支持fou、l2tp、vxlan、tipc、geneve等UDP隧道協議。git
最開始,各個隧道協議都是獨立實現的,但隨着數量的增多,在patch以後,內核將這些UDP隧道公共的部分抽離出來,也就造成了UDP隧道框架,其涉及的API在include/net/udp_tunnel.h
中定義segmentfault
下圖以vxlan爲例,展現了內核UDP隧道的工做過程:網絡
其中,左邊是發送端,右邊是接收端,綠色陰影的部分是內核協議棧。能夠看出,不管是發送端仍是接收端,都涉及函數重入:發送端兩次進入ip_local_out()
, 接收端兩次進入ip_local_deliver()
框架
對發送端來講,第一次進入ip_local_out()
傳入的sk
是與原始報文關聯的套接字,也就是原始協議的套接字,它多是個TCP套接字,也多是UDP套接字或者RAWIP套接字,隧道並不care這件事。可是第二次進入ip_local_out()
時,它須要一個隧道的UDP套接字。UDP隧道框架提供了一個建立隧道套接字的API。socket
static inline int udp_sock_create(struct net *net, struct udp_port_cfg *cfg, struct socket **sockp) { if (cfg->family == AF_INET) return udp_sock_create4(net, cfg, sockp); ...... return -EPFNOSUPPORT; }
cfg
參數指定了UDP隧道本端和對端和IP地址和使用的端口號。這裏建立的套接字都是內核套接字(區別於用戶態使用socket()建立的)函數
接收端也是一樣的道理,從真實網卡收到的必定是一個UDP報文,所以接收端也須要一個UDP套接字。這個套接字中記錄的地址和端口信息與接收端正好相反。spa
對接收端來講,收到UDP報文後,它還須要將報文找到分流給正確的隧道協議,好比這個UDP隧道報文是交給vxlan,仍是交給geneve?又或者這根本就只是一個普通的UDP報文,不是一個UDP隧道報文?code
所以,內核須要將如何分流記錄在UDP套接字上。blog
struct udp_sock { ...... /* * For encapsulation sockets. */ int (*encap_rcv)(struct sock *sk, struct sk_buff *skb); ...... };
這裏的encap_rcv
回調函數即是起到隧道報文分流的做用
在UDP接收時,內核會首先查看套接字上是否設置了該回調函數,若是設置了,表示這是一個隧道套接字。調用對應的處理函數,好比vxlan隧道會將其設置爲vxlan_rcv
,genven隧道會將其設置爲geneve_udp_encap_recv
。
設置的過程是經過下面這個API完成的
void setup_udp_tunnel_sock(struct net *net, struct socket *sock, struct udp_tunnel_sock_cfg *cfg)