网桥原理

  • Post author:
  • Post category:其他




目 录



1



…..



前言



2






网桥的原理



2.1






桥接的概念



2.2






linux

的桥接实现



2.3






网桥的功能



3






网桥的配置



4






网桥的实现



4.1






初始化



4.2






新建网桥



4.3






添加删除端口



5






网桥数据结构



6






网桥数据库的维护



6.1






数据库的创建和销毁



6.2






数据库更新



6.3






创建数据项



6.4






查找数据项



6.5






MAC

地址过期清理



7






网桥数据包的处理流程



7.1






netif_receive_skb



7.2






Br_handle_frame



7.3






Br_handle_frame_finish



7.4






Br_pass_frame_up



7.5






Br_forward



7.6






__br_forward



7.7






Br_forward_finish



7.8






Br_dev_queue_push_xmit



8



…..



参考文献



1


前言


本文的参考分析的源代码版本是2.6.15,我是边学习边总结,学习的过程中得益于Linux论坛(


http://linux.chinaunix.net/bbs/


)上大侠们总结分析的文档,他山之石可以攻玉,学习过程中我也会边学边总结,开源的发展在于共享,我也抛块砖,望能引到玉!


由于自身水平有限,且相关的参考资料较少,因此其中的结论不能保证完全正确,如果在阅读本文的过程中发现了问题欢迎及时与作者联系。也希望能有机会和大家多多交流学习心得!



2


网桥的原理



2.1





桥接的概念


简单来说,桥接就是把一台机器上的若干个网络接口“连接”起来。其结果是,其中一个网口收到的报文会被复制给其他网口并发送出去。以使得网口之间的报文能够互相转发。

交换机就是这样一个设备,它有若干个网口,并且这些网口是桥接起来的。于是,与交换机相连的若干主机就能够通过交换机的报文转发而互相通信。

如下图:主机A发送的报文被送到交换机S1的eth0口,由于eth0与eth1、eth2桥接在一起,故而报文被复制到eth1和eth2,并且发送出 去,然后被主机B和交换机S2接收到。而S2又会将报文转发给主机C、D。



交换机在报文转发的过程中并不会篡改报文数据,只是做原样复制。然而桥接却并不是在物理层实现的,而是在数据链路层。交换机能够理解数据链路层的报文,所 以实际上桥接却又不是单纯的报文转发。

交换机会关心填写在报文的数据链路层头部中的Mac地址信息(包括源地址和目的地址),以便了解每个Mac地址所代表的主机都在什么位置(与本交换机的哪 个网口相连)。在报文转发时,交换机就只需要向特定的网口转发即可,从而避免不必要的网络交互。这个就是交换机的“地址学习”。但是如果交换机遇到一个自 己未学习到的地址,就不会知道这个报文应该从哪个网口转发,则只好将报文转发给所有网口(接收报文的那个网口除外)。

比如主机C向主机A发送一个报文,报文来到了交换机S1的eth2网口上。假设S1刚刚启动,还没有学习到任何地址,则它会将报文转发给eth0和 eth1。同时,S1会根据报文的源Mac地址,记录下“主机C是通过eth2网口接入的”。于是当主机A向C发送报文时,S1只需要将报文转发到 eth2网口即可。而当主机D向C发送报文时,假设交换机S2将报文转发到了S1的eth2网口(实际上S2也多半会因为地址学习而不这么做),则S1会 直接将报文丢弃而不做转发(因为主机C就是从eth2接入的)。

然而,网络拓扑不可能是永不改变的。假设我们将主机B和主机C换个位置,当主机C发出报文时(不管发给谁),交换机S1的eth1口收到报文,于是交换机 S1会更新其学习到的地址,将原来的“主机C是通过eth2网口接入的”改为“主机C是通过eth1网口接入的”。

但是如果主机C一直不发送报文呢?S1将一直认为“主机C是通过eth2网口接入的”,于是将其他主机发送给C的报文都从eth2转发出去,结果报文就发 丢了。所以交换机的地址学习需要有超时策略。对于交换机S1来说,如果距离最后一次收到主机C的报文已经过去一定时间了(默认为5分钟),则S1需要忘记 “主机C是通过eth2网口接入的”这件事情。这样一来,发往主机C的报文又会被转发到所有网口上去,而其中从eth1转发出去的报文将被主机C收到。


2.2







linux






的桥接实现







linux

内核支持网口的桥接(目前只支持以太网接口)。但是与单纯的交换机不同,交换机只是一个二层设备,对于接收到的报文,要么转发、要么丢弃。小型


的交换机里面只需要一块交换芯片即可,并不需要

CPU

。而运行着

linux

内核的机器本身就是一台主机,有可能就是网络报文的目的地。其收到的报文除了转


发和丢弃,还可能被送到网络协议栈的上层(网络层),从而被自己消化。


linux

内核是通过一个虚拟的网桥设备来实现桥接的。这个虚拟设备可以绑定若干个以太网接口设备,从而将它们桥接起来。如下图(摘自

ULNI

):







网桥设备br0绑定了eth0和eth1。对于网络协议栈的上层来说,只看得到br0,因为桥接是在数据链路层实现的,上层不需要关心桥接的细节。于是协 议栈上层需要发送的报文被送到br0,网桥设备的处理代码再来判断报文该被转发到eth0或是eth1,或者两者皆是;反过来,从eth0或从eth1接 收到的报文被提交给网桥的处理代码,在这里会判断报文该转发、丢弃、或提交到协议栈上层。

而有时候eth0、eth1也可能会作为报文的源地址或目的地址,直接参与报文的发送与接收(从而绕过网桥)。



2.3








网桥的功能






概括来说,网桥实现最重要的两点:


1.


MAC学习:学习MAC地址,起初,网桥是没有任何地址与端口的对应关系的,它发送数据,还是得想HUB一样,但是每发送一个数据,它都会关心数据包的来源MAC是从自己的哪个端口来的,由于学习,建立地址-端口的对照表(CAM表)。


2.


报文转发:每发送一个数据包,网桥都会提取其目的MAC地址,从自己的地址-端口对照表(CAM表)中查找由哪个端口把数据包发送出去。



3


网桥的配置







Linux


里面使用网桥非常简单,仅需要做两件事情就可以配置了。其一是在编译内核里把


CONFIG_BRIDGE





CONDIG_BRIDGE_MODULE


编译选项打开;其二是安装


brctl


工具。第一步是使内核协议栈支持网桥,第二步是安装用户空间工具,通过一系列的


ioctl


调用来配置网桥。下面以一个相对简单的实例来贯穿全文,以便分析代码。


Linux机器有4个网卡,分别是eth0~eth4,其中eth0用于连接外网,而eth1, eth2, eth3都连接到一台PC机,用于配置网桥。只需要用下面的命令就可以完成网桥的配置:


Brctl addbr br0 (建立一个网桥br0, 同时在Linux内核里面创建虚拟网卡br0)


Brctl addif br0 eth1


Brctl addif br0 eth2


Brctl addif br0 eth3 (分别为网桥br0添加接口eth1, eth2和eth3)


其中br0作为一个网桥,同时也是虚拟的网络设备,它即可以用作网桥的管理端口,也可作为网桥所连接局域网的网关,具体情况视你的需求而定。要使用br0接口时,必需为它分配IP地址。为正常工作,PC1, PC2,PC3和br0的IP地址分配在同一个网段。





在内核,网桥是以模块的方式存在,注册源码路径:\net\brige\br.c:


4.1 初始化





static


int

__init br_init

(


void


)



{



br_fdb_init

(


)


;




//网桥数据库初始化,分配slab缓冲区


#


ifdef

CONFIG_BRIDGE_NETFILTER


if


(

br_netfilter_init

(


)


)




//netfilter钩子初始化




return

1

;



#


endif


brioctl_set

(

br_ioctl_deviceless_stub

)


;




//设置ioctl钩子函数:br_ioctl_hook



br_handle_frame_hook

=

br_handle_frame

;




//设置报文处理钩子:br_ioctl_hook




//网桥数据库处理钩子



br_fdb_get_hook

=

br_fdb_get

;


br_fdb_put_hook

=

br_fdb_put

;




//在netdev_chain通知链表上注册



register_netdevice_notifier

(


&

br_device_notifier

)


;


return

0

;



}



4.2 新建网桥


前面说到通过brctl addbr br0命令建立网桥,此处用户控件调用的brctl命令最终对应到内核中的br_ioctl_deviceless_stub处理函数:





int

br_ioctl_deviceless_stub

(


unsigned


int

cmd

,


void

__user

*

uarg

)



{




switch


(

cmd

)


{




case


SIOCGIFBR


:



case


SIOCSIFBR


:



return

old_deviceless

(

uarg

)


;


case

SIOCBRADDBR

:




//新建网桥




case

SIOCBRDELBR

:




//删除网桥




{




char

buf

[

IFNAMSIZ

]


;


if


(


!

capable

(

CAP_NET_ADMIN

)


)



return


-

EPERM

;




//copy_from_user:把用户空间的数据拷入内核空间




if


(

copy_from_user

(

buf

,

uarg

,

IFNAMSIZ

)


)



return


-

EFAULT

;

buf

[

IFNAMSIZ

-

1

]


=

0

;



if


(

cmd

=


=

SIOCBRADDBR

)



return

br_add_bridge

(

buf

)


;


return

br_del_bridge

(

buf

)


;



}



}



return


-

EOPNOTSUPP

;



}



在这里,我们传入的cmd为SIOCBRADDBR.转入br_add_bridge(buf)中进行:





int

br_add_bridge

(


const


char


*

name

)



{




struct

net_device

*

dev

;



int

ret

;




//为虚拟桥新建一个net_device



dev

=

new_bridge_dev

(

name

)


;



if


(


!

dev

)



return


-

ENOMEM

;

rtnl_lock

(


)


;





//由内核确定接口名字,例如eth0 eth1等




if


(


strchr


(

dev

-


>

name

,


'%'


)


)


{



ret

=

dev_alloc_name

(

dev

,

dev

-


>

name

)


;



if


(

ret

<

0

)



goto

err1

;



}





//向内核注册此网络设备



ret

=

register_netdevice

(

dev

)


;



if


(

ret

)



goto

err2

;




/* network device kobject is not setup until

* after rtnl_unlock does it's hotplug magic.

* so hold reference to avoid race.

*/


dev_hold

(

dev

)


;


rtnl_unlock

(


)


;




//在sysfs中建立相关信息



ret

=

br_sysfs_addbr

(

dev

)


;


dev_put

(

dev

)


;


if


(

ret

)


unregister_netdev

(

dev

)


;


out

:



return

ret

;

err2

:


free_netdev

(

dev

)


;


err1

:


rtnl_unlock

(


)


;



goto

out

;



}



网桥是一个虚拟的设备,它的注册跟实际的物理网络设备注册是一样的。我们关心的是网桥对应的net_device结构是什么样的,继续跟踪进new_bridge_dev:





static


struct

net_device

*

new_bridge_dev

(


const


char


*

name

)



{




struct

net_bridge

*

br

;



struct

net_device

*

dev

;




//分配net_device



dev

=

alloc_netdev

(


sizeof


(


struct

net_bridge

)


,

name

,


br_dev_setup

)


;


if


(


!

dev

)



return


NULL


;





//网桥的私区结构为net_bridge



br

=

netdev_priv

(

dev

)


;





//私区结构中的dev字段指向设备本身



br

-


>

dev

=

dev

;

spin_lock_init

(


&

br

-


>

lock

)


;





//队列初始化。在port_list中保存了这个桥上的端口列表



INIT_LIST_HEAD

(


&

br

-


>

port_list

)


;


spin_lock_init

(


&

br

-


>

hash_lock

)


;




//下面这部份代码跟stp协议相关,我们暂不关心



br

-


>

bridge_id

.

prio

[

0

]


=

0x80

;


br

-


>

bridge_id

.

prio

[

1

]


=

0x00

;



memset


(

br

-


>

bridge_id

.

addr

,

0

,

ETH_ALEN

)


;

br

-


>

stp_enabled

=

0

;


br

-


>

designated_root

=

br

-


>

bridge_id

;


br

-


>

root_path_cost

=

0

;


br

-


>

root_port

=

0

;


br

-


>

bridge_max_age

=

br

-


>

max_age

=

20

*

HZ

;


br

-


>

bridge_hello_time

=

br

-


>

hello_time

=

2

*

HZ

;


br

-


>

bridge_forward_delay

=

br

-


>

forward_delay

=

15

*

HZ

;


br

-


>

topology_change

=

0

;


br

-


>

topology_change_detected

=

0

;


br

-


>

ageing_time

=

300

*

HZ

;


INIT_LIST_HEAD

(


&

br

-


>

age_list

)


;

br_stp_timer_init

(

br

)


;


return

dev

;



}



在br_dev_setup中还做了一些另外在函数指针初始化:





void

br_dev_setup

(


struct

net_device

*

dev

)



{






//将桥的MAC地址设为零




memset


(

dev

-


>

dev_addr

,

0

,

ETH_ALEN

)


;





//初始化dev的部分函数指针,因为目前网桥设备主适用于以及网,






//以太网的部分功能对它也适用



ether_setup

(

dev

)


;




//设置设备的ioctl函数为br_dev_ioctl



dev

-


>

do_ioctl

=

br_dev_ioctl

;





//网桥与一般网卡不同,网桥统一统计它的数据包和字节数等信息



dev

-


>

get_stats

=

br_dev_get_stats

;





// 网桥接口的数据包发送函数,真实设备要向外发送数据时,是通过网卡向外发送数据






// 而该网桥设备要向外发送数据时,它的处理逻辑与网桥其它接口的基本一致。



dev

-


>

hard_start_xmit

=

br_dev_xmit

;


dev

-


>


open


=

br_dev_open

;


dev

-


>

set_multicast_list

=

br_dev_set_multicast_list

;


dev

-


>

change_mtu

=

br_change_mtu

;


dev

-


>

destructor

=

free_netdev

;


SET_MODULE_OWNER

(

dev

)


;


dev

-


>

stop

=

br_dev_stop

;


dev

-


>

tx_queue_len

=

0

;


dev

-


>

set_mac_address

=


NULL


;


dev

-


>

priv_flags

=

IFF_EBRIDGE

;



}






4.3


添加删除端口




仅仅创建网桥,还是不够的。实际应用中的网桥需要添加实际的端口(即物理接口),如例子中的


eth1, eth2


等。应用程序在使用


ioctl


来为网桥增加物理接口,对应内核函数


br_dev_ioctl


的代码和分析如下:






int

br_dev_ioctl

(


struct

net_device

*

dev

,


struct

ifreq

*

rq

,


int

cmd

)



{




struct

net_bridge

*

br

=

netdev_priv

(

dev

)


;


switch


(

cmd

)


{




case


SIOCDEVPRIVATE


:



return

old_dev_ioctl

(

dev

,

rq

,

cmd

)


;


case

SIOCBRADDIF

:




//添加




case

SIOCBRDELIF

:




//删除






//同一处理函数,默认为添加




return

add_del_if

(

br

,

rq

-


>

ifr_ifindex

,

cmd

=


=

SIOCBRADDIF

)


;


}

pr_debug

(


"Bridge does not support ioctl 0x%x\n"


,

cmd

)


;



return


-

EOPNOTSUPP

;



}





下面分析具体的添加删除函数add_del_if:






static


int

add_del_if

(


struct

net_bridge

*

br

,


int

ifindex

,


int

isadd

)



{




struct

net_device

*

dev

;



int

ret

;


if


(


!

capable

(

CAP_NET_ADMIN

)


)



return


-

EPERM

;

dev

=

dev_get_by_index

(

ifindex

)


;



if


(

dev

=


=


NULL


)



return


-

EINVAL

;


if


(

isadd

)


ret

=

br_add_if

(

br

,

dev

)


;



else


ret

=

br_del_if

(

br

,

dev

)


;

dev_put

(

dev

)


;



return

ret

;



}





对应的添加删除函数分别为:br_add_if, br_del_if;


br_add_if:





int

br_add_if

(


struct

net_bridge

*

br

,


struct

net_device

*

dev

)



{




struct

net_bridge_port

*

p

;



int

err

=

0

;


/*--Kernel仅支持以太网网桥--*/



if


(

dev

-


>

flags

&

IFF_LOOPBACK

|


|

dev

-


>

type

!


=

ARPHRD_ETHER

)



return


-

EINVAL

;




/*--把网桥接口当作物理接口加入到另一个网桥中,是不行的,

逻辑和代码上都会出现 loop--*/



if


(

dev

-


>

hard_start_xmit

=


=

br_dev_xmit

)



return


-

ELOOP

;


/*--该物理接口已经绑定到另一个网桥了--*/



if


(

dev

-


>

br_port

!


=


NULL


)



return


-

EBUSY

;


/*--为该接口创建一个网桥端口数据,并初始化好该端口的相关数据--*/



if


(

IS_ERR

(

p

=

new_nbp

(

br

,

dev

,

br_initial_port_cost

(

dev

)


)


)


)



return

PTR_ERR

(

p

)


;




/*--将该接口的物理地址写入到 MAC-端口映射表中,

该MAC是属于网桥内部端口的固定MAC地址,

它在fdb中的记录是固定的,不会失效(agged)--*/



if


(


(

err

=

br_fdb_insert

(

br

,

p

,

dev

-


>

dev_addr

)


)


)


destroy_nbp

(

p

)


;



/*--添加相应的系统文件信息--*/



else


if


(


(

err

=

br_sysfs_addif

(

p

)


)


)


del_nbp

(

p

)


;



else


{






/*--打开该接口的混杂模式,网桥中的各个端口必须处于混杂模式,

网桥才能正确工作--*/


dev_set_promiscuity

(

dev

,

1

)


;


/*--加到端口列表--*/


list_add_rcu

(


&

p

-


>


list


,


&

br

-


>

port_list

)


;


/*--STP相关设置-*/


spin_lock_bh

(


&

br

-


>

lock

)


;


br_stp_recalculate_bridge_id

(

br

)


;


br_features_recompute

(

br

)


;



if


(


(

br

-


>

dev

-


>

flags

&

IFF_UP

)



&


&


(

dev

-


>

flags

&

IFF_UP

)


&


&

netif_carrier_ok

(

dev

)


)


br_stp_enable_port

(

p

)


;


spin_unlock_bh

(


&

br

-


>

lock

)


;


/*--设置设备的mtu--*/


dev_set_mtu

(

br

-


>

dev

,

br_min_mtu

(

br

)


)


;



}


return

err

;



}






br_del_if:








int

br_del_if

(


struct

net_bridge

*

br

,


struct

net_device

*

dev

)



{




struct

net_bridge_port

*

p

=

dev

-


>

br_port

;


if


(


!

p

|


|

p

-


>

br

!


=

br

)



return


-

EINVAL

;

br_sysfs_removeif

(

p

)


;


del_nbp

(

p

)


;

spin_lock_bh

(


&

br

-


>

lock

)


;


br_stp_recalculate_bridge_id

(

br

)


;


br_features_recompute

(

br

)


;


spin_unlock_bh

(


&

br

-


>

lock

)


;


return

0

;



}






5


网桥数据结构



网桥最主要有三个数据结构:struct net_bridge,struct net_bridge_port,struct net_bridge_fdb_entry,他们之间的关系如下图:






展开来如下图:




说明:


1.


其中最左边的

net_device

是一个代表网桥的虚拟设备结构,它关联了一个

net_bridge

结构,这是网桥设备所特有的数据结构。


2.




net_bridge

结构中,

port_list

成员下挂一个链表,链表中的每一个节点(

net_bridge_port

结构)关联到一个真实的网口设


备的

net_device

。网口设备也通过其

br_port

指针做反向的关联(那么显然,一个网口最多只能同时被绑定到一个网桥)。


3.


net_bridge


结构中还维护了一个

hash

表,是用来处理地址学习的。当网桥准备转发一个报文时,以报文的目的

Mac

地址为

key

,如果可以在

hash

表中索引到一个

net_bridge_fdb_entry

结构,通过这个结构能找到一个网口设备的

net_device

,于是报文就应该从这个网


口转发出去;否则,报文将从所有网口转发。



各个结构体具体内容如下:


struct net_bridge




struct

net_bridge


{



spinlock_t            lock

;




//读写锁






//网桥所有端口的链表,其中每个元素都是一个net_bridge_port结构




struct

list_head        port_list

;



struct

net_device

*

dev

;




//网桥对应的设备




struct

net_device_stats        statistics

;




//网桥对应的虚拟网卡的统计数据



spinlock_t            hash_lock

;




//hash表的锁






/*--CAM: 保存forwarding database的一个hash链表(这个也就是地址学习的东东,

所以通过hash能 快速定位),这里每个元素都是一个net_bridge_fsb_entry结构--*/



struct

hlist_head        hash

[

BR_HASH_SIZE

]


;



struct

list_head        age_list

;


/* STP */




//与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

;

u16                root_port

;



unsigned


char

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

;



}




2.


struct net_bridge_port





struct

net_bridge_port


{




struct

net_bridge

*

br

;




//从属于的网桥设备




struct

net_device

*

dev

;




//表示链接到这个端口的物理设备




struct

list_head

list


;


/* STP */




//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

;



}



3. struct net_bridge_fdb_entry




struct

net_bridge_fdb_entry


{




struct

hlist_node        hlist

;





//桥的端口(最主要的两个域就是这个域和下面的mac地址域)




struct

net_bridge_port

*

dst

;


struct

rcu_head            rcu

;




//当使用RCU策略,才用到

atomic_t                use_count

;




//引用计数




unsigned


long

ageing_timer

;




//MAC超时时间



mac_addr                addr

;




//mac地址。


unsigned


char

is_local

;




//是否为本机的MAC地址




unsigned


char

is_static

;




//是否为静态MAC地址




}




6


网桥数据库的维护



这里所说的网桥数据库指的是CAM表,即struct net_bridge结构中的hash表,数据库的维护对应的是对结构struct net_bridge_fdb_entry的操作;



众所周知,网桥需要维护一个


MAC


地址





端口映射表,端口是指网桥自身提供的端口,而


MAC


地址是指与端口相连的另一端的


MAC


地址。当网桥收到一个报文时,先获取它的源


MAC


,更新数据库,然后读取该报文的目标


MAC


地址,查找该数据库,如果找到,根据找到条目的端口进行转发;否则会把数据包向除入口端口以外的所有端口转发。



6.1


数据库的创建和销毁



数据库使用


kmem_cache_create


函数进行创建,使用


kmem_cache_desctory


进行销毁。路径:


[/net/bridge/br_fdb.c]:




void

__init br_fdb_init

(


void


)



{



br_fdb_cache

=

kmem_cache_create

(


"bridge_fdb_cache"


,



sizeof


(


struct

net_bridge_fdb_entry

)


,


0

,


SLAB_HWCACHE_ALIGN

,


NULL


,


NULL


)


;



}




6.2


数据库更新




当网桥收到一个数据包时,它会获取该数据的源


MAC


地址,然后对数据库进行更新。如果该


MAC


地址不在数库中,则创新一个数据项。如果存在,更新它的年龄。数据库使用


hash


表的结构方式,便于高效查询。下面是


hash


功能代码的分析:


路径:[/net/bridge/br_fdb.c]




void

br_fdb_update

(


struct

net_bridge

*

br

,


struct

net_bridge_port

*

source

,



const


unsigned


char


*

addr

)



{






/*--br_mac_hash函数是hash表中的hash函数,具体算法过程可参阅该函数代码;

br->hash就是数据库的hash表,每个hash值对应一个链表;

数据库的每项为net_bridge_fdb_entry结构--*/



struct

hlist_head

*

head

=


&

br

-


>

hash

[

br_mac_hash

(

addr

)


]


;



struct

net_bridge_fdb_entry

*

fdb

;


/* some users want to always flood. */



if


(

hold_time

(

br

)


=


=

0

)



return


;

rcu_read_lock

(


)


;


fdb

=

fdb_find

(

head

,

addr

)


;



/*--如果找到对应的fdb,更新fdb->dst,fdb->ageing_timer--*/



if


(

likely

(

fdb

)


)


{




/* attempt to update an entry for a local interface */



if


(

unlikely

(

fdb

-


>

is_local

)


)


{




if


(

net_ratelimit

(


)


)


printk

(

KERN_WARNING

"%s: received packet with "



" own address as source address\n"


,


source

-


>

dev

-


>

name

)


;



}


else


{




/* fastpath: update of existing entry */


fdb

-


>

dst

=

source

;


fdb

-


>

ageing_timer

=

jiffies

;



}



}


else


{



/*--没有找到,则新建一个fdb--*/


spin_lock_bh

(


&

br

-


>

hash_lock

)


;



if


(


!

fdb_find

(

head

,

addr

)


)


fdb_create

(

head

,

source

,

addr

,

0

)


;





/* else we lose race and someone else inserts

* it first, don't bother updating

*/


spin_unlock_bh

(


&

br

-


>

hash_lock

)


;



}


rcu_read_unlock

(


)


;



}





6.3


创建数据项




在更新函数里面已为某一


MAC


找到了它所属于的


Hash


链表,因此,创建函数只需要在该链上添加一个数据项即可。




static


struct

net_bridge_fdb_entry

*

fdb_create

(


struct

hlist_head

*

head

,



struct

net_bridge_port

*

source

,



const


unsigned


char


*

addr

,



int

is_local

)



{




struct

net_bridge_fdb_entry

*

fdb

;


/*--申请数据区--*/


fdb

=

kmem_cache_alloc

(

br_fdb_cache

,

GFP_ATOMIC

)


;



if


(

fdb

)


{




memcpy


(

fdb

-


>

addr

.

addr

,

addr

,

ETH_ALEN

)


;


atomic_set

(


&

fdb

-


>

use_count

,

1

)


;


hlist_add_head_rcu

(


&

fdb

-


>

hlist

,

head

)


;


/*--添加到链表--*/

fdb

-


>

dst

=

source

;


fdb

-


>

is_local

=

is_local

;


fdb

-


>

is_static

=

is_local

;


fdb

-


>

ageing_timer

=

jiffies

;




//MAC年龄




}



return

fdb

;



}




6.4


查找数据项




查找分两种:一种是数据项更新时候的查找,另一种是转发报文时候查找,两者区别是转发时查找需要判断


MAC


地址是否过期,即我们常说的


MAC


老化;更新时则不用判断;



网桥更新一


MAC


地址时,不管该地址是否已经过期了,只需遍历该


MAC


地址对应的


Hash


链表,然后更新年龄,此时它肯定不过期了。



网桥要转发数据时,除了要找到该目标


MAC


的出口端口外,还要判断该记录是否过期了。




更新时查找:





static


inline


struct

net_bridge_fdb_entry

*

fdb_find

(


struct

hlist_head

*

head

,



const


unsigned


char


*

addr

)



{




struct

hlist_node

*

h

;



struct

net_bridge_fdb_entry

*

fdb

;


/*--遍历链表比较地址--*/


hlist_for_each_entry_rcu

(

fdb

,

h

,

head

,

hlist

)


{




if


(


!

compare_ether_addr

(

fdb

-


>

addr

.

addr

,

addr

)


)



return

fdb

;



}



return


NULL


;



}





转发时查找:





struct

net_bridge_fdb_entry

*

__br_fdb_get

(


struct

net_bridge

*

br

,



const


unsigned


char


*

addr

)



{




struct

hlist_node

*

h

;



struct

net_bridge_fdb_entry

*

fdb

;


/*--遍历链表比较地址--*/


hlist_for_each_entry_rcu

(

fdb

,

h

,


&

br

-


>

hash

[

br_mac_hash

(

addr

)


]


,

hlist

)


{




if


(


!

compare_ether_addr

(

fdb

-


>

addr

.

addr

,

addr

)


)


{




/*--判断是否过期--*/



if


(

unlikely

(

has_expired

(

br

,

fdb

)


)


)



break


;



return

fdb

;



}



}


return


NULL


;



}



比较一下,转发时多了一个函数处理:has_expired,

Has_expired


函数来决定该数据项是否是过期的,代码如下:




/*--数据项的可保留时间根据拓扑结构是否改变来决定,

改变则为forward_delay,否则为ageing_time--*/





/* if topology_changing then use forward_delay (default 15 sec)

* otherwise keep longer (default 5 minutes)

*/



static


__inline__


unsigned


long

hold_time

(


const


struct

net_bridge

*

br

)



{




return

br

-


>

topology_change

?

br

-


>

forward_delay

:

br

-


>

ageing_time

;



}


static


__inline__


int

has_expired

(


const


struct

net_bridge

*

br

,



const


struct

net_bridge_fdb_entry

*

fdb

)



{






/*--1. 如果该数据项是静态的,即不是学习过来的,它永远不会过期。

因为它就是网桥自己端口的地址

2. 如果最近更新时间加上可保留时间大于当前时间,即老化时间还在以后,

表示尚未过期,time_before_eq返回真,否则返回假

--*/



return


!

fdb

-


>

is_static


&


&

time_before_eq

(

fdb

-


>

ageing_timer

+

hold_time

(

br

)


,

jiffies

)


;



}




6.5


MAC



地址过期清理


桥建立时设置一个定时器,循环检测,如果发现有过期的MAC,则清除对应的数据项,MAC地址过期清除由函数br_fdb_cleanup实现:





/*--定时器循环检查MAC地址是否过期

定时器在桥初始化中定义开启--*/



void

br_fdb_cleanup

(


unsigned


long

_data

)



{




struct

net_bridge

*

br

=


(


struct

net_bridge

*


)

_data

;



unsigned


long

delay

=

hold_time

(

br

)


;


/*--获取MAC地址可保留时间--*/



int

i

;

spin_lock_bh

(


&

br

-


>

hash_lock

)


;



for


(

i

=

0

;

i

<

BR_HASH_SIZE

;

i

+


+


)


{




struct

net_bridge_fdb_entry

*

f

;



struct

hlist_node

*

h

,


*

n

;


/*--如果该地址不是静态的,并且已经过期,则从数据库中清除该MAC映射--*/


hlist_for_each_entry_safe

(

f

,

h

,

n

,


&

br

-


>

hash

[

i

]


,

hlist

)


{




if


(


!

f

-


>

is_static

&


&


time_before_eq

(

f

-


>

ageing_timer

+

delay

,

jiffies

)


)


fdb_delete

(

f

)


;



}



}


spin_unlock_bh

(


&

br

-


>

hash_lock

)


;


/*--更新检查定时器--*/


mod_timer

(


&

br

-


>

gc_timer

,

jiffies

+

HZ

/

10

)


;



}




7


网桥数据包的处理流程



网桥处理包遵循以下几条原则:


1.


在一个接口上接收的包不会再在那个接口上发送这个数据包;


2.


每个接收到的数据包都要学习其源地址;


3.


如果数据包是多播或广播包,则要在同一个网段中除了接收端口外的其他所有端口发送这个数据包,如果上层协议栈对多播包感兴趣,则需要把数据包提交给上层协议栈;


4.


如果数据包的目的MAC地址不能再CAM表中找到,则要在同一个网段中除了接收端口外的其他所有端口发送这个数据包;


5.


如果能够在CAM表中查询到目的MAC地址,则在特定的端口上发送这个数据包,如果发送端口和接收端口是同一端口则不发送;

网桥在整个网络子系统中处理可用下列简图说明:



网络数据包在软终端处理时会进行网桥部分处理,大致的处理流程如下(处理函数调用链):





7.1


netif_receive_skb



netif_recerve_skb函数主要做三件事情:


1.


如果有抓包程序(socket)需要skb,则将skb复制给他们;


2.


处理桥接,即如果开启了网桥,进行网桥处理;


3.

将skb交给网络层;





int

netif_receive_skb

(


struct

sk_buff

*

skb

)



{




struct

packet_type

*

ptype

,


*

pt_prev

;



struct

net_device

*

orig_dev

;



int

ret

=

NET_RX_DROP

;



unsigned


short

type

;


/* if we've gotten here through NAPI, check netpoll */



if


(

skb

-


>

dev

-


>

poll

&


&

netpoll_rx

(

skb

)


)



return

NET_RX_DROP

;


if


(


!

skb

-


>

tstamp

.

off_sec

)


net_timestamp

(

skb

)


;


if


(


!

skb

-


>

input_dev

)


skb

-


>

input_dev

=

skb

-


>

dev

;

orig_dev

=

skb_bond

(

skb

)


;

__get_cpu_var

(

netdev_rx_stat

)


.

total

+


+


;

skb

-


>

h

.

raw

=

skb

-


>

nh

.

raw

=

skb

-


>

data

;


skb

-


>

mac_len

=

skb

-


>

nh

.

raw

-

skb

-


>

mac

.

raw

;

pt_prev

=


NULL


;

rcu_read_lock

(


)


;


#


ifdef

CONFIG_NET_CLS_ACT


if


(

skb

-


>

tc_verd

&

TC_NCLS

)


{



skb

-


>

tc_verd

=

CLR_TC_NCLS

(

skb

-


>

tc_verd

)


;



goto

ncls

;



}



#


endif




/*--yangxh mark:

当网络设备收到网络数据包时,最终会在软件中断环境里调用此函数

检查该数据包是否有packet socket来接收该包,如果有则往该socket

拷贝一份,由deliver_skb来完成。

--*/


list_for_each_entry_rcu

(

ptype

,


&

ptype_all

,


list


)


{




if


(


!

ptype

-


>

dev

|


|

ptype

-


>

dev

=


=

skb

-


>

dev

)


{




if


(

pt_prev

)


ret

=

deliver_skb

(

skb

,

pt_prev

,

orig_dev

)


;


pt_prev

=

ptype

;



}



}


#


ifdef

CONFIG_NET_CLS_ACT


if


(

pt_prev

)


{



ret

=

deliver_skb

(

skb

,

pt_prev

,

orig_dev

)


;


pt_prev

=


NULL


;


/* noone else should process this after*/



}


else


{



skb

-


>

tc_verd

=

SET_TC_OK2MUNGE

(

skb

-


>

tc_verd

)


;



}

ret

=

ing_filter

(

skb

)


;


if


(

ret

=


=

TC_ACT_SHOT

|


|


(

ret

=


=

TC_ACT_STOLEN

)


)


{



kfree_skb

(

skb

)


;



goto

out

;



}

skb

-


>

tc_verd

=

0

;


ncls

:



#


endif

handle_diverter

(

skb

)


;




/*--

先试着将该数据包让网桥函数来处理,如果该数据包的入口接口确实是网桥接口,

则按网桥方式来处理,如果不是网桥接口的数据包,则不应该让网桥来处理

--*/



if


(

handle_bridge

(


&

skb

,


&

pt_prev

,


&

ret

,

orig_dev

)


)



goto

out

;


/*--对该数据包转发到它L3协议的处理函数--*/


type

=

skb

-


>

protocol

;


list_for_each_entry_rcu

(

ptype

,


&

ptype_base

[


ntohs


(

type

)


&

15

]


,


list


)


{




if


(

ptype

-


>

type

=


=

type

&


&



(


!

ptype

-


>

dev

|


|

ptype

-


>

dev

=


=

skb

-


>

dev

)


)


{




if


(

pt_prev

)


ret

=

deliver_skb

(

skb

,

pt_prev

,

orig_dev

)


;


pt_prev

=

ptype

;



}



}


if


(

pt_prev

)


{



ret

=

pt_prev

-


>

func

(

skb

,

skb

-


>

dev

,

pt_prev

,

orig_dev

)


;



}


else


{



kfree_skb

(

skb

)


;





/* Jamal, now you will not able to escape explaining

* me how you were going to use this. :-)

*/


ret

=

NET_RX_DROP

;



}

out

:


rcu_read_unlock

(


)


;



return

ret

;



}





7.2


Br_handle_frame



1.


如果

skb

的目的

Mac

地址与接收该

skb

的网口的

Mac

地址相同,则结束桥接处理过程(返回到

net_receive_skb

函数后,这个

skb

会最终


被提交给网络层);


2.


否则,调用到

br_handle_frame_finish

函数将报文转发,然后释放

skb

(返回到

net_receive_skb

函数后,这个

skb




不会往网络层提交了);






int

br_handle_frame

(


struct

net_bridge_port

*

p

,


struct

sk_buff

*


*

pskb

)



{




struct

sk_buff

*

skb

=


*

pskb

;



/*--取得数据包目的地址--*/



const


unsigned


char


*

dest

=

eth_hdr

(

skb

)


-


>

h_dest

;


/*--网桥状态为disable,返回错误,丢弃数据包--*/



if


(

p

-


>

state

=


=

BR_STATE_DISABLED

)



goto

err

;



/*--源MAC地址为非法,返回错误,丢弃数据包--*/



if


(


!

is_valid_ether_addr

(

eth_hdr

(

skb

)


-


>

h_source

)


)



goto

err

;


/*--如果网桥状态处于学习状态,则更新数据库--*/



if


(

p

-


>

state

=


=

BR_STATE_LEARNING

)


br_fdb_update

(

p

-


>

br

,

p

,

eth_hdr

(

skb

)


-


>

h_source

)


;


/*--如果是STP的BPDU数据包,则进入STP处理--*/



if


(

p

-


>

br

-


>

stp_enabled

&


&



!


memcmp


(

dest

,

bridge_ula

,

5

)


&


&



!


(

dest

[

5

]


&

0xF0

)


)


{




if


(


!

dest

[

5

]


)


{



NF_HOOK

(


PF_BRIDGE


,

NF_BR_LOCAL_IN

,

skb

,

skb

-


>

dev

,



NULL


,

br_stp_handle_bpdu

)


;



return

1

;



}



}



else


if


(

p

-


>

state

=


=

BR_STATE_FORWARDING

)


{






/*--如果该接口处于Forwarding状态,并且该报文必需要走L3层进行转发,

则直接返回--*/



if


(

br_should_route_hook

)


{




if


(

br_should_route_hook

(

pskb

)


)



return

0

;


skb

=


*

pskb

;


dest

=

eth_hdr

(

skb

)


-


>

h_dest

;



}




/*--

当用内核创建一个网桥的同时也会创建一个虚拟的网络设备,它的名字

为网桥的名字,保存在p->br->dev指针里。P->br->dev和port_list里面的

接口共同组成一个网桥。如果该报文是要发往此接,则标记skb->pkt_type为

PACKET_HOST。因为报文最终是要发送到p->br->dev的输送队列里面,

正如一般的网卡驱动程序将数据包送往到某个net_device的输入队列一样,

这样bridge功能充当了虚拟网卡(如例子中的br0)驱动,

应当设置skb->pkt_type为PACKET_HOST,

表明数据包是要发送该接口,而非是因为打开混杂模式而接收到的。

--*/



if


(


!

compare_ether_addr

(

p

-


>

br

-


>

dev

-


>

dev_addr

,

dest

)


)


skb

-


>

pkt_type

=

PACKET_HOST

;

NF_HOOK

(


PF_BRIDGE


,

NF_BR_PRE_ROUTING

,

skb

,

skb

-


>

dev

,


NULL


,


br_handle_frame_finish

)


;



return

1

;



}

err

:


/*--不能处理数据包,直接丢弃。--*/


kfree_skb

(

skb

)


;



return

1

;



}






7.3


Br_handle_frame_finish






int

br_handle_frame_finish

(


struct

sk_buff

*

skb

)



{




const


unsigned


char


*

dest

=

eth_hdr

(

skb

)


-


>

h_dest

;



struct

net_bridge_port

*

p

=

skb

-


>

dev

-


>

br_port

;



struct

net_bridge

*

br

=

p

-


>

br

;



struct

net_bridge_fdb_entry

*

dst

;



int

passedup

=

0

;




/*--

对所有报的源MAC地址进行学习,这是网桥的特点之一,

通过对源地址的学习来建立MAC地址到端口的映射。

--*/



/* insert into forwarding database after filtering to avoid spoofing */


br_fdb_update

(

p

-


>

br

,

p

,

eth_hdr

(

skb

)


-


>

h_source

)


;




/*--如果网桥的虚拟网卡处于混杂模式,那么每个接收到的数据包都需要克隆一份送到

AF_PACKET协议处理体(网络软中断函数net_rx_action中ptype_all链的处理)--*/



if


(

br

-


>

dev

-


>

flags

&

IFF_PROMISC

)


{




struct

sk_buff

*

skb2

;



/*--skb2非空,表明要发往本机,br_pass_frame_up函数完成发往本机的工作--*/


skb2

=

skb_clone

(

skb

,

GFP_ATOMIC

)


;



if


(

skb2

!


=


NULL


)


{



passedup

=

1

;


br_pass_frame_up

(

br

,

skb2

)


;



}



}



if


(

dest

[

0

]


&

1

)


{






/*--此报文是广播或组播报文,

由br_flood_forward函数把报文向所有端口转发出去

如果本地协议栈已经发过了,则算了,不再发送--*/


br_flood_forward

(

br

,

skb

,


!

passedup

)


;



if


(


!

passedup

)


br_pass_frame_up

(

br

,

skb

)


;



goto

out

;



}




/*--__br_fdb_get函数先查MAC-端口映射表,表中每项是通过结构

struct net_bridge_fdb_entry来描述的,这一步是网桥的关键。

这个报文应从哪个接口转发出去就看它了。

如果这个报文应发往本机,那么skb置空。不需要再转发了

因为发往本机接口从逻辑上来说本身就是一个转发,后续有上层协议栈处理

--*/


dst

=

__br_fdb_get

(

br

,

dest

)


;



if


(

dst

!


=


NULL


&


&

dst

-


>

is_local

)


{




if


(


!

passedup

)


br_pass_frame_up

(

br

,

skb

)


;



else


kfree_skb

(

skb

)


;



goto

out

;



}



/*--找到MAC映射,则发往对应的目的端口--*/



if


(

dst

!


=


NULL


)


{



br_forward

(

dst

-


>

dst

,

skb

)


;



goto

out

;



}



/*--dst==NULL,没有赚到映射,则广播--*/


br_flood_forward

(

br

,

skb

,

0

)


;

out

:



return

0

;



}





7.4


Br_pass_frame_up



在上个函数Br_handle_frame_finish中如果报文是需要发往本地协议栈处理的,则由函数Br_pass_frame_up实现:






static


void

br_pass_frame_up

(


struct

net_bridge

*

br

,


struct

sk_buff

*

skb

)



{




struct

net_device

*

indev

;

br

-


>

statistics

.

rx_packets

+


+


;


br

-


>

statistics

.

rx_bytes

+


=

skb

-


>

len

;

indev

=

skb

-


>

dev

;


skb

-


>

dev

=

br

-


>

dev

;


/*--报文中的dev被赋予网桥本身的虚拟dev--*/

NF_HOOK

(


PF_BRIDGE


,

NF_BR_LOCAL_IN

,

skb

,

indev

,


NULL


,


br_pass_frame_up_finish

)


;



}






这段代码非常简单,对


net_bridge


的数据统计进行更新以后,再更新


skb->dev


,最后通过


NF_HOOK





NF_BR_LOCAL_IN


挂接点上调用回了


netif_receive_skb









netif_receive_skb


函数中,调用了


handle_bridge


函数,重新触发了网桥处理流程,现在发往网桥虚拟设备的数据包又回到了


netif_receive_skb,


那么网桥的处理过程会不会又被调用呢?在


linux/net/bridge/br_if.c


里面可以看到


br_add_if


函数,实际上的操作是将某一网口加入网桥组,这个函数调用了


new_nbp(br, dev);


用以填充


net_bridge


以及


dev


结构的重要成员,里面将


dev->br_port


设定为一个新建的


net_bridge_port


结构,而上面的


br_pass_frame_up


函数将


skb->dev


赋成了


br->dev,


实际上


skb->dev


变成了网桥建立的虚拟设备,这个设备是网桥本身而不是桥组的某一端口,系统没有为其调用


br_add_if


,所以这个


net_device


结构的


br_port


指针没有进行赋值;


br_port


为空,不进入网桥处理流程




;从而进入上层协议栈处理;



7.5


Br_forward





void

br_forward

(


const


struct

net_bridge_port

*

to

,


struct

sk_buff

*

skb

)



{






/*--should_deliver: 是否符合转发条件

__br_forward: 转发--*/



if


(

should_deliver

(

to

,

skb

)


)


{



__br_forward

(

to

,

skb

)


;



return


;



}

kfree_skb

(

skb

)


;



}





7.6



__br_forward





static


void

__br_forward

(


const


struct

net_bridge_port

*

to

,


struct

sk_buff

*

skb

)



{




struct

net_device

*

indev

;

indev

=

skb

-


>

dev

;


skb

-


>

dev

=

to

-


>

dev

;


/*--替换报文中的dev为转发端口对应的dev--*/


skb

-


>

ip_summed

=

CHECKSUM_NONE

;

NF_HOOK

(


PF_BRIDGE


,

NF_BR_FORWARD

,

skb

,

indev

,

skb

-


>

dev

,


br_forward_finish

)


;



}





7.7


Br_forward_finish






int

br_forward_finish

(


struct

sk_buff

*

skb

)



{



NF_HOOK

(


PF_BRIDGE


,

NF_BR_POST_ROUTING

,

skb

,


NULL


,

skb

-


>

dev

,


br_dev_queue_push_xmit

)


;


return

0

;



}





7.8


Br_dev_queue_push_xmit






int

br_dev_queue_push_xmit

(


struct

sk_buff

*

skb

)



{




/*--报文长度超过dev发送的mtu限制,丢弃报文--*/



/* drop mtu oversized packets except tso */



if


(

skb

-


>

len

>

skb

-


>

dev

-


>

mtu

&


&


!

skb_shinfo

(

skb

)


-


>

tso_size

)


kfree_skb

(

skb

)


;



else


{




#


ifdef

CONFIG_BRIDGE_NETFILTER


/* ip_refrag calls ip_fragment, doesn't copy the MAC header. */


nf_bridge_maybe_copy_header

(

skb

)


;



#


endif


skb_push

(

skb

,

ETH_HLEN

)


;

dev_queue_xmit

(

skb

)


;



}


return

0

;



}





7.9


报文处理总结



进入桥的数据报文分为几个类型,桥对应的处理方法也不同:



1.




报文是本机发送给自己的,桥不处理,交给上层协议栈;



2.




接收报文的物理接口不是网桥接口,桥不处理,交给上层协议栈;



3.




进入网桥后,如果网桥的状态为Disable


,则将包丢弃不处理;



4.




报文源地址无效(广播,多播,以及00:00:00:00:00:00


),丢包;



5.




如果是STP


的BPDU包,进入STP处理,处理后不再转发,也不再交给上层协议栈;



6.




如果是发给本机的报文,桥直接返回,交给上层协议栈,不转发;



7.




需要转发的报文分三种情况:



1)




广播或多播,则除接收端口外的所有端口都需要转发一份;



2)




单播并且在CAM


表中能找到端口映射的,只需要网映射端口转发一份即可;



3)




单播但找不到端口映射的,则除了接收端口外其余端口都需要转发;




8


参考文献


1.



http://hi.baidu.com/_kouu/blog/item/ad2abf3ffa61cf3170cf6cd7.html


2.



http://hi.baidu.com/jrckkyy/blog/item/3bedbef37234d0c70b46e08b.html


3.



http://blog.csdn.net/linyt/archive/2010/01/15/5191512.aspx


4.



http://www.loosky.net/?p=307


5.



http://blog.csdn.net/zhaodm/archive/2006/12/25/1460041.aspx


6.



http://blog.chinaunix.net/u/12313/showart_246678.html