1. veth 是什么 veth ,又名虚拟网络设备对,主要是用于解决不同网络命名空间之间的通信。
说起网络名称空间(network namespace),大家应该都不陌生,这是 Linux 用来隔离容器网络环境的一项技术,主要隔离的资源有:
iptables
路由规则表
网络设备列表
虽然不同 namespace 之间是隔离的,但也有办法让它们之间完成通信,veth 就是其中的一种比较常见的解决方式。
可以将 veth 看成是两块通过网线连接的网卡,只要将其中之一放置到网络命名空间 A,另一个放置到网络命名空间 B,那么两个不同的网络命名空间就能够通信。
众所周知,网线是一个冷酷无情的传输设备,从一端发送的数据会沿着线路传输到另一端,交付给另一端的设备,这一切是由硬件来完成的,不需要我们去干预,不过对于 veth 这种软件模拟的方式来说,就需要动一番脑子了。
常用的 veth 的使用方式为:
ip link add veth0 type veth peer name veth1 ip link set dev veth0 up ip link set dev veth1 up ip link set veth0 netns netns0 ip link set veth1 netns netns1 ip netns exec ns0 ip a a 10.1.1.2/24 dev veth0 ip netns exec ns1 ip a a 10.1.1.3/24 dev veth1 ip netns exec ns0 ping -I veth0 10.1.1.3
2. veth internal 下面分别从 创建,初始化,发送数据 三个角度来剖析 veth 源码。
2.1 创建 veth pair 使用 ip 命令创建一对 veth 时,会通过 netlink 触发 rtnl_link_ops.newlink
回调函数,如果感兴趣,可以看我的另一篇文章 [netlink 机制] ,对应 veth_newlink
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 static int veth_newlink (struct net *src_net, struct net_device *dev, struct nlattr *tb[], struct nlattr *data[], struct netlink_ext_ack *extack) { net = rtnl_link_get_net(src_net, tbp); if (IS_ERR(net)) return PTR_ERR(net); peer = rtnl_create_link(net, ifname, name_assign_type, &veth_link_ops, tbp, extack); err = register_netdevice(peer); put_net(net); net = NULL ; if (err < 0 ) goto err_register_peer; netif_carrier_off(peer); err = rtnl_configure_link(peer, ifmp); if (err < 0 ) goto err_configure_peer; err = register_netdevice(dev); netif_carrier_off(dev); priv = netdev_priv(dev); rcu_assign_pointer(priv->peer, peer); priv = netdev_priv(peer); rcu_assign_pointer(priv->peer, dev); return 0 ; }
veth 由两个设备组成,其中 dev 在调用 veth_newlink
之前已经创建完成了,接下来首先需要通过 rtnl_create_link
创建另一个设备 peer,并通过 register_netdevice
分别将 peer 和 dev 注册到系统的 netdevice 列表当中
下一步是 veth 的核心:通过 priv 字段关联 dev 和 peer,即 rcu_assign_pointer(priv->peer, peer)
和 rcu_assign_pointer(priv->peer, dev)
这里有必要解释一下 net_device
的结构,以及 netdev_priv
的作用
Kernel 使用 net_device
来对网络设备进行抽象,但是不同厂商的设备规格有一定差别,所以 net_device 结构体采用了面向对象 的思想,不仅有所有设备共有的结构(基类),还保留了私有数据的存储空间(子类),私有结构一般保存在 net_device 对象结束之后的位置(不难从 netdev_priv
中的 sizeof(struct net_device
得到证实),从而可以轻松使用强制类型转换分别获取到父类和子类,示意图如下所示:
1 2 3 4 5 6 7 8 9 |------------|\ | net_device | \ | | / sizeof net_device |------------|/ |------------|\ | xx_private | \ | | / sizeof private struct |------------|/
1 2 3 4 static inline void *netdev_priv (const struct net_device *dev) { return (char *)dev + ALIGN(sizeof (struct net_device), NETDEV_ALIGN); }
而对于 veth 来说,这个 private 字段就是 struct veth_priv
1 2 3 4 5 6 7 struct veth_priv { struct net_device __rcu *peer ; atomic64_t dropped; struct bpf_prog *_xdp_prog ; struct veth_rq *rq ; unsigned int requested_headroom; };
通过私有结构中的 peer 字段就能关联另一个 net_device
,这下你应该理解了 veth pair 是如何成双结对的了。
2.2 初始化 veth 创建好 veth 之后,还需要通过 rtnl_link_ops.setup
进行初始化才能使用,对应 veth_setup
该函数中主要设置了 veth 对应 net_device 的一些字段
1 2 3 4 5 6 7 static void veth_setup (struct net_device *dev) { ether_setup(dev); dev->netdev_ops = &veth_netdev_ops; ... }
最重要的是通过 dev->netdev_ops = &veth_netdev_ops;
设置了收发包的处理函数,典型的 Linux 回调设计模式。
在 veth 模块 的初始化操作中,还将 veth_netdev_ops 注册到了 rtnetlink 中,从而可以通过 netlink 向外提供服务
1 2 3 4 static __init int veth_init (void ) { return rtnl_link_register(&veth_link_ops); }
像 ip link add veth0 type veth peer name veth1
这些和 veth 相关的网络命令,都会通过 netlink socket,最终交由 veth_link_ops 中的回调函数处理。
2.3 发送数据 在驱动层,内核通过 netdev_ops->ndo_start_xmit
发送数据包,前面说到,veth_setup
函数中已经将 netdev_ops 设置为 veth_netdev_ops
,其中 ndo_start_xmit
对应下面的 veth_xmit
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 static netdev_tx_t veth_xmit (struct sk_buff *skb, struct net_device *dev) { struct veth_priv *rcv_priv , *priv = netdev_priv(dev); rcu_read_lock(); rcv = rcu_dereference(priv->peer); if (likely(veth_forward_skb(rcv, skb, rq, rcv_xdp) == NET_RX_SUCCESS)) { if (!rcv_xdp) dev_lstats_add(dev, length); } else { drop: atomic64_inc(&priv->dropped); } if (rcv_xdp) __veth_xdp_flush(rq); rcu_read_unlock(); return NETDEV_TX_OK; }
来看该函数的逻辑:首先获取到了 veth_priv
结构中的 peer,也就是 veth 的对端虚拟网络设备,接下来调用 veth_forward_skb
,将数据包转发到 peer 上。
1 2 3 4 5 6 7 static int veth_forward_skb (struct net_device *dev, struct sk_buff *skb, struct veth_rq *rq, bool xdp) { return __dev_forward_skb(dev, skb) ?: xdp ? veth_xdp_rx(rq, skb) : netif_rx(skb); }
其实,无论什么时候,将数据包从网络层发送到网络设备上,最重要的都是设置 skb 对应的 net_device,从而调用对应的 net_device_ops->ndo_start_xmit
。veth 数据包既然要发送到另一端的设备上,只需要将 skb->dev 设置为 peer 即可,之后便可以走正常的网络包接收路径 netif_rx
。
netif_rx
操作中,主要是将数据包放置到了 backlog
队列中,触发自底向上的收包流程,从这里开始,便与物理网络设备无异了。
1 2 3 4 5 6 7 static int netif_rx_internal (struct sk_buff *skb) { ret = enqueue_to_backlog(skb, get_cpu(), &qtail); return ret; }
很有意思的一点是,从 veth_xmit 这个发送数据包的路径,转移到了 netif_rx_internal 这个接收数据包的路径,利用好 veth 这个特点,可以完成很多有趣的事情。比如 docker 中的桥接模式,就是非常典型的应用。