起源
TCP 虽然能保证传输的可靠性,但其繁琐的状态机以及复杂的拥塞控制机制让它难以作为隧道报文的外层封装,详见 TCP-in-TCP。
相对而言,UDP 就没这个困扰了,丢包的事情交给应用层处理就行。因而,不少隧道协议都是将 UDP 作为外层报文的方案。自然而然,与网络发展联系紧密的 Linux 内核也开始支持这些隧道协议,较新的内核已经支持 fou、l2tp、vxlan、tipc、geneve 等 UDP 隧道协议。
最开始,各个隧道协议都是独立实现的,但随着数量的增多,在 patch 之后,内核将这些 UDP 隧道公共的部分抽离出来,也就形成了 UDP 隧道框架,其涉及的 API 在 include/net/udp_tunnel.h
中定义
内核 UDP 隧道的原理
下图以 vxlan 为例,展示了内核 UDP 隧道的工作过程:
其中,左边是发送端,右边是接收端,绿色阴影的部分是内核协议栈。可以看出,无论是发送端还是接收端,都涉及 函数重入:发送端两次进入ip_local_out()
, 接收端两次进入ip_local_deliver()
隧道 socket
对发送端来说,第一次进入 ip_local_out()
传入的 sk
是与原始报文关联的套接字,也就是原始协议的套接字,它可能是个 TCP 套接字,也可能是 UDP 套接字或者 RAWIP 套接字,隧道并不 care 这件事。但是第二次进入 ip_local_out()
时,它需要一个隧道的 UDP 套接字。UDP 隧道框架提供了一个创建隧道套接字的 API。
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 套接字。这个套接字中记录的地址和端口信息与接收端正好相反。
Encap rcv 回调函数
对接收端来说,收到 UDP 报文后,它还需要将报文找到分流给正确的隧道协议,比如这个 UDP 隧道报文是交给 vxlan,还是交给 geneve?又或者这根本就只是一个普通的 UDP 报文,不是一个 UDP 隧道报文?
因此,内核需要将如何分流记录在 UDP 套接字上。
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)