网桥设备抽象:
对Linux而言,网桥是虚拟设备,要想传输或接收数据,需要将真实设备绑定到虚拟网桥上。
上图中,有几点需要注意:
LAN1和LAN2通过网桥连接在一起,子网都是一样的。
网桥连接到路由器上,这样LAN1,LAN2,LAN3可以通信。
从路由器角度看,在eth0上只有一个LAN。
因为Linux实现了桥接和路由,我们可以将这两种设备合并到一个Linux系统中。如图16-2a,网桥和路由器的网络连接是内核的事。
现在,内核需要处理下面两个问题:
在路由器层次上,虽然有三个接口(eth0,eth1,eth2),但内核只能看见两个子网。
内核只会在eth0和eth1间使用桥接,且认为这两个接口配置在相同的ip子网内。
当创建一个虚拟的网桥时,必须告诉内核它要绑定到哪些接口上。以上面的例子来说,我们需要建立一个虚拟网桥,假设名为br0,然后将eth0,eth1绑定到br0。由于eth0,eth1都是网桥接口,不需要配置ip。我们可以把路由器和网桥间的连接所具有的ip信息指派给网桥设备,结果如图16-2b。
如图16-4,可以给绑定到br0的eth0配置ip信息。这样,eth0可以接收要传给br0网桥的信息,也可以接收传给br0本身的信息。
默认情况下,被绑定的设备所接收的数据会分配给它绑定的网桥设备,但是,一个入口帧到底是要进行桥接还是路由(也就是交给br0还是eth0),可以通过ebtables配置(参见数据帧和BPDU一节)。
重要的数据结构:
mac_addr;//MAC地址
bridge_id;//网桥id
net_bridge_fdb_entry;//转发数据库的记录项
net_bridge_port;//网桥端口
net_bridge;//表示网桥。该结构会附加到net_device数据结构上。
br_config_bpdu;//入口配置BPDU的一些关键字段会复制到该结构中
上述数据结构都定义在net/bridge/br_private.h中,但br_config_bpdu定义在net/bridge/br_private_stp.h中
注意上图中,age_list已不再使用。
桥接程序的初始化:
桥接程序既可以集成在内核中,也可以编译成独立模块。初始化函数br_init和清理函数br_uninit定义在net/bridge/br.c。
初始化工作包括:
转发数据库初始化。
初始化函数指针br_ioctl_hook为处理ioctl命令的函数。
初始化函数指针br_handle_frame_hook,使其指向处理入口BPDU的函数。
若内核编译为支持Bridging-Firewalling时,br_netfilter_init就会在此时初始化Bridging-Firewalling。
清理函数所做的工作正好相反。
建立网桥设备和网桥端口函数:
网桥的建立和删除通过函数br_add_bridge和br_del_bridge进行。
端口的添加和删除通过函数br_add_if函数和br_del_if进行。
这四个函数定义在net/bridge/br_if.c
网桥设备建立:
网桥设备的建立和注册遵循第八章讲述的模型,区别在于,网桥需要在其私有区域内(即图16-6所展示的net_bridge结构)做一些额外的初始化。这个任务由new_bridge_dev函数完成:
分配一个net_device数据结构并初始化。
初始化私有数据结构net_bridge。
初始化网桥id,根路径开销(设为0),根端口(设为0,即没有根端口)。这样设置是因为网桥首次启动,会认为自己是根网桥。
设置老化时间,即转发数据中的记录项的有效时间。
用br_stp_timer_init初始化每个网桥定时器。
无论该网桥是否启动STP,都会对生成树的参数初始化。
网桥设备使用br_dev_setup函数对net_device结构中的一些通用字段初始化。
void br_dev_setup(struct net_device *dev)
{
memset(dev->dev_addr,0,ETH_ALEN);//网桥mac地址会被清除掉。因为这个地址将由br_stp_recaculate_bridge_id函数从其绑定的设备上配置的mac获取。基于通用的理由,set_mac_addr置为NULL。
dev->tx_queue_len = 0;//网桥设备默认没有实现队列机制,而是让被绑定的设备实现。管理员可以通过ipconfig或ip link来配置tx_queue_len。
dev->change_mtu = br_change_mtu;//网桥设备的MTU改变时,要确保新的mtu不会大于被绑定的设备中最小的mtu。这一点由br_change_mtu来确保。
dev->set_mac_addr = NULL;
dev->priv_flags = IFF_EBRIDGE;//必要时设置,使内核可以区分网桥设备和其他类型的设备。
dev->do_ioctl = br_dev_ioctl;//网桥上发出的ioctl命令由br_dev_ioctl函数处理。
dev->hard_start_xmit = br_dev_xmit;
dev->stop = br_dev_close;
dev->open = br_dev_open;
}
删除网桥设备:
删除网桥设备前要先将其关闭掉。删除时,br_del_bridge会调用del_br做如下工作:
删除网桥的全部端口。
用br_fdb_delete_by_port函数删除每个端口在转发数据库中所有相关的数据项。停止该端口的所有定时器,然后将 promiscurity计数器减一。
停止垃圾收集定时器br->gc_timer。
用br_sysfs_delbr函数删除/sys/class/net中的网桥目录
用unregister_netdevice函数在内核注销该设备。
给网桥添加端口:
在当前的桥接实现中,NIC和端口是一一对应的。(现在好像一个NIC可以虚拟出多个接口,都可以添加到网桥中??)
br_add_if函数可以给网桥添加端口,在添加之前会做一些检测,若下列任一条件满足则中断执行:
要添加的设备不是Etherner设备(或回环设备)。
若要添加端口的是网桥,则网桥端口必须指定到真实设备。(存疑??现在好像不是这样了)
该端口已经指派给一个设备了。(即dev->br_port不为NULL)。
上述检测通过后,就会:
给该网桥端口指派一个端口号
给该端口指派默认优先权
计算出端口id。
根据被绑定设的传输速率设置默认开销。传输速率的获取需要用到ethtool接口,且驱动程序支持ethtool接口。
指定BR_STATE_disABLEED的初始状态。
将网桥端口连接到被绑定的设备和网桥设备。
该网桥相关的NIC会进入有dev_set_promiscurity函数设置为混杂模式。
删除网桥端口的过程就是把建立端口所做的事撤销掉。
启动和关闭网桥设备:
br_dev_open启动网桥的步骤如下:
br_feathers_recomputed将网桥设备的基本特征初始化为其绑定的设备所支持的功能的最小子集。
用netif_start_queue函数启动设备进行数据传输(见第十一章)。
用br_stp_enable_bridge启动网桥设备。
当网桥设备启动时,绑定到该设备的端口也会跟着启动。
启动和关闭网桥端口:
网桥端口用br_stp_enable和br_stp_disable_port来启动和关闭。
要启用网桥端口,必须满足下列所有条件:
被绑定的相关设备已经用管理手段启动。
被绑定的相关设备有载波检测。(参见第八章链路状态变更侦测)
相关的网桥设备已用管理手段启动。
若上述条件满足,端口会在下面这些情况满足:
当被绑定的设备检测到载波状态时,桥接程序会收到NETDEV_CHANGE通知信息。
当被关掉的绑定设备重新启动时,桥接程序会收到NETDEV_UP通知信息。
注意,网桥上没有载波检测,因为是虚拟设备。
当一个端口启动时,br_port_state_selection会对其初始化为指定状态。若该网桥没有运行STP,把端口指定为BR_STATE_FORWARDING。
改变网桥端口状态:
网桥端口状态不是atcive就是inactive,相关的状态是BR_STATE_FORWARDING。BR_STATE_BLOCKINGBR_STATE_BLOCKING可以立即被设置,但BR_STATE_FORWARDING需要经历listen,learning两个中间态。第十五章有介绍。相关函数是br_make_blocking和br_make_forwarding。
大蓝图:
下图是桥接程序用于处理入口帧和出口帧(包括数据帧和BPDU)的一些重要函数。
注意,无论是否开启STP,桥接程序都使用同一组核心程序。
用于转发数据库的所有函数都在net/bridge/br_fdb.c中。
该数据库嵌入在net_bridge数据结构中,并被定义成一个hash表。
查询转发数据库的函数有两个:fdb_find和_ _br_fdb_get。
外部子系统想查询转发数据,可以使用br_fdb_get,该函数是一个_ _br_fdb_get的包裹函数。但是,不能直接调用br_fdb_get。而是通过br_fdb_get_hook函数调用,这个函数在br_init中初始化为指向br_fdb_get的指针。
br_add_if创建一个端口时,调用br_fdb_insert把被绑定设备的mac添加的转发数据库。
当端口相关联的本地设备改变其mac地址时,调用br_fdb_change_addr来改变转发数据库的记录项。
通过入口帧学习到的地址,调用br_fdb_update来添加到数据库,若该地址已经存在,需要更新相关的入口端口。
转发数据中的记录项由fdb_delete负责删除。该函数不会直接调用,而是通过br_fdb_cleanup和br_fdb_delete_by_port等这些包裹函数使用。
老化:
每个网桥实例都有一个垃圾收集定时器(gc_timer),定期扫描转发数据库以清理过期的数据项。
网桥实例初始化时,该定时器会在br_stp_timer_init中被初始化,之后br_stp_enable_bridge启动网桥,该定时器就会启动。
处理入口流量:
netif_receive_skb函数在把帧交给上层协议前,若内核支持桥接,会调用handle_bridge函数。当网桥端口上接收到帧,handle_bridge就会用br_handle_frame_hook(桥接模块初始化时,该hook指向br_handle_frame)处理该帧。
数据帧和BPDU:
只有处于BR_STATE_FORWARDING态的端口才可接受数据帧,而只要STP启动,任何启用的端口都可以接收BPDU。
关于图中ebtables的说明:
ebtables是一个架构,也能查看帧,提供一些Netfilter没有的额外功能。ebtables可以过滤和修改任何类型的帧。
就图中而言,涉及到了ebtables的两个功能:①定义规则,哪些流量走桥接,哪些流量走路由,也就是说,一块NIC既是网桥的一个端口,又分离L3地址。②可以修改mac地址,所以ebtables完成任务后检查目的mac地址。
入口数据帧的处理是br_handle_frame_finish完成的。
关于图16-13中入口帧传送至本地的原理见下图:
当该帧由NIC设备驱动程序收到时,skb->dev就被初始化为指向真实设备。然后。该帧经过网络堆栈到达br_pass_frame_up函数,再经过一个Netfilter钩子,然后调用br_pass_frame_up_finish。此时,skb->dev的值会被入口端口所属的网桥设备替换掉,并再次调用netif_receive_skb。这次,handle_bridge看到该设备不是被绑定的设备(根据br_port为NULL判断)。因此会把该帧交给正确的协议处理函数。
网桥设备上的传输:
网桥设备抽象层要求把一台网桥上的传输转变成一个或所有网桥端口上的传输。图16-11给出了相关的主要函数。网桥驱动(虚拟设备驱动)实现hard_start_xmit的函数是br_dev_xmit。
生成树协议STP:
第十五章了讲了STP如何运行,下面讨论如何处理入口BPDU,如何发出BPDU,如何处理定时器。
br_become_root_bridge:把非根网桥变成根网桥。
br_is_root_bridge:判断网桥是不是根网桥。
br_should_become_designated_port:如果输入端口应获得指定角色,返回1.
br_designated_port_selection:遍历所有网桥端口,把应该成为指定角色的端口变成指定端口。
br_become_designated_port:把指定角色指派给一个端口。
br_is_designated_port:若输入端口是指定端口,返回1
br_is_designated_port_for_some_port:若给定网桥至少由一个端口是指定角色,返回1
br_supersedes_port_info:给定一个端口和该端口接收的输入配置BPDU,若这个BPDU比该端口所知的优先权向量更高级,返回1br_should_become_root_port:给定一个端口和当前根端口,比较优先级,若给定端口优先级更高,则返回1.
br_root_selection:给定一台网桥,选出根端口
br_configuration_update:给定一台网桥,测定根端口和指定端口,并返回它们的信息。
br_port_state_selection:给定一个网桥,该函数可以为每个端口选出正确的端口状态。
br_topology_change_detection:探测拓扑变化。
br_topology_change_ackNowledge:发出一个设有TCA标识的BPDN作为对TCN的应该。
br_topology_change_ackNowledged:停止定时器
br_record_config_information:给定一个网桥端口和一个输入配置BPDU,该函数会把BPDU的优先权向量记录再该端口的net_bridge_port数据结构中,然后重启消息生存期定时器。
br_record_config_timeout_values:该函数记录BPDU定时器配置信息。
br_get_port:给定一个网桥设备和一个端口号,该函数返回相关的net_bridge_port结构。
网桥ID和端口ID:
网桥id的mac地址部分和端口id的端口号部分由内核按下面的方式初始化:
网桥mac地址:从被绑定的设备上所配置的MAC地址中,选出最低的MAC地址。
端口号:从1到BR_MAX_PORTS的范围中第一个尚未被使用的号会被选中。
处理入口BPDU:
入口BPDU帧会传给br_stp_handle_bpdu。
传输BPDU:
配置更新和选择根网桥:
负责配置更新的函数是br_configuration_update,该函数会在下面的一些情况被调用:
调用时机 |
|
br_received_config_bpdu |
|
br_message_age_timer_expired |
端口知道的信息已经过期,该变化也能导致拓扑变化,从而改变根网桥 |
br_stp_disable_port |
|
br_stp_change_bridge_id |
网桥id的mac地址部分改变了,这种改变可能会改变根网桥 |
br_stp_set_bridge_priority |
网桥id的优先权部分已经被改变,这种改变可能会改变根网桥 |
br_stp_set_path_cost |
端口路径开销改变了 |
每次调用br_configuration_update后,总是接着调用br_port_state_selection。
定时器:
端口和网桥定时器可分别用br_stp_port_timer_init和br_stp_imer_init初始化。
STP网桥定时器处理函数:
定时器 |
处理函数 |
Hello |
br_hello_timer_expired |
Topology Change Notification |
br_tcn_timer_expired |
Topology Change |
br_topology_change_timer_expired |
STP端口定时器处理函数:
定时器 |
处理函数 |
Max Age | br_message_age_timer_expired |
Forward Delay |
br_forward_delay_timer_expired |
Hold |
br_hold_timer_expired |
处理拓扑变化:
由上一章的拓扑变化一节知道哪些事件会导致拓扑变化,这些事件由下列函数进行探测:
br_make_blocking:当STP决定阻塞一个转发端口时会调用该函数。
br_forward_delay_timer_expired:处于BR_STATE_LEARNING状态的某个端口要转换到BR_STATE_FORDWARDING态时会调用该函数。
br_become_root_bridge:当非根网桥变成根网桥时调用。
br_received_tcn:收到TCN BPDU时调用。
netdevice 通知链:
由于虚拟网桥设备是定义在被绑定的真实设备之上的,所以当任一绑定设备状态改变时,网桥会收到通知(因为桥接程序初始化时会用netdevice通知链向内核注册br_device_event回调函数)