Veth虚拟以太网设备对

内核网络中VETH设备表示一对虚拟的互联接口,可使用以下IP命令创建:

$ sudo ip link add ep1 type veth peer name ep2

$ ip link list
5: ep2@ep1:  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:  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:  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:  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

 

你可能感兴趣的:(网络虚拟化)