背景信息
因为工作性质的变动,许久不更新文章了。最近在做裸金属管理的项目,因为资源限制,缺少物理机,而且物理机启动太慢,不利于开发,所以选择用虚拟机来模拟物理机。在搭建测试环境的过程中,遇到了一些问题,在解决这些问题的过程中,有很多网络方面的收货,经过一些思考和阅读源码解开了心中的很多疑惑。比如下面这些问题,你是否想的清楚呢?
- 将物理网卡添加到linx bridge中,linux bridge是如何接管物理网卡的呢?
- 将物理网卡添加到ovs桥中,ovs桥是如何接管物理网卡的呢?
- 为什么物理网卡不能同时加入到linux bridge和ovs桥呢?
- 在linux系统中,为什么不能创建多个vlan id相同的vlan子接口呢?
- 将网卡加入到bond中,又在bond上创建了vlan子接口,流量又是如何转发的呢?
资源部署架构
这是在一台物理机部署的很多虚拟机,其中虚拟机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,通过这些模拟方案,可以实现用虚拟机来模拟物理机,搭建裸金属管理的开发环境。
网络逻辑关系
现在把目光聚集到网络层面,看一下各个网络设备之间的关系。
从类别上分,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虚拟网卡和各种桥接的原理,但是并没有太多的去关注细节。希望本文的解读,可以帮助各位更好的理解网络的基本原理,让心中悬而未决的疑问更加的清晰,最后祝各位看官看的开心、生活愉快。创作不易,假如文章对你有帮助,记得点赞支持。如果有其他问题,可以直接评论,或者加微信进行更深入的交流。