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)