内核网络中VETH设备表示一对虚拟的互联接口,可使用以下IP命令创建:
$ sudo ip link add ep1 type veth peer name ep2
$ ip link list
5: ep2@ep1: <BROADCAST,MULTICAST,M-DOWN> mtu 1500 qdisc noop state DOWN mode DEFAULT group default qlen 1000
link/ether 66:98:ec:9d:d0:12 brd ff:ff:ff:ff:ff:ff
6: ep1@ep2: <BROADCAST,MULTICAST,M-DOWN> mtu 1500 qdisc noop state DOWN mode DEFAULT group default qlen 1000
link/ether e6:07:7e:eb:9e:c2 brd ff:ff:ff:ff:ff:ff
以上创建的一对veth设备ep1和ep2。名字没有意义可任意指定。在OpenStack中,veth通常用来连接两个虚拟的内部网桥。下面为这两个接口增加IP地址。
$ sudo ip addr add 10.0.0.10 dev ep1
$ sudo ip addr add 10.0.0.11 dev ep2
使用ping命令测试联通性。-I参数指定ping命令使用的源接口,10.0.0.10表示veth接口设备ep1。-c1表示仅发送一个ping包。
$ ping -I 10.0.0.10 -c1 10.0.0.11
PING 10.0.0.11 (10.0.0.11) from 10.0.0.10 : 56(84) bytes of data.
64 bytes from 10.0.0.11: icmp_seq=1 ttl=64 time=0.036 ms
--- 10.0.0.11 ping statistics ---
1 packets transmitted, 1 received, 0% packet loss, time 0ms
rtt min/avg/max/mdev = 0.036/0.036/0.036/0.000 ms
VETH联通命名空间
如下,验证veth连接两个网络命名空间(netns01和netns02),首先,创建这两个命名空间。
$ sudo ip netns add netns01
$ sudo ip netns add netns02
将veth设备分别移到两个网络命名空间中:
$ sudo ip link set ep1 netns netns01
$ sudo ip link set ep2 netns netns02
注意在移动完成之后,veth设备的IP地址消失。这与内核在改变设备命名空间时的操作有关,所谓移动实际上是内核首先在设备所在netnamespace中销毁设备,然后在所要移到到的网络命名空间中重建设备,因此比将导致设备的IP地址信息丢失。
$ sudo ip netns exec netns01 ip addr
6: ep1@if5: <BROADCAST,MULTICAST> mtu 1500 qdisc noop state DOWN group default qlen 1000
link/ether e6:07:7e:eb:9e:c2 brd ff:ff:ff:ff:ff:ff link-netnsid 1
$
$ sudo ip netns exec netns02 ip addr
5: ep2@if6: <BROADCAST,MULTICAST> mtu 1500 qdisc noop state DOWN group default qlen 1000
link/ether 66:98:ec:9d:d0:12 brd ff:ff:ff:ff:ff:ff link-netnsid 0
恢复我们之前的IP地址。
$ sudo ip netns exec netns01 ip addr add 10.0.0.10/24 dev ep1
$ sudo ip netns exec netns01 ip link set ep1 up
$
$ sudo ip netns exec netns02 ip addr add 10.0.0.11/24 dev ep2
$ sudo ip netns exec netns02 ip link set ep2 up
测试两个网络命名空间是否联通:
$ sudo ip netns exec netns01 ping 10.0.0.11
$ sudo ip netns exec netns02 ping 10.0.0.10
veth设备创建
内核相关代码位于文件linux-4.15/net/core/rtnetlink.c和linux-4.15/drivers/net/veth.c中。文件rtnetlink.c中的函数rtnl_newlink为处理ip link命令的入口函数,首先执行到。此函数完成IP命令(ip link set ep1)中设备ep1的创建工作(rtnl_create_link)。另外一个重要功能就是根据netlink消息中的IFLA_INFO_KIND字段(值为veth),找到相应的操作集(rtnl_link_ops_get), 调用其中的newlink回调函数继续进行处理。
static int rtnl_newlink(struct sk_buff *skb, struct nlmsghdr *nlh, struct netlink_ext_ack *extack)
{
if (tb[IFLA_IFNAME])
nla_strlcpy(ifname, tb[IFLA_IFNAME], IFNAMSIZ);
else
ifname[0] = '\0';
if (linkinfo[IFLA_INFO_KIND]) {
nla_strlcpy(kind, linkinfo[IFLA_INFO_KIND], sizeof(kind));
ops = rtnl_link_ops_get(kind);
}
(...)
dev = rtnl_create_link(link_net ? : dest_net, ifname, name_assign_type, ops, tb);
if (ops->newlink)
err = ops->newlink(link_net ? : net, dev, tb, data, extack);
}
在文件drivers/net/veth.c中,可见创建了veth的设备操作集rtnl_link_ops,并注册到了rtnl系统中(rtnl_link_register)。所以在以上rtnl_newlink函数中,可找到此操作集,在调用newlink函数时,调用此文件中的veth_newlink函数。
static struct rtnl_link_ops veth_link_ops = {
.kind = DRV_NAME, /* "veth" */
.priv_size = sizeof(struct veth_priv),
.setup = veth_setup,
.validate = veth_validate,
.newlink = veth_newlink,
.dellink = veth_dellink,
.policy = veth_policy,
.maxtype = VETH_INFO_MAX,
.get_link_net = veth_get_link_net,
};
static __init int veth_init(void)
{
return rtnl_link_register(&veth_link_ops);
}
函数veth_newlink继续完成veth设备的创建工作。创建IP命令中的ep2设备,并且注册到系统中。同时注册之前创建的ep1设备到系统中。
struct net_device *peer;
struct veth_priv *priv;
peer = rtnl_create_link(net, ifname, name_assign_type, &veth_link_ops, tbp);
err = register_netdevice(peer);
err = register_netdevice(dev);
将两个veth设备相互指向对方。
priv = netdev_priv(dev);
rcu_assign_pointer(priv->peer, peer);
priv = netdev_priv(peer);
rcu_assign_pointer(priv->peer, dev);
veth设备发送与接收
在veth设备初始化时,我们指定了设备的操作函数集veth_netdev_ops,此处我们仅关心ndo_start_xmit回调(veth_xmit)函数的实现。
static const struct net_device_ops veth_netdev_ops = {
.ndo_init = veth_dev_init,
.ndo_open = veth_open,
.ndo_stop = veth_close,
.ndo_start_xmit = veth_xmit,
.ndo_get_stats64 = veth_get_stats64,
};
static void veth_setup(struct net_device *dev)
{
dev->netdev_ops = &veth_netdev_ops;
}
在协议栈要发送数据报文时,最终到达设备层,将由veth_xmit函数处理。处理逻辑比较简单,首先从设备net_device中取出veth设备私有数据veth_priv,之后根据其成员peer找到veth对端的设备。调用dev_forward_skb函数将设备发送给对端。
static netdev_tx_t veth_xmit(struct sk_buff *skb, struct net_device *dev)
{
struct veth_priv *priv = netdev_priv(dev);
struct net_device *rcv;
rcv = rcu_dereference(priv->peer);
dev_forward_skb(rcv, skb) == NET_RX_SUCCESS);
}
dev_forward_skb函数实现的数据报文的具体发送和接收。
int dev_forward_skb(struct net_device *dev, struct sk_buff *skb)
{
return __dev_forward_skb(dev, skb) ?: netif_rx_internal(skb);
}
其中函数__dev_forward_skb可看做报文发送。其会在____dev_forward_skb函数中使用skb_scrub_packet抹除skb数据包中一些记录的信息,包括时间戳、路由缓存、Netfilter信息和IPVS信息等,使数据包看起来像是从外部环境中接收的一样。eth_type_trans除了设置protocol字段外,还将skb的成员dev由之前的ep1设备修改为ep2设备。
int __dev_forward_skb(struct net_device *dev, struct sk_buff *skb)
{
int ret = ____dev_forward_skb(dev, skb);
if (likely(!ret)) {
skb->protocol = eth_type_trans(skb, dev);
skb_postpull_rcsum(skb, eth_hdr(skb), ETH_HLEN);
}
}
netif_rx_internal函数重新将数据包接收到内核协议栈中。
内核版本
Linux-4.15