Bridge in Linux Kernel——STP

来源:互联网 发布:淘宝兼职工作 编辑:程序博客网 时间:2024/05/29 14:24

   这篇博客是Bridge in Linux Kernel系列的第三篇,主要介绍Bridge使用的STP协议的原理,包括STP协议解决的问题,STP协议包的格式及主要功能、Bridge的STP的部分实现以及“地址学习”的原理和实现。STP协议很大程度与IEEE 802.1D标准有关,本篇博客是在Undersatand Linux Network Internal的Chapter 15的基础上写成的。

    本篇博客耗时56小时。

1、STP解决的问题

    网桥的本质工作是在链路层转发(IEEE 802.1D叫中继,Relay,我们权且将转发和中继一词等同,转发有时会改变帧)和过滤数据帧,那么就会有一些基本问题有待解决:

  • 如何在数据链路层转发,如何通过MAC层地址知道该向哪个LAN转发?
  • 如果一个网桥连接多个LAN,而转发的数据帧并非直接相连的LAN所有,那么应该选择哪条路径转发才能使得经过的网桥数量最少(路径最短)?
  • 如果几个网桥桥接了几个LAN,形成环路,如何避免数据帧转发时形成环路?
  • 当网络拓扑结构发生变化时,意味着从某个网桥转发的数据帧的最小路径可能发生变化,如何去维护拓扑结构的变化?

    STP协议用于解决这些问题。STP,Spanning Tree Protocol,IEEE 802.1D定义,目前已经被RSTP、MSTP协议所更新、兼容,但是Linux系统仅仅实现了STP协议。STP协议定义了一种数据帧结构,用于网桥之间的通信,通过不同网桥之间发送这种数据帧,相互通知、应打,起到解决上述问题的效果,这种数据帧名字时BPDU,Bridge Protocol Data Unit(BPDU不仅仅为STP协议服务,还为网桥的其他协议如RSTP、MSTP协议服务)

    对于网桥的主要功能——链路层数据帧转发,不是在转发时临时在网络里查询应该向哪条路径转发,而是提前将网络拓扑探测好,并记录没条路径的权重、相关网桥、端口等信息,将这些信息存储在一个转发数据库中(类似于路由表),在转发时通过查阅转发数据库来确定转发路径,这一转发数据库叫做FDB,Forward Database。

    STP协议,最小生成树协议,用于找到一个图中的最小生成树,关于找最小生成树算法,STP协议使用了这些算法,关于算法,可以参考最小生成树算法;802.1D中的STP协议使用的最小生成树算法并非在本地运行的,而是在网络上运行的。

 

2、LLC和BPDU

    首先STP协议运行在L2层,也即ISO/OSI的链路层,Data Link Layer,而链路层本身又分为两个子层,介质访问控制子层(MAC)和逻辑链路控制子层(LLC),STP工作于LLC层之上,STP使用的结构叫做BPDU,如下图所示:

llc-bpdu

    STP工作于LLC层之上,这意味着如果要使用STP协议,则sk_buff里要具备MAC头和LLC头,然后才是STP的帧格式BPDU,STP帧的手发都要经过LLC层。下面看一下BPDU的格式,根据BDPU的类型,可以分为两类,分别是Configuration BPDU和TCN BPDU(Topology Change Notification),Configration BPDU用于通知声明和通知root bridge,而TCN BPDU用于通知网络拓扑变化:

bpdu

    Protocol ID为0,代表STP协议,BPDU还支持RSTP和MSTP,通过Protocol Version区别:

    BPDU Type分为两种,一种为Configuration BPDU,一种为TCN BPDU:


#define BPDU_TYPE_CONFIG 0#define BPDU_TYPE_TCN 0x80

接下来是一个标志位,但只有两种情况被使用TC(Topology Change)和TCA(Topology Change Acknowledgment)。

    然后是Priority vector,优先级向量,用于描述网桥、端口的优先级,比较重要,用于确定根网桥、根端口、指定端口等。包含四个部分,下文中会详细解释。

    Message Age,有点类似与IP协议的TTL,用于描述当前Configuration BPDU已经经过的跳数(hop,当然也可以是实际的时间),经过一个Port就增加该部分,但Linux系统没有使用该方法,而是计算从接收该BPDU到传输BPDU之间所花费的时间,这也是IEEE 802.1D原本的定义。

    Max Age,最大生存周期,如果Message Age超过了Max Age,drop即可。

    Hello Time,Hello时间,根网桥周期性的发送Configuration BPDU,告知其他Bridge,Hello Time用于描述该周期,默认2s。

    Forward Delay,转发时间,默认15s,端口从LISTENING状态转到LEARNING状态,经过Forward Delay Time,从LEARNING状态转到FORWARDING状态,经过Forward Delay Time。

    Linux内核中定一个Configuration BPDU的结构体(TCN的不使用结构体):


struct br_config_bpdu{unsigned int   topology_change : 1;unsigned int   topology_change_ack : 1;bridge_id   root;int    root_path_cost;bridge_id   bridge_id;port_id     port_id;int    message_age;int    max_age;int    hello_time;int    forward_delay;};


 
3、STP的几个基本概念

    为了方便描述和管理网桥,引入几个基本概念:根网桥(Root Bridge)、根端口(Root Port)、指定桥(Designated Bridge)、指定端口(Designated Port)。

    根网桥,指连通不同的LAN的网桥中,网桥ID最小的那个,也即抽象为最小生成树后的树根。网桥ID的描述方法如下:


struct bridge_id{unsigned char  prio[2];unsigned char  addr[6];};

addr为MAC地质,prio为优先级。prio为人为设置,重点在区分根网桥与非根网桥,如果Adminastrator想让一个网桥成为根网桥,则将其优先级设高。

    根端口,一个网桥的所有端口中,到达根网桥的cost最少的那个端口被称为根端口。根端口的在生成树中相当于任意一个节点通往根结点最短的边。这里的cost指从某一网桥的某一个端口到达根网桥任意端口所耗费的时间,可以由管理员手动指定cost。网桥通过Confiuration BPDU的cost字段来判断到达该port的cost。cost是Port的一个属性,cost越低,越应该选择该Port转发数据。一个100Mb的以太网的cost应该比1000Mb的以太网的cost高。Linux内核中关于cost的计算,使用port_cost()函数:


static int port_cost(struct net_device *dev){struct ethtool_cmd ecmd; if(!__ethtool_get_settings(dev, &ecmd)) {switch(ethtool_cmd_speed(&ecmd)) {case SPEED_10000:return 2;case SPEED_1000:return 4;case SPEED_100:return 19;case SPEED_10:return 100;}  }   /* Old silly heuristics based on name */if(!strncmp(dev->name,"lec", 3)) return 7; if(!strncmp(dev->name,"plip", 4)) return 2500; return 100;/* assume old 10Mbps */}

cost是网卡的属性,比较重要,Bridge和LAN很可能是下面这种方式连接的:cost

    转发Configuration BPDU时,必须将自己的cost加上;假设到达Bridge1的cost为orig cost,那么从Bridge1的p1端口出来的BPDU,经过LAN1到达Bridge3的p3端口时,cost为10+orig cost,而Bridge3再通过其p1端口转发时,cost将达到orig cost+12,而不是orig cost+22,也即发送Configuration BPDU时增加cost,接收时,并不增加cost。这样是否合理?感觉cost应该是path的属性,而不是网卡的属性。

    指定桥,是一个从LAN的角度看的概念,如果一个LAN连接了几个网桥(通过几个Port),则在连接这个LAN的所有Bridge中,到达根网桥的cost最小的Port被称为LAN的指定端口(选择指定端口时,实际上看的时Priority vector,不仅仅看Root Path cost),指定端口所在的桥成为LAN的指定桥。指定端口的意义在于通往目标LAN的cost最短。

    对于任意一个网桥的任意端口,都属于三种角色(role)中的任意一种,根端口、指定端口、普通端口(不是根端口、也不是指定端口,官方文档上并没有这样的定义,只是说起来比较方百年)。任意一个网桥都只有一个根端口,任意一个LAN也只有一个指定端口,根网桥没有根端口,所有端口都是指定端口(虽然根网桥的端口有可能直接连接其他网桥,而不是LAN,但是也将这种情况说成指定端口),一个网桥有可能有几个指定端口。指定端口通常用来转发根网桥的Configuration BPDU,而根端口通常用于向根网桥发送TCN BPDU。

    如下图所示,首先这并不是一颗实际的STP协议生成树,而是便于理解根网桥、根端口、指定端口、指定网桥等概念而画的一张示意图,途中方框节点为网桥,圆形节点为LAN,边代表网桥的Port(实际生成树中不考虑LAN节点,只有网桥节点)。很显然,途中紫色节点2号ID最小,为根节点,也即根网桥;图中所有圆形节点,也即LAN节点有且仅有一条实线与网桥节点相连,相连的网桥节点就是该LAN节点的指定网桥;任意一个网桥节点,图中标有红色线条的是其根端口。图中剩余的线条均为虚线,为实际上物理相连、但生成树中不使用的端口,即不是根端口,也不是指定端口,就是所谓的普通端口。这些端口在网络稳定后,通常处于BLOCKING状态,只能接收STP协议包,但并不能发送STP协议包,也不能学习、转发数据:

tree

    根据行为方式的不同,将端口划分为五种状态(Port stat):


#define BR_STATE_DISABLED 0#define BR_STATE_LISTENING 1#define BR_STATE_LEARNING 2#define BR_STATE_FORWARDING 3#define BR_STATE_BLOCKING 4

五种端口状态的功能区别:

  • DISABLE:什么功能都没有,只有一个逻辑设备。
  • LISTENING:可以接收和发送网络传输的BPDU,包括Configureation BPDU和TCN BPDU,但不能进行数据帧的转发、不能学习。
  • LEARNING:可以接收和发送BPDU,可以学习,但是不能进行数据帧的转发。
  • FORWARDING:可以接收和发送BPDU、可以学习、可以进行数据帧的转发。
  • BLOCKING:只能接收BPDU,不能发送BPDU,不能学习,不能转发数据帧。

    这五种状态转换过程如下:

5state

    随着STP协议的运行或者管理员的操作,Port在五种状态之间转换。Listening经过一段时间进入Learning状态,Learning状态经过一段时间进入Forwarding状态,这两段时间叫做Forward Delay Time,通过定时器Forward Delay Timer来计时。

 

4、选择Root Bridge、Root Port、Designated Port

    既然定义了Root Bridge、Root Port、Designated Port,STP协议必须能够找出他们,首先是根网桥。

    任意一个网桥启动后,由于不知道网络拓扑结构,所以它认为自己是根网桥,自己所有端口都认为是Designated Port,cost均为0,对外广播Configuration BPDU数据包,将BPDU的Priority vector填充后发送(也填充其他字段,含义不在这里描述了)。其中Priority vector比较重要,由4个字段组成,分别是Root Bridge ID、Root Path cost、Bridge ID、Port ID,都是字面意思,从哪个Port发送,填充哪个Port的ID。Priority vector不但用于确定根网桥,还用于确定根端口和指定端口。

    如果网络中没有其他网桥,则没有回应,该网桥成为根桥,并且以Hello time(默认2s)周期性的向网络广播根桥的Configuration BPDU,以通知其他网桥根桥的配置。

    如果网络中已经存在根桥,则网络中其他网桥存储了根网桥的信息,当其他网桥(或根网桥)收到新启动网桥的自认为根桥的Configuration BPDU时,将其Priority vector中的Root Bridge ID与现有根桥的Bridge ID相比较,如果其Root Bridge ID项小于现有根网桥的Bridge ID,则认为新启动网桥为根网桥,并更新本地的根桥配置信息,并转发其Configuration BPDU;如果其Root Bridge ID项不小于现有根网桥的Bridge ID,则将自己知道的根桥信息,生成一个Configuration BPDU,从接收端口发送出去,而后新启动网桥接收到该Configuration BPDU,则获知Root Bridge ID,并更新本地根网桥的配置。

    根网桥的发现,用一句话描述,“启动自认根网桥,BPDU来广播,ID小者真根桥,BPDU来纠错”。

    当确定根网桥后,根端口的选择就比较简单,当任意网桥的任意端口收到Configuration  BPDU时,比较此前的根端口的Priority vector相比较,小的那个就是根端口。

    指定桥,对于任意一个Port,并定能够接收到所连接相同LAN的所有Port的Configuration BPDU,那么就可以通过比较这些Priority vector和本地桥其他Port接收Configuration BPDU的Priority vector。,然后再加上相应的cost,就得到了自己的到达该LAN的cost,通过比较这些Priority vector(cost一致的时候比较Bridge ID和Port ID),就选择出最小的那个作为指定端口。

    OK,等根桥、所有网桥的根端口、指定桥、指定端口都已经确定后,整个网络拓扑结构中没有环路的时候,我们说这个网络是稳定的。对于新增加的网桥,已经存在的非根桥网桥,只在其指定桥端口转发Configuration BPDU,换言之,只有指定桥处理新增加网桥的Configuration BPDU。

5、一次稳定状态的过程

    首先,假设已经由Bridge1、Bridge2、Bridge4连接了五个局域网LAN1、LAN2、LAN3、LAN4、LAN5,并且Bridge1的ID为BR1,Bridge2的ID为BR2,Bridge4的ID为BR4,且BR1<BR2<BR4,则Bridge1为根网桥,其上段都p1、p2、p3均为指定端口,Bridge2的p3为根端口,p1和p2为指定端口,Bridge4的p2为根端口、p1为指定端口,Bridge1周期性的(Hello Time)的广播Configuration BPDU,Bridge2、Bridge3则从其指定端口转发这些Configuration BPDU,各端口port cost如图所示:

stp1

    其次,假设此时要增加一个Bridge3,用于连接LAN3和LAN5,Bridge3上两个端口,p1连接LAN5,p2连接LAN3,Bridge3的ID为BR3,BR2<BR3<BR4,Bridge3的p1的cost值为20,p2的cost值为30,则Bridge3刚启动,认为自己是根桥,通过自己的p1端口和p2端口向外发送Configuration BPDU(刚启动的Bridge将自己的Port设置为LISTENING状态,以发送BPDU):

stp2

    再次,当Bridge4和p1端口和Bridge2的p1端口接收到Bridge3的自认为根网桥的Configuration BPDU时,通过与此前存储的Bridge1的Priority vector相比较,得到BR3>BR1的结论,因此仍然认为Bridge1为根网桥,但是Bridge2和Bridge4转发Bridge1的Configuration BPDU给Bridge3,通知其真正的根网桥是Bridge1(注意,Bridge3的BPDU不一定非得到达根桥Bridge1才能得到回复,只要根桥发送的Configuration BPDU的Max Age没有过期,其他非根桥就存储该BPDU,以备转发,默认Max Age为20,而根桥以Hello Time为周期发送Configuration BPDU,通常不会过期):

stp3

    接着,当Bridge3接收到Bridge2和Bridge4转发过来的BPDU时,通过比对Bridge ID发现Bridge1才是根网桥,则将根网桥Bridge1的相关信息(periority vector),存储起来,并通过比较Priority的Root Path cost,得出自己的p1端口到达LAN5的cost(50),比Bridge4的p1端口到达LAN5的cost更少(110),因此更新自己的p1端口、p2端口的角色(role),分别为指定端口和根端口,并通过p1端口向Bridge4发送Contigration BPDU通知其Root Path cost:

stp4

    最后,Bridge4收到了Bridge3发送Contigration BPDU,通过比较Root Path cost获知Bridge3的p1端口到达LAN5的cost(50)低于自己的p1端口到达LAN5的cost(110),因此改变自己的p1端口的状态为BLOCKING(BLOCKING状态仍然可以接收BPDU):

stp5

    通过这个例子,可以看到,Configuration BPDU通常在下列情况下发送:

  • 根网桥以Hello Time为周期广播Configuration BPDU;
  • 非根网桥向其指定端口转发Configuration BPDU;
  • 新启动网桥自以为是根网桥,广播Configuration BPDU;

    另外,下文中将会看到,当网络拓扑发生变化时,也会使用到Configuration BPDU

  • 当响应TCN BPDU时,需要发送带有TCA标志的Configuration BPDU;
  • 当根网桥广播网络拓扑变化时,需要发送带有TC标志的Configuration BPDU;

5、拓扑结构改变

    Configuration BPDU用于告知根网桥的配置,以及计算Root Path Cost,TCN BPDU就是用来处理网络拓扑改变的情况。首先,要确认什么时所谓的“拓扑结构改变”,对于图来说,拓扑结构改变意味着,有点或者边的变化,比如添加一个节点,并添加几条边。但是在STP协议中,拓扑结构改变仅仅指根桥变化,或则边的添加或减少:

  • 某个网桥的非FORWARDING状态端口变为FORWARDING状态;
  • 根网桥ID变化;
  • 收到TCN BPDU,意味着另一个网桥发现了拓扑变化;

    一旦某网桥发现网络拓扑结构发生变化,则通过TCN BPDU通知根网桥,随后,根网桥通过带有TC标志的Configuration BPDU,让所有的网桥知道这次网络拓扑结构的变化。具体过程如下(仍然使用稳定状态的例子):

  1. Bridge3通过p2端口周期性的向LAN5发送TCN BPDU,以Hello Time为周期,直至收到带有TCA标志的Configuration BPDU。
  2. Bridge4收到Bridge3通过p2端口发送的TCN BPDU后,给Bridge3回复带有TCA标志的Configuration BPDU,表示已经知道其发生拓扑变化,同时将Bridge3发送的TCN BPDU通过其root端口向根桥转发,Bridge2重复Bridge4的行为,直到Bridge1收到TCN BPDU,并向Bridge2发出带有TCA标志的Configuration BPDU作为响应。
  3. Bridge1通过其所有的指定端口向外广播带有TC标志的Configuration BPDU。
  4. 所有收到Bridge1的带有TC标志的Configuration BPDU的网桥,启动Short Aging Timer,同时向其指定端口转发TC标志的Configuration BPDU。

    ​收到带有TC标志的Configuration BPDU的网桥,不断重复步骤4,所有网桥均获知网络拓扑变化的小时,同时也都启动了Short Aging Timer用于刷新本地的转发数据库,清除(clean up)过时的信息。关于Short Aging Timer,就使用来清除过时信息的计时器,具体的清除方法是,从Short Aging Timer计时开始到其过期的期间,如果转发数据库的某些信息没有被使用过,则删除这些信息。Short Aging Timer默认5分钟(ULNI中说也可以为Forward Delay,没有在IEEE 802.1D中查到,但Linux是这么实现的,在拓扑变化时为Forward Delay,其他情况为默认5分钟)。

    仍然使用稳定状态的例子,在Bridge3经过Forward Delay Time后,将将p1端口和p2端口的状态由LISTENING转为LEARNING,再经过Forward Delay Time,将他们的状态转为FORWARDING,等待端口p1或p2的状态转为FORWARDING后,Bridge3通过其根端口p2,以Hello Time为周期(实际上使用的计时器名字为TCN Timer)向外发送TCN BPDU,直至收到带TCA标志的Configuration BPDU:

tcn1

    接着,Bridge2的p1端口收到了Bridge3的p2端口的TCN BPDU,Bridge2先判断自己的p1端口是否为指定端口(不是指定端口,直接丢弃,什么事情也不做),如果是则通过p1端口发送带有TCA标志的Configuration BPDU,作为响应,同时通过其根端口p3做Bridge3在p2上做的事情,以Hello Time为周期向外发送TCN BPDU,直至收到带TCA标志的Configuration BPDU;当Bridge1通过p3端口收到Bridge2的TCN BPDU时,也做Bridge2在收到Bridge3的根端口时的事情,校验p3是否为指定端口、向Bridge2的p3回复带有TCA标志的Configuration BPDU

tcn2

    最后,当根网桥Bridge1获知网络拓扑变化后,启动Topology Change Timer(Max Age + Forward Delay,默认35秒),在Topology Change Timer结束前,通过广播带有TC标志的Configuration BPDU将网络拓扑变化的消息通知所有Bridge,任意一个网桥都通过其指定端口转发该消息,并同时回复带有TCA标志的Configuration BPDU,同时内部启动Short Aging Timer准备刷新转发数据库,将拓扑结构更新到转发数据库中:

tcn3

    关于刷新数据库,这里有个疑问。在Short Aging Timer期间(或者叫Aging Timer),如果没有转发的数据包使用该记录,则将其删除。那么就有个方向问题,Bridge4不会将LAN5的数据包转发给LAN4,那么LAN1如果想访问LAN5,那么还是通过Bridge2,Bridge2由于此前一直认为目的MAC为xxx的都要向Brdige4转发,现在还是这么转发,只不过收不到Bridge4的回复了。那么这种情况算不使用转发数据库中的信息吗?下文中将有详细解释。

    有个问题,想想有这种可能性吗?一个LAN连接了两个网桥,其中一个新启动的Brige3,一个旧的Bridge4,LAN原先通过Bridge4连到根网桥,即Bridge3为LAN的指定桥,而Bridge3启动后,Root Path cost较小,则LAN选择了Bridge3作为指定桥,同时Bridge4的相关端口将转为BLOCKING状态,但是Bridge3的相关端口此时还是LISTENING状态,并不是FORWARDING,这意味着此时LAN被孤立?是的,确实由这种情况,着决定了Bridge是否可用。

想想吧,怎么才能解决这个问题?

 6、收发Configuraton BPDU数据帧的处理流程

    先看发送,STP协议并不允许以太快的速度发送BPDU,任意两个BPDU包的发送时间间隔必须超过Hold Time(默认1s)。对于任意接收的Configuration BPDU的Bridge按照下面的流程对其进行处理(摘自ULNI Chapter15):

transmit_configuration_bpdu

    上述流程对应Linux内核Bridge代码的br_transmit_config()函数,从函数的字面意思来看,符合上述流程(怀疑就是根据这个函数写的流程,并且这么多年根本没变化过),其中具体的BPDU的赋值过程在br_send_config_bpdu()接口中完成:


void br_transmit_config(struct net_bridge_port *p){struct br_config_bpdu bpdu;struct net_bridge *br;if(timer_pending(&p->hold_timer)) {p->config_pending = 1;return;}br = p->br;bpdu.topology_change = br->topology_change;bpdu.topology_change_ack = p->topology_change_ack;bpdu.root = br->designated_root;bpdu.root_path_cost = br->root_path_cost;bpdu.bridge_id = br->bridge_id;bpdu.port_id = p->port_id;if(br_is_root_bridge(br))bpdu.message_age = 0;else{struct net_bridge_port *root= br_get_port(br, br->root_port);bpdu.message_age = (jiffies - root->designated_age)+ MESSAGE_AGE_INCR;}bpdu.max_age = br->max_age;bpdu.hello_time = br->hello_time;bpdu.forward_delay = br->forward_delay;if(bpdu.message_age < br->max_age) {br_send_config_bpdu(p, &bpdu);p->topology_change_ack = 0;p->config_pending = 0;mod_timer(&p->hold_timer,round_jiffies(jiffies + BR_HOLD_TIME));}}

再看接收,对于任意一个接收的Configuration BPDU数据帧,按照如下流程进行处理,注意各种Timer的使用(有个疑问,priority vector不高,则判断不是指定端口接收的,那么):

process_configuration_bpdu

    该操作对应Linux内核Bridge代码中的br_received_config_bpdu()函数,仅仅从函数的字面意思看,是符合上述流程的(有些细节可能看不到):


void br_received_config_bpdu(struct net_bridge_port *p, const struct br_config_bpdu *bpdu){struct net_bridge *br;int was_root;br = p->br;was_root = br_is_root_bridge(br);if(br_supersedes_port_info(p, bpdu)) {br_record_config_information(p, bpdu);br_configuration_update(br);br_port_state_selection(br);if(!br_is_root_bridge(br) && was_root) {del_timer(&br->hello_timer);if(br->topology_change_detected) {del_timer(&br->topology_change_timer);br_transmit_tcn(br);mod_timer(&br->tcn_timer,  jiffies + br->bridge_hello_time);}  }  if(p->port_no == br->root_port) {br_record_config_timeout_values(br, bpdu);br_config_bpdu_generation(br);if(bpdu->topology_change_ack)br_topology_change_acknowledged(br);}}elseif(br_is_designated_port(p)) {br_reply(p);}}

7、各种Timer和STP收敛时间

    STP收敛时间,是指对于一次网络拓扑的变化引起的STP协议执行到网络稳定的时间。对于一个复杂的网络,STP可能经过数分钟才能够稳定,那么要计算STP的收敛时间,先看一下STP协议中的各种Timer。STP中的Timer分两类,分别是Bridge的Timer和Port的Timer:

    Bridge Timers(此处存疑吧,我查阅了IEEE 802.1D的说法,Aging Time的默认值为300,范围为10.0-1,000,000.0s,没提和Forward Delay的关系,但Linux实现为如果拓扑变化时则Aging Time为Forward Delay,其他情况为默认值300s,想想也应该):

    Port Timers:

    根据本文中“一次稳定状态的过程”中的例子,从新添加一个Bridge3,到最终Bridge3的p1变为FORWARDING状态,中间最少要经过2个Forward Delay和一个Short Aging Time,按照默认值计算,这是300+30=330s,按照Short Aging Time等于Forward Delay算,45s过去了。

    又假设根网桥发生了变化(本文中没有画这种情况的图),比如根网桥被管理员移除,瞬间网络可能出现分割的情况,而其他网桥并不能瞬间获知这一变化,必须经过Max Age后,发现没有根网桥的Configuration BPDU,那么所有网桥开始认为自己是根网桥(可能不是同时的,但大概差不多),然后经过至少两个Forward Delay以后,才能选出真正的根网桥。那么,这段时间耗费是Max Age + 2*Forward Delay,都按照默认的计算,为20+2*15=50s。

    增加节点5分钟起作用,改变根网桥50秒起作用,对于高速网络而言,这是不可接受的,这也引出了RSTP和MSTP。

8、转发数据库操作

    地址学习很重要,没有地址学习功能,Bridge就没法在L2层上转发数据帧,但相对于STP协议,地址学习属于细节,因此放在后面。

    转发数据库,Forward Database,起到转发目的网桥查询功能,即转发每个数据帧时,根据目的MAC地址通过查询FDB中对应的PortID,并通过该Port将数据帧转发出去,FDB使用“地址学习”的方法来增加其条目,通过Shrot Aging Timer来清除过期的条目,下文中也将FDB条目成为FDB entry。看下Linux对FDB的实现:

struct net_bridge_fdb_entry{struct hlist_node       hlist;struct net_bridge_port      *dst;struct rcu_head         rcu;unsigned long          updated;unsigned long          used;mac_addr            addr;unsigned char          is_local;unsigned char          is_static;__u16               vlan_id;};


mac_addr和net_bridge_port是目的MAC和其对应的网桥端口ID,hlist_node用于链表维护、rcu_head用于同步链表,这两个与FDB本身的功能无关。剩下几个属性,从名字上看,updated是否更新过?used是否用过?is_local是否本地?is_static是否静态?is_local还比较好理解,因为网桥连接了不同的LAN,因此一台主机上运行的Linux系统上启动了网桥功能,很可能是连接所有LAN的网桥中的一个,那么自己维护的网桥就是本地桥,其他主机维护的网桥就是远程桥。vlan_id,显然与vlan有关,vlan应该是在链路层上有比较特殊的结构,以至于不能仅仅通过MAC地址的方式完成转发。

下面看下“地址学习”的过程和Short Aging Timer的原理,首先是地址学习:

    Bridge通过一种叫“地址学习”的功能来确定数据帧如何在L2上转发,也即对于任意一个数据帧,从某个Port到达Bridge,Bridge就可以分析其源MAC和目的MAC,Bridge认为,源MAC地址属于接收Port连接的LAN,而目的MAC地址,查阅FDB是否有描述属于哪个LAN(对应的Port),如果査到目的MAC对应的LAN,则能够找到转发Port,转发即可。如果没有査到目的MAC对应的LAN,则对该数据帧进行“洪泛”,也即将其发送给除了源Port以外的所有Port。一但有一个Port有反馈相应MAC地址的数据帧,Bridge认为目的MAC地址属于相关Port得到确认,并将这条记录存在FDB中:

address_learn

    通过上述图片容易理解“地址学习”过程,说白了就两句话:从一个Port收到的数据帧,数据帧的源MAC地址与该PortID相关联;不识别的目的MAC地址,进行洪泛(flooding),即向除了源Port外的所有Port广播该数据帧。但是地址学习的过程也比较容易出现问题:

  • 因为很多情况,并不会有反馈的数据包发送过来,比如UDP包,那么这就意味着,洪泛的次数会很多,这样会严重影响网络的带宽;
  • IP层有多播和广播,不能仅仅通过MAC地址来判断该发给哪个LAN,难不成要分析IP地址?
  • 对于发网Internet的数据包,肯定不属于任意一个LAN,每次都要洪泛?那不是很容易被Dos?此处先存疑,相信Bridge肯定解决了这些问题。

​    对于以上三个问题,解答如下,不知到对不对:

  • UDP数据包并非没个都UDP包都没有反馈,只要有一个反馈数据包,就可以记住,不过这样也很讨厌,想象一个使用UDP传输大量数据的应用,如FTP,前期通过UDP socket进行通信,可以有UDP包返回,但在数据传输过程中,就没有UDP包返回了,一旦传输时间很长,就会发生再次洪泛的可能性;
  • 对于IP曾的多播和广播,Bridge并不管,只是学习,如果FDB中没有该条目,就洪泛。如果是多播或者组播,洪泛的结果可能是多个Port均有反馈的数据包,那么就插入多条记录。这样也有不好的情况,因为多播地址是随时可以加入的,一旦某IP临时加入多播地址,却属于不同的LAN,Bridge就无法解决此问题?
  • Bridge只转发LAN数据包,不转发Internet数据包,Internet数据包归Router管。

    看下Linux内核如何实现Address Learning的过程:

    内核初始化网桥模块的时候就应该创建FDB,即使此时系统内一个网桥也没有,FDB可以为空;创建网桥后,可以先不添加Port(网卡),那么此时FDB仍然为空;如果添加Port后,那么在Port的链表里面就应该有了相关的mac_addr,如果Port转化为FORWARDING状态后,如果从该Port上接收到数据包,就可以执行更新FDB entry的操作了,如果是洪泛,则不添加FDB entry,等待有反馈数据包是再更新FDB entry,关联上Port。同样的,为一个网桥删除Port时,将删除与Port关联的所有FDB enry,在内核卸载bridge模块时,删除FDB。具备地址学习功能的Port必须处于LEARNING或者FORWARDING状态。

    Bridge提供了一系列函数操作FDB,br_fdb_xxx(),这里仅仅列出部分函数名字,功能如字面含义:

  • br_fdb_init()
  • br_fdb_fini()
  • br_fdb_changeaddr()
  • br_fdb_delete_by_port()
  • br_fdb_insert()
  • br_fdb_update()
  • br_fdb_delete_by_addr()

    下面来段逆向工程,首先找到fdb_create()为添加、更新FDB entry的原始函数,再看其调用关系:

fdb_create

    fdb_create()接口直接创建一个FDB entry并插入到net_bridge->hlist里。Bridge代码在几个位置调用了该函数,分别是br_device_ops.ndo_do_ioctl、br_device_ops.ndo_set_mac_address、br_device_ops.ndo_fdb_add以及br_device_notifier.notifier_call()。这些位置均为更改网桥配置导致的FDB的变化,在为网桥增加网卡、改变网桥MAC地址、其他网卡改变MAC地址、或者用户手动添加一条FDB记录,都会触发向FDB里插入一条记录的动作,但这不是真正的“地址学习”的过程。真正的地址“学习”过程是br_handle_frame接口,该接口与网桥设备的rx_handler相挂接,负责当网桥接收到数据帧时,对数据帧进行处理,处理的过程无非就是过滤和转发,在drop掉无需转发的数据帧,通过br_fdb_update()接口来更新FDB条目,此处不讨论他的细节了,但br_handle_frame()会在另外一篇Blog中详细讨论。

  Short Aging Timer用于刷新FDB,将那些过期的条目删除掉,这些过期的条目主要由网络拓扑结构变化引起。原理上,关于Aging Timer的启动点:

aging

    看下Linux内核对Short Aging Timer的实现。在Linux内核中的Bridge代码实现,Short Aging Timer被实现为gc_timer,是结构体net_bridge的一个成员。gc_timer在网桥这一网络设备open时初始化,在其stop时删除,gc_timer同时在刷新FDB后被重置。gc_timer在增加网桥时被setup_timer()函数注册为到期自动触发br_fdb_cleanup()

gc_timer

gc_time到期后自动触发br_fdb_cleanup(),用于清理过期的条目:


void br_fdb_cleanup(unsigned long _data){struct net_bridge *br = (struct net_bridge *)_data;unsigned long delay = hold_time(br);unsigned long next_timer = jiffies + br->ageing_time;int i;spin_lock(&br->hash_lock);for(i = 0; i < BR_HASH_SIZE; i++) {struct net_bridge_fdb_entry *f; struct hlist_node *n; hlist_for_each_entry_safe(f, n, &br->hash[i], hlist) {unsigned long this_timer;if(f->is_static)continue;this_timer = f->updated + delay;if(time_before_eq(this_timer, jiffies))fdb_delete(br, f); elseif(time_before(this_timer, next_timer))next_timer = this_timer;}  }  spin_unlock(&br->hash_lock);mod_timer(&br->gc_timer, round_jiffies_up(next_timer));}

br->aging_time默认为300s,hold_time代码如下,delay代表Short Aging Timer的时间段,为300s或者15s,根据是否发生网络拓扑

变化:

static inline unsigned long hold_time(const struct net_bridge *br){returnbr->topology_change ? br->forward_delay : br->ageing_time;}

mod_timer重置timer的到期时间,其他代码容易理解不逐条说明了。

 

 总结

    这篇博客写得并不容易,起初我想自己通过Linux内核代码学习STP的实现,通过提出问题,解决问题的方法来学习,但是发现在很多理论不清楚的情况下,非常费劲,最终不得不退回到ULNI的Chapter14、15、16三章,ULNI写得非常清晰,当时我不清楚ULNI为什么使用4章共112页去描述网桥,读完其中的部分章节后发现,ULNI说的比较清楚,篇幅用来将复杂问题简单化,篇幅等于理解。

    其次,写道这我还是有不少不明白的东西,或者疑问,在原文中直接以问题的方式提出,并标记为黄色背景,但由于时间问题,可能会留到后面去具体分析了。本文中还几个问题没有弄清除:

  • 对VLAN的处理,Linux内核中有专门的处理方法,从STP到FDB都与普通的Data Frame不太一致;
  • 对MAC地址组播和广播的处理;

     另外画图浪费时间,应该放到最后去做。

    由于文章并非一次性写成,因此可能存在上下文的错误,敬请指出。