网络实践- vlan与桥接原理

背景信息

因为工作性质的变动,许久不更新文章了。最近在做裸金属管理的项目,因为资源限制,缺少物理机,而且物理机启动太慢,不利于开发,所以选择用虚拟机来模拟物理机。在搭建测试环境的过程中,遇到了一些问题,在解决这些问题的过程中,有很多网络方面的收货,经过一些思考和阅读源码解开了心中的很多疑惑。比如下面这些问题,你是否想的清楚呢?

  • 将物理网卡添加到linx bridge中,linux bridge是如何接管物理网卡的呢?
  • 将物理网卡添加到ovs桥中,ovs桥是如何接管物理网卡的呢?
  • 为什么物理网卡不能同时加入到linux bridge和ovs桥呢?
  • 在linux系统中,为什么不能创建多个vlan id相同的vlan子接口呢?
  • 将网卡加入到bond中,又在bond上创建了vlan子接口,流量又是如何转发的呢?

资源部署架构

部署关系图.png

这是在一台物理机部署的很多虚拟机,其中虚拟机1、虚拟机2和裸金属服务虚机是直接在物理机上使用libvirt启动的虚拟机,虚拟机3、虚拟机4、虚拟机5是通过kubevirt启动的虚机,kubevirt使用的网络方案是kube-ovn的underlay的方式,即ovs桥接管bond0网卡,虚拟机通过ovs桥,直接连到物理网络上。

首先需要介绍一些前置的知识,才能更好的理解这些内容。

  • 裸金属管理:裸金属管理的最终目的是 能够像虚拟机一样管理虚拟机,包括裸金属服务器的启停、监控、自动安装系统、修改密码、自动扩容根分区等等。裸金属管理的方案选择的是使用开源的ironic,又因为ironic依赖于openstack环境,所以选择使用bifrost,bifrost是用于自动部署ironic服务,并使得ironic不依赖于openstack环境。
  • kubevirt : kubevirt是基于kubernetes的轻量虚拟化管理平台,使得用户能够在kubernetes环境中同时使用容器和虚拟机。
  • ovs :全称是openvswitch,简单来说,就是一个虚拟的交换机,通过sdn接管ovs,可以实现很强大的网络功能。
  • 虚拟机模拟物理机:物理机的自动装机包括dhcp、ipmi控制从网络启动、开关机等,可用使用vbmcd来模拟ipmi,通过这些模拟方案,可以实现用虚拟机来模拟物理机,搭建裸金属管理的开发环境。

网络逻辑关系

现在把目光聚集到网络层面,看一下各个网络设备之间的关系。


网络设备关系图.png

从类别上分,enp1和enp2属于物理网卡,vlan子接口和bond0属于虚拟网络设备,linux bridge和ovs桥属于虚拟交换机。

从主从关系讲,vlan子接口是linux bridge的从设备,bond0是ovs桥的从设备,enp1和enp2是bond0的从设备。

有几个特殊的点,需要提一下,vlan子接口并不是bond0的从设备,这个后面会说明我的分析,bond0的角色既是主设备,又是从设备。

网络协议栈

网络协议栈十分复杂,这里并不会详细分析网络协议栈的实现,只是简要介绍一个网络协议栈的处理流程,方便对于后文的理解。

为了简便和扩展,网络进行了分层,就是大家熟悉的二层、三层、四层、七层。其中,二、三、四是在内核态处理的,七层是在用户态处理的。在内核态处理的这部分就是网络协议栈的核心内容。

已收包为例,物理网卡收到包后,进行拆包,解析二层的类型,调用对应的二层的处理函数,二层收到包后,进行拆包,解析三层包的类型后,调用对应的三层处理去处理,三层收到包后,进行拆包,解析四层包的类型后,调用对应的四层函数去处理。每一层都有对应的入口函数,如ip_rcv是三层的入口函数,tcp_rcv是四层tcp的入口函数。

网络协议栈也是可以重入的,如各种封包,已vxlan发送数据包为例,当数据包分别经过四层、三层、二层的处理后,到达vxlan驱动,vlan驱动处理时,又调用相应的函数重新回到了四层处理,接下来又通过三层、二层的处理,最后通过物理网卡发送出去。

vlan子接口

vlan网卡是一个虚拟的网络设备,下面简要介绍vlan子接口的创建、vlan虚拟网卡的收发包流程、以及为什么不能创建相同vlanid的子接口。

创建vlan虚机网卡

ip link add link enp1 name enp1.100 type vlan id 100

上面就是创建vlan子接口的命令,其中 enp1 是物理网卡的名称,enp1.100是vlan子接口的名称(名称可以随便起,enp1.100只是可以简明的表示出来逻辑关系),id 100表示封vlan包时使用的id是100。之后当要发送的数据包达到enp1.100时,vlan驱动会将数据包加上vlan tag,之后转交给主设备enp1物理网卡进行发送。

vlan驱动收发包

  • vlan驱动收包
    __netif_receive_skb_core函数是二层处理中一个很重要的函数,下面简要分析一下这个函数(代码只展示最关键部分)。
static int __netif_receive_skb_core(struct sk_buff *skb, bool pfmemalloc,
                    struct packet_type **ppt_prev)
{
    if (skb_vlan_tag_present(skb)) {
        vlan_do_receive(&skb);
    }

    rx_handler = rcu_dereference(skb->dev->rx_handler);
    rx_handler(&skb);
    deliver_skb(skb, pt_prev, orig_dev)
}

其中,skb_vlan_tag_present用于判断数据包有没有vlan,如果有的话,调用vlan_do_receive进行处理,rx_handler就是判断一个设备是否是从设备的关键因素,如将一个物理网卡加入到linux bridge中,linux bridge的驱动就会将物理网卡的rx_handler进行赋值。本来这里处理完后,应该向上传递数据包了,但是因为rx_handler不为空,所以会调用linux bridge的相关函数被处理,简单点说,就是数据包被linux bridge截胡了。

  • vlan驱动发包
    vlan驱动的发包函数是vlan_dev_hard_start_xmit,下面简要分析这个函数,代码只展示最关键的流程。
static netdev_tx_t vlan_dev_hard_start_xmit(struct sk_buff *skb,
                        struct net_device *dev)
{
    struct vlan_dev_priv vlan = vlan_dev_priv(dev);

    skb->dev = vlan->real_dev;

    vlan_tci = vlan->vlan_id;
    vlan_tci |= vlan_dev_get_egress_qos_mask(dev, skb->priority);

    __vlan_hwaccel_put_tag(skb, vlan->vlan_proto, vlan_tci);

    ret = dev_queue_xmit(skb);

       return ret;

通过vlan_dev_priv获取到vlan设备的私有数据,将skb->dev赋值为vlan子接口的主设备,再通过vlan获取到vlan id,调用__vlan_hwaccel_put_tag进行打vlan的tag,最后通过dev_queue_xmit将数据包转给主设备进行发送。

创建相同的vlan id的子网卡

在不同的系统上,呈现的错误并不一样。

  • ubuntu 22.04 上
root@VM-0-5-ubuntu:~# ip -V
ip utility, iproute2-5.15.0, libbpf 0.5.0

root@VM-0-5-ubuntu:~# ip link add link eth0 name vlan98 type vlan id 98
Error: 8021q: VLAN device already exists.
  • centos7.6
[root@10 ~]# ip -V
ip utility, iproute2-ss170501

[root@10 ~]# ip link add link bond0 name vlan98 type vlan id 98
RTNETLINK answers: File exists

ubuntu22.04 因为ip命令的版本比较高,很好的将错误的原因展示了出来,centos7.6中ip只是提示已存在。这个错误来自于内核,可以看这个函数vlan_check_real_dev:

int vlan_check_real_dev(struct net_device *real_dev,
            __be16 protocol, u16 vlan_id,
            struct netlink_ext_ack *extack)
{
    if (vlan_find_dev(real_dev, protocol, vlan_id) != NULL) {
        NL_SET_ERR_MSG_MOD(extack, "VLAN device already exists");
        return -EEXIST;
    }
}

从用户的需求角度看,确实是有创建多个相同vlan id的vlan子接口的需求,但是内核为什么不支持呢? 这其实跟内核的实现有关,内核完全是可以支持的,但是本着机制和策略分离的原则,vlan驱动只是做vlan隔离,用户需要多个相同vlan id的子接口,可以通过bridge进行将vlan子接口与用户的虚拟网卡进行桥接即可(纯属猜测)。

桥接原理

如上文分析,将物理网卡进行桥接,相应的桥接驱动都会改变物理网卡的rx_handler函数,进而在__netif_receive_skb_core函数处理时拦截物理网卡的数据包。bond、ovs、bridge都是相同的原理。

  • ovs
ovs-vsctl add-port br-int port0

这条命令是将port0网卡加入到br-int的ovs桥上时,对应的ovs驱动会做处理,在ovs_netdev_link函数会调用netdev_rx_handler_register将物理网卡的rx_handler赋值为netdev_frame_hook

struct vport *ovs_netdev_link(struct vport *vport, const char *name)
{
    vport->dev = dev_get_by_name(ovs_dp_get_net(vport->dp), name);
    
    err = netdev_rx_handler_register(vport->dev, netdev_frame_hook,
                     vport);

    return vport;
  • linux bridge
brctl add-if br0 port0

这条命令是将网卡port0加入到linux bridge的br0上。对应的linux-bridge驱动会做处理。

int br_add_if(struct net_bridge *br, struct net_device *dev,
          struct netlink_ext_ack *extack)
{
    err = netdev_rx_handler_register(dev, br_handle_frame, p);
}
  • 将物理网卡同时接入linux bridge和ovs桥
    调用桥接时,会调用netdev_is_rx_handler_busy进行检测,这个函数的注释也写的很清楚,如果网卡的rx_handler已经有值了,这里就会返回true。从逻辑的角度也能想清楚,如果对于一个函数变量重复赋值,必然导致变量值被覆盖,这显然也是不合理的。
/**
 *  netdev_is_rx_handler_busy - check if receive handler is registered
 *  @dev: device to check
 *
 *  Check if a receive handler is already registered for a given device.
 *  Return true if there one.
 *
 *  The caller must hold the rtnl_mutex.
 */
bool netdev_is_rx_handler_busy(struct net_device *dev)
{
    ASSERT_RTNL();
    return dev && rtnl_dereference(dev->rx_handler);
}

流量走向总结

从最上面的部署关系图看到,bond0是ovs桥的从设备,vlan虚拟网卡的上层设备是bond0,如果一个数据包是带vlan tag的,在内核中是如何处理的呢,从__netif_receive_skb_core的处理也可以看到,是先调用vlan_do_recieve进行处理,再看网卡的rx_handler是否有值。

总结

本文简要的介绍了vlan虚拟网卡和各种桥接的原理,但是并没有太多的去关注细节。希望本文的解读,可以帮助各位更好的理解网络的基本原理,让心中悬而未决的疑问更加的清晰,最后祝各位看官看的开心、生活愉快。创作不易,假如文章对你有帮助,记得点赞支持。如果有其他问题,可以直接评论,或者加微信进行更深入的交流。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容