网桥原理

Posted Rosanne

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了网桥原理相关的知识,希望对你有一定的参考价值。

转自:blog.csdn.net/bailyzheng/article/details/29244963

 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;

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;
}
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

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) 单播但找不到端口映射的,则除了接收端口外其余端口都需要转发;

  

  

  

  

  

  

  

  

 

 

  

  

  

  

  

  

  

  

  

  

 

 

  

  

  

  

  

  

  

  

  

以上是关于网桥原理的主要内容,如果未能解决你的问题,请参考以下文章

网桥的自学习算法原理

网桥及以太网交换机原理

Docker自定义网桥pipework工作原理

生成树协议(STP)常用配置命令及原理

计算机网络基础 — 网络设备 — 网桥(Bridge)

计算机网络基础 — 网络设备 — 网桥(Bridge)