网桥设备作为一个虚拟设备,用于连接多个端口,可以构建一个局域网。与之相似的是vlan设备,在linux中,vlan设备是为了处理802.1q的添加和去除TAG的问题,这和传统交换机中vlan的功能—划分局域网,不太一样,在这里仅仅是处理了消息头,只是实现了隔离功能,并未实现交换功能,如果需要vlan内的数据转发,可以把vlan子接口挂接在网桥设备下。
网桥设备中添加的接口,在同一个二层域中。网桥在Linux中的实现如下:
Bridge-utils工具的实现
先查看一下Bridge-utils工具的工作原理,它是用于用户态配置网桥的工具,可以用于添加/删除网桥,往网桥添加/删除接口等操作。
在brctl.c的main()中,读取了参数列表,这里使用了getopt_long()函数,可以借鉴使用。之后进行br的初始化,主要就是创建一个socket接口,为之后IOCTL操作。然后调用command_lookup(),查找到参数命令,最后调用cmd->func(),执行命令。最后在内核中sock_ioctl()会收到,然后调用br_ioctl_hook(),从这里,就开始内核态的处理。
网桥设备的初始化
Br_init(),br_init是注册到内核进行初始化的,module_init(br_init),在br_init中,主要做的事如下:
注册stp(生成树协议),初始化转发表,注册network namespace(暂不知啥用),注册通知连,netlink的初始化,设置br_ioctl_hook。
网桥的建立和操作
从br_ioctl_hook调用开始,在br_init中注册的br_ioctl_hook为
br_ioctl_deviceless_stub,可以看到SIOCBRADDBR添加和删除网桥。添加网桥中alloc_netdev()分配了设备结构体,并进行了初始化设置br_dev_setup,在初始化设置的时候,就填充了netdev_ops->br_netdev_ops,这里就是对于设备的操作回调函数咯,重点关注br_dev_ioctl,在这里,会用添加和删除接口的操作。等再次添加从接口设备到网桥时,此时网桥设备已经存在,调用br_netdev_ops中的添加接口的回调函数,br_add_if中注册了接收数据的处理函数:netdev_rx_handler_register(dev, br_handle_frame, p);br_handle_frame就是处理函数咯。
网桥的数据流程
发送流程:
首先说一点是,网桥有自己的MAC地址,01:80:c2:00:00:0X,路由后发送函数dev_queue_xmit()发送数据出去,因为此时通过路由,skb->dev已经设置成为了br,所以,最后调用设备的发送回调ndo_start_xmit,在br初始化时,设置为br_dev_xmit,这样,最后的发送函数就是它咯。在发送中,确定目的地址是单播还是洪泛。
接收流程:
接收上,是从netif_receive_skb()中接收报文,因为只有在attach到网桥的从接口注册了rx_handle,即br_handle_frame,也就是只有这些接口才会把数据往网桥上送。接着调用br_handle_frame进行报文的处理,是consumed掉,还是转发等在这里完成。具体的代码就不分析咯。
注意事项:
网桥设备本身有自己的MAC,以及IP。发送和接收也正是通过路由后找到网桥接口的。
加入网桥的接口,其本身的IP和MAC地址都已经不再有效,路由并不会使用其中的地址,而是使用网桥的。
网桥实现最重要的两点:
1. MAC学习:学习MAC地址,起初,网桥是没有任何地址与端口的对应关系的,它发送数据,还是得想HUB一样,但是每发送一个数据,它都会关心数据包的来源MAC是从自己的哪个端口来的,由于学习,建立地址-端口的对照表(CAM表)。
2. 报文转发:每发送一个数据包,网桥都会提取其目的MAC地址,从自己的地址-端口对照表(CAM表)中查找由哪个端口把数据包发送出去。
在Linux里面使用网桥非常简单,仅需要做两件事情就可以配置了。其一是在编译内核里把CONFIG_BRIDGE或CONDIG_BRIDGE_MODULE编译选项打开;其二是安装brctl工具。第一步是使内核协议栈支持网桥,第二步是安装用户空间工具,通过一系列的ioctl调用来配置网桥。在我们开发过程中,常见的几条命令:
Brctl addbr br0 (建立一个网桥br0, 同时在Linux内核里面创建虚拟网卡br0)
Brctl addif br0 eth0
Brctl addif br0 ath0
Brctl addif br0 ath1 (分别为网桥br0添加接口eth0, ath0和ath1)
和网桥息息相关的几个结构体包括:网桥自身定义(net_bridge)、网桥端口(net_bridge_port)、网桥端口-MAC映射表项(net_bridge_fdb_entry)等。另外,网桥本身也是一个虚拟的网卡设备(net_device)。Net_device是一个庞大的结构体,我们在这里就不展现了。关于net_device详细介绍请参考《Linux设备驱动程序》网络驱动程序章节, net_device的详细介绍。下面我们介绍网桥、端口、端口-MAC映射表项的数据结构。
网桥定义:
struct net_bridge
{
//自旋锁
spinlock_t lock;
//网桥所有端口的链表,其中每个元素都是一个net_bridge_port结构。
struct list_head port_list;
//网桥会建立一个虚拟设备来进行管理,这个设备的MAC地址是动态指定的,通常就是桥组中一个物理端口的MAC地址
struct net_device dev;
//这个锁是用来保护下面的那个hash链表。
spinlock_t hash_lock;
//保存forwarding database的一个hash链表(这个也就是地址学习的东东,所以通过hash能 快速定位),这里每个元素都是一个net_bridge_fsb_entry结构
struct hlist_head hash[BR_HASH_SIZE];
//这个结构没有被使用
struct list_head age_list;
unsigned long feature_mask;
#ifdef CONFIG_BRIDGE_NETFILTER
struct rtable fake_rtable;
#endif
unsigned long flags;
#define BR_SET_MAC_ADDR 0x00000001
//stp相关的一些东西
bridge_id designated_root;
bridge_id bridge_id;
u32 root_path_cost;
unsigned long max_age;
unsigned long hello_time;
unsigned long forward_delay;
unsigned long bridge_max_age;
unsigned long ageing_time;
unsigned long bridge_hello_time;
unsigned long bridge_forward_delay;
u8 group_addr[ETH_ALEN];
u16 root_port;
//STP当前使用的协议
enum {
BR_NO_STP, / no spanning tree /
BR_KERNEL_STP, / old STP in kernel /
BR_USER_STP, / new RSTP in userspace */
} stp_enabled;
unsigned char topology_change;
unsigned char topology_change_detected;
//stp要用的一些定时器列表
struct timer_list hello_timer;
struct timer_list tcn_timer;
struct timer_list topology_change_timer;
struct timer_list gc_timer;
struct kobject *ifobj;
};
网桥端口数据结构体:
struct net_bridge_port
{
//当前端口所属的网桥设备
struct net_bridge *br;
//表示链接到这个端口的物理设备
struct net_device *dev;
//同一桥内的端口链表
struct list_head list;
//stp相关的一些参数
u8 priority;
u8 state;
u16 port_no;
unsigned char topology_change_ack;
unsigned char config_pending;
port_id port_id;
port_id designated_port;
bridge_id designated_root;
bridge_id designated_bridge;
u32 path_cost;
u32 designated_cost;
//端口定时器,也就是stp控制超时的一些定时器列表
struct timer_list forward_delay_timer;
struct timer_list hold_timer;
struct timer_list message_age_timer;
struct kobject kobj;
struct rcu_head rcu;
};
网桥端口-MAC映射表项:
struct net_bridge_fdb_entry
{
//用于CAM表连接的链表指针
struct hlist_node hlist;
//桥的端口(最主要的两个域就是这个域和下面的mac地址域)
struct net_bridge_port *dst;
//当使用RCU策略,才用到
struct rcu_head rcu;
//引用计数
atomic_t use_count;
unsigned long ageing_timer;
//mac地址
mac_addr addr;
//标明是否为本机MAC地址
unsigned char is_local;
//标明是否为静态地址
unsigned char is_static;
};
关于net_bridge、 net_bridge_port、net_bridge_fdb_entry它们之间的关系可以使用如下图的示意图表示:
网桥处理流程小结
进入桥的数据报文分为几个类型,桥对应的处理方法也不同:
1、 报文是本机发送给自己的,桥不处理,交给上层协议栈;
2、 接收报文的物理接口不是网桥接口,桥不处理,交给上层协议栈;
3、 进入网桥后,如果网桥的状态为Disable,则将包丢弃不处理;
4、 报文源地址无效(广播,多播,以及00:00:00:00:00:00),丢包;
5、 如果是STP的BPDU包,交给上层协议栈;
6、 如果是发给本机的报文,桥直接返回,交给上层协议栈,不转发;
7、 需要转发的报文分三种情况:
1) 广播或多播,则除接收端口外的所有端口都需要转发一份;
2) 单播并且在端口-MAC映射表中能找到端口映射的,只需要网映射端口转发一份即可;
3) 单播但找不到端口映射的,则除了接收端口外其余端口都需要转发。
网桥需要维护一个MAC地址-端口映射表,端口是指网桥自身提供的端口,而MAC地址是指与端口相连的另一端主机的MAC地址。当网桥收到一个报文时,先获取它的源MAC,更新数据库,然后读取该报文的目标MAC地址,查找该数据库,如果找到,根据找到条目的端口进行转发;否则会把数据包向除入口端口以外的所有端口转发。
生成树协议STP(Spanning Tree Protocol)的主要功能有两个:一是在利用生成树算法、在以太网络中,创建一个以某台交换机的某个端口为根的生成树,避免环路。二是在以太网络拓扑发生变化时,通过生成树协议达到收敛保护的目的。
6.1.1 名词解释
STP :生成树算法。
BPDU:STP的数据单元,在网桥局域网内传递信息。
TCN:拓扑改变通知BPDU。
根网桥:具有最小网桥ID的网桥被选作根网桥,网桥ID应为唯一的。
根端口:在指定网桥上面,到根网桥路径花费最小的端口为根端口,如果指定网桥上面有几个端口,到根网桥路径花费一样小,那么选择端口id 最小的端口为根端口。
指定网桥:局域网通过所连的网桥,接收和发送数据帧,如果局域网有且只有一个网桥相连,那么这个网桥必定是指定网桥,如果有多个网桥跟这个局域网相连,那么到根网桥路径花费最少的那个网桥为指定网桥,如果,有几个网桥到到根网桥路径花费一样,那么比较网桥id,id最小的被选作为指定网桥。
指定端口:指定网桥上面和局域网相连的端口叫做指定端口,如果指定网桥上面有几个端口,同时和局域网相连,那么选择端口id 最小的端口为所在局域网的指定端口。
根路径花费:当端口为根端口时候,通过这个端口的路径花费。 对于这个网桥来说,路径费用是到根网桥的费用之和。
指定花费:当端口为所在局域网的指定端口时候,即为根路径费用,当不为指定端口时候,是所在局域网指定端口到根网桥的费用。
6.1.2 网桥有五种状态
BR_STATE_DISABLED(0):禁用状态,不参与生成树,不转发任何数据帧。
BR_STATE_LISTENING(1): 监听状态,能够决定根,可以选择根端口、指定端口和非指定端口。在监昕状态的过程中,端口不能学 习任何接收帧的单播地址。
BR_STATE_LEARNING (2): 学习状态,端口能学习流入帧的MAC地址,不能转发帧。
BR_STATE_FORWARDING(3): 转发状态,接口能够转发帧。端口学习到接收帧的源 MAC地址,并可根据目标MAC地址进行恰当地转发。
BR_STATE_BLOCKING(4):阻塞状态,不参与帧转发、监听流人的BPDU,不能学习接收帧的任何MAC地址 。
6.1.3 STP关键点
运行生成树算法(STA)的网桥定期发送BPDU;选取唯一一个根网桥;在每个非根网桥选取唯一一个根端口;在每网段选取唯一一个标志端口。
(1) 选取唯一一个根网桥:BPDU中包含Bridge ID;Bridge ID(8B)=优先级(2B)+交换机MAC地址(6B);一些交换机的优先级默认为32768,可以修改;优先级值最小的成为根网桥;优先级值最小的成为根网桥;优先级值相同,MAC地址最小的成为根网桥;Bridge ID值最小的成为根网桥;根网桥缺省每2秒发送一次BPDU。
(2) 在每个非根网桥选取唯一一个根端口:根网桥上没有根端口;端口代价最小的成为根端口;端口代价相同,Port ID最小端口的成为端口;Port ID通常为端口的MAC地址;MAC地址最小的端口成为根端口。
(3) 在每网段选取唯一一个标志端口:端口代价最小的成为标识端口;根网桥端口到各网段的代价最小;通常只有根网桥端口成为标识端口;被选定为根端口和标识端口的进行转发状态;落选端口进入阻塞状态,只侦听BPDU。
(4) 阻塞端口在指定的时间间隔(缺省20秒)收不到BPDU时,会重新运行生成树算法进行选举;缺点:在运行生成树算法的过程中,网络处理阻断状态,所有端口都不进行转发。计算过程缺省为50秒。
6.1.4 STP工作过程
当网桥加电的时,网桥将认为它就是根网桥,并且将过渡到监听状态。一般情况下,当网桥认识到网络拓扑发生变更的时,将出现两种过渡状态:在拓扑变更的过程中,端口需要根据转发延迟计时器的数值而临时性地实施监听和学习状态。
当端口处于监听状态的时,它将利用发送和接收BPDU来确定活跃( active)的拓扑;当网络拓扑处于过渡期的时候,将不传递任何用户数据; 在监听状态的过程中,网桥将处理它所接收的BPDU;对于作为指定端口或根端口的端口,它们将在15秒(转发延迟的默认值)之启过渡到学习状态;对于不是指定端口或根端口的端口,它们将过渡返回到阻塞状态。
当端口处于学习状态的时,将利用从端口所学到的MAC地址来组建自己的MAC地址表;不能转发用户数据帧;在这个时刻,网桥不能传递任何用户数据。
当端口处于数据转发的时,学习状态能够降低所需扩散的数据帧的数量;如果某个端口在学习状态结束的时候仍然是指定端口或根端口,那么该端口就将过渡到转发状态;对于不是指定端口 或根端口的端口,它们将过渡返回到阻塞状态;在转发状态中,端口能够发送和接收用户数据;端口从阻塞状态过渡到转发状态的正常时间是30~50秒。
注:如果端口所连接的对象是主机,那么因为在这些链珞上的转发不会造成STP环路,所以这些端口也就不需要参与STP监听和学习的过程
网桥初始化所需要做的功能,主要有以下几项
1、CAM表的初始化
2、注册网桥相关的网络防火墙钩子函数
3、向通知链表中注册网桥的回调函数,处理网桥感兴趣的一些事件
4、设置网桥的ioctl,以便处理应用层添加网桥、删除网桥的需求
5、注册网桥处理回调函数,在接收封包处理函数netif_receive_skb中用来处理网桥设备
而网桥相关的初始化,主要是由函数br_init来完成的。
下面是br_init的代码
其完成的功能有:
1、调用stp_proto_register进行stp相关的初始化
2、调用br_fdb_init进行CAM表的初始化
3、调用register_pernet_subsys,为bridge模块注册网络命名空间。而br_net_ops的 init函数为NULL,所以调用register_pernet_subsys并没有在/proc目录下生成任何与bridge相关的目录,如果我们想在/proc下生成bridge相关的子目录或子文件,我们可以自己写init函数。
关于register_pernet_subsys函数的详细处理流程可看我以前的文档register_pernet_subsys相关学习
static structpernet_operations br_net_ops = {
.exit = br_net_exit,
};
4、调用函数br_netfilter_init,注册网络防火墙相关的钩子函数,主要是实现ebtables相关的功能
5、调用函数register_netdevice_notifier,向通知链中注册网桥感兴趣的信息。
6、调用函数br_netlink_init,进行netlink的初始化
7、调用brioctl_set,设置网桥相关的ioctl回调函数br_ioctl_deviceless_stub,
8、设置br_handle_frame_hook的回调函数