起源TCP虽然可以保证传输的可靠性,但其繁琐的状态机和复杂的拥塞控制机制使其难以作为隧道数据包的外层封装。有关详细信息,请参阅TCP-in-TCP。相对来说UDP没有这个问题,丢包的事情可以交由应用层来处理。因此,许多隧道协议都使用UDP作为外层消息。自然而然,与网络发展息息相关的Linux内核也开始支持这些隧道协议。较新的内核已经支持UDP隧道协议,例如fou、l2tp、vxlan、tipc和geneve。一开始,每个隧道协议都是独立实现的,但随着数量的增加,打补丁后,内核提取了这些UDP隧道的公共部分,形成了一个UDP隧道框架。涉及到的API在include/net/udp_tunnel.h中定义内核UDP隧道的原理下图以vxlan为例,展示了内核UDP隧道的工作过程:其中,发送端在左边,右边是接收端,绿色阴影部分是内核协议栈。可以看出发送端和接收端都涉及到函数重入:发送端两次进入ip_local_out(),接收端两次进入ip_local_deliver()隧道套接字。对于发送端来说,第一次进入ip_local_out()时传入的sk是与原始消息关联的socket,即原始协议的socket。它可以是TCP套接字,也可以是UDP套接字或RAWIP套接字。隧道不关心这个东西。但它第二次转到ip_local_out()时,它需要一个隧道UDP套接字。UDP隧道框架提供了一个API来创建隧道套接字。staticinlineintudp_sock_create(structnet*net,structudp_port_cfg*cfg,structsocket**sockp){if(cfg->family==AF_INET)返回udp_sock_create4(net,cfg,sockp);…return-EPFNOSUPPORT;}cfg参数指定UDP隧道本地端和对端、IP地址和使用的端口号。这里创建的套接字都是内核套接字(不同于用户态下socket()创建的套接字)。接收端也是如此。从真正的网卡接收到的一定是UDP报文,所以接收端也需要一个UDP套接字。这个socket中记录的地址和端口信息与接收端正好相反。Encaprcv回调函数对于接收端来说,在收到UDP报文后,还需要找到报文并将其分发到正确的隧道协议中。比如UDP隧道报文应该交给vxlan还是geneve?或者这根本就是一条普通的UDP消息,而不是UDP隧道消息?因此,内核需要记录如何卸载UDP套接字。structudp_sock{....../**用于封装套接字。*/int(*encap_rcv)(structsock*sk,structsk_buff*skb);......};这里的encap_rcv回调函数就是起到隧道分包的作用。当接收到UDP时,内核首先会检查套接字上是否设置了回调函数。如果已设置,则表示这是一个隧道套接字。调用相应的处理函数。比如vxlan隧道会设置为vxlan_rcv,genven隧道会设置为geneve_udp_encap_recv。设置过程通过以下APIvoidsetup_udp_tunnel_sock(structnet*net,structsocket*sock,structudp_tunnel_sock_cfg*cfg)
