1.    前言

本文结合网络上关于Linux网桥的说明、 Linux平台的代码阅读记录,整理的一篇总结性文档。由于时间仓促,分析可能存在不足之外,望大家见谅和指正。

对于接触过Linux 网络的童鞋,对网桥功能应该不陌生。概括来说,网桥实现最重要的两点:

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

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

本文目的让读者对Linux网桥有个全面的认识。作重讲述Linux网桥的定义、网桥管理、数据流程和端口-MAC映射管理,以及网桥的Netfilter。关于网桥的STP,由于ap121上,网桥并没启用STP,所以这部分不做详细介绍,只在文档后面做一个简单介绍。

2.    网桥定义和网桥管理

在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)

本章我们的目的就是弄清楚以上几条命令在内核中是如何实现、生效的。

2.1  网桥定义

按照惯例,先熟悉一下网桥相关的重要数据结构体定义,方便后续讲解。和网桥息息相关的几个结构体包括:网桥自身定义(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它们之间的关系可以使用如下图的示意图表示:

重要数据结构关系示意图

2.2  网桥模块初始化

网桥在内核中,被实现为一个内核模块,源代码在~/1xU/ap121/linux/kernels/mips-linux-2.6.31/net/bridge/br.c中。初始化方法br_init:

static int __init br_init(void)

{

int err;

//stp的注册

err = stp_proto_register(&br_stp_proto);

if (err < 0) {

printk(KERN_ERR "bridge: can't register sap for STP\n");

return err;

}

//CAM表的初始化

err = br_fdb_init();

if (err)

goto err_out;

//网桥的netfilter钩子函数的初始化

err = br_netfilter_init();

if (err)

goto err_out1;

//注册到netdevice的通知链上

err = register_netdevice_notifier(&br_device_notifier);

if (err)

goto err_out2;

err = br_netlink_init();

if (err)

goto err_out3;

//设置网桥设备的do_ioctl函数,也就是提供给用户空间ioctl接口

brioctl_set(br_ioctl_deviceless_stub);

//设置网桥数据处理接口

br_handle_frame_hook = br_handle_frame;

//设置网桥CAM数据交换接口

br_fdb_get_hook = br_fdb_get;

br_fdb_put_hook = br_fdb_put;

return 0;

// 异常处理略

return err;

}

2.3  网桥管理

网桥内核模块初始化后,并没有真正的一个网桥设备被实例化,它只是搭建好了运行环境。要网桥真正的运作,还需要从创建一个网桥设备开始。

2.3.1    创建和删除网桥

接上文,在网桥初始化的时候,设置了网桥的ioctl接口:br_ioctl_deviceless_stub。下面看看br_ioctl_deviceless_stub的实现:

int br_ioctl_deviceless_stub(struct net *net, unsigned int cmd, void __user *uarg)

{

switch (cmd) {

case SIOCGIFBR:

case SIOCSIFBR:

return old_deviceless(net, uarg);

case SIOCBRADDBR:

case SIOCBRDELBR:

{

char buf[IFNAMSIZ];

if (!capable(CAP_NET_ADMIN))

return -EPERM;

if (copy_from_user(buf, uarg, IFNAMSIZ))

return -EFAULT;

buf[IFNAMSIZ-1] = 0;

if (cmd == SIOCBRADDBR)

return br_add_bridge(net, buf); //添加网桥

return br_del_bridge(net, buf); //删除网桥

}

}

return -EOPNOTSUPP;

}

当我们执行brctl addbr br0时,我们传入的cmd为SIOCBRADDBR,会转入br_add_bridge中进行:

int br_add_bridge(struct net *net, const char *name)

{

struct net_device *dev;

int ret;

// 创建一个网卡设备

dev = new_bridge_dev(net, name);

if (!dev)

return -ENOMEM;

rtnl_lock();

// 内核确认设备名称

if (strchr(dev->name, '%')) {

ret = dev_alloc_name(dev, dev->name);

if (ret < 0)

goto out_free;

}

// 注册网卡设备

ret = register_netdevice(dev);

if (ret)

goto out_free;

//在sysfs中建立相关信息,便于查看和管理

ret = br_sysfs_addbr(dev);

if (ret)

unregister_netdevice(dev);

out:

rtnl_unlock();

return ret;

out_free:

free_netdev(dev);

goto out;

}

网桥是一个虚拟的设备,它的注册跟实际的物理网络设备注册是一样的(可以参看《Linux设备驱动程序》网络驱动程序中,net_device创建和注册过程):

static struct net_device *new_bridge_dev(struct net *net, const char *name)

{

struct net_bridge *br;

struct net_device *dev;

// 创建net_device设备,执行网桥设备初始化程序:br_dev_setup

dev = alloc_netdev(sizeof(struct net_bridge), name,

br_dev_setup);

if (!dev)

return NULL;

// 设定net

dev_net_set(dev, net);

// net_device私有区被指定网桥,然后进行网桥相关初始化填充

br = netdev_priv(dev);

br->dev = dev;

spin_lock_init(&br->lock);

INIT_LIST_HEAD(&br->port_list);

spin_lock_init(&br->hash_lock);

br->bridge_id.prio[0] = 0x80;

br->bridge_id.prio[1] = 0x00;

memcpy(br->group_addr, br_group_address, ETH_ALEN);

br->feature_mask = dev->features;

br->stp_enabled = BR_NO_STP; // 默认不开启STP功能

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;

br_netfilter_rtable_init(br);

INIT_LIST_HEAD(&br->age_list);

br_stp_timer_init(br);

return dev;

}

更详细的,看看网桥虚拟设备初始化的细节:

void br_dev_setup(struct net_device *dev)

{

//初始化MAC

random_ether_addr(dev->dev_addr);

// 网桥设备也是以太网设备,需要进行以太网部分初始化

ether_setup(dev);

dev->netdev_ops = &br_netdev_ops;

dev->destructor = free_netdev;

SET_ETHTOOL_OPS(dev, &br_ethtool_ops);

dev->tx_queue_len = 0;

dev->priv_flags = IFF_EBRIDGE;

dev->features = NETIF_F_SG | NETIF_F_FRAGLIST | NETIF_F_HIGHDMA |

NETIF_F_GSO_MASK | NETIF_F_NO_CSUM | NETIF_F_LLTX |

NETIF_F_NETNS_LOCAL | NETIF_F_GSO;

}

static const struct ethtool_ops br_ethtool_ops = {

.get_drvinfo    = br_getinfo,

.get_link  = ethtool_op_get_link,

.get_tx_csum  = ethtool_op_get_tx_csum,

.set_tx_csum = br_set_tx_csum,

.get_sg       = ethtool_op_get_sg,

.set_sg       = br_set_sg,

.get_tso   = ethtool_op_get_tso,

.set_tso   = br_set_tso,

.get_ufo   = ethtool_op_get_ufo,

.get_flags = ethtool_op_get_flags,

};

static const struct net_device_ops br_netdev_ops = {

.ndo_open     = br_dev_open, // 打开设备

.ndo_stop     = br_dev_stop, // 停止设备

.ndo_start_xmit      = br_dev_xmit, // 发送数据

.ndo_set_mac_address = br_set_mac_address, // 设置MAC

.ndo_set_multicast_list  = br_dev_set_multicast_list, // 设置mutlicast

.ndo_change_mtu      = br_change_mtu, // 设置MTU

.ndo_do_ioctl     = br_dev_ioctl, // 设备ioctl

};

以上是创建网桥及网桥初始化的全部过程,关于网桥删除主要是上述网桥注册过程的逆过程:解除端口,清除定时器,删除sysfs设备,注销虚拟设备:

static void del_br(struct net_bridge *br)

{

struct net_bridge_port *p, *n;

list_for_each_entry_safe(p, n, &br->port_list, list) {

del_nbp(p);

}

del_timer_sync(&br->gc_timer);

br_sysfs_delbr(br->dev);

unregister_netdevice(br->dev);

}

2.3.2    添加和删除端口

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

brctl addif br0 eth0

应用程序在使用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:

// 根据cmd类型执行添加或删除端口

return add_del_if(br, rq->ifr_ifindex, cmd == SIOCBRADDIF);

}

pr_debug("Bridge does not support ioctl 0x%x\n", cmd);

return -EOPNOTSUPP;

}

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(dev_net(br->dev), 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;

}

事实上,增加一个端口就是实例化并填充一个net_bridge_port,并将其加入到网桥的端口记录表中:

int br_add_if(struct net_bridge *br, struct net_device *dev)

{

struct net_bridge_port *p;

int err = 0;

// 环路端口和非以太网设备不添加

if (dev->flags & IFF_LOOPBACK || dev->type != ARPHRD_ETHER)

return -EINVAL;

// 如果加入端口本身也是网桥设备,不添加

if (dev->netdev_ops->ndo_start_xmit == br_dev_xmit)

return -ELOOP;

// 如果加入端口设备已经属于其他网桥,不添加

if (dev->br_port != NULL)

return -EBUSY;

// new一个port

p = new_nbp(br, dev);

if (IS_ERR(p))

return PTR_ERR(p);

// 设置为混杂模式

err = dev_set_promiscuity(dev, 1);

if (err)

goto put_back;

// 一些初始化

err = kobject_init_and_add(&p->kobj, &brport_ktype, &(dev->dev.kobj),

SYSFS_BRIDGE_PORT_ATTR);

if (err)

goto err0;

// 将端口的MAC插入到端口-MAC映射表中

err = br_fdb_insert(br, p, dev->dev_addr);

if (err)

goto err1;

// 添加到sysfs文件系统中

err = br_sysfs_addif(p);

if (err)

goto err2;

rcu_assign_pointer(dev->br_port, p);

dev_disable_lro(dev);

// 添加到网桥端口记录表中

list_add_rcu(&p->list, &br->port_list);

spin_lock_bh(&br->lock);

br_stp_recalculate_bridge_id(br);

br_features_recompute(br);

if ((dev->flags & IFF_UP) && netif_carrier_ok(dev) &&

(br->dev->flags & IFF_UP))

br_stp_enable_port(p);

spin_unlock_bh(&br->lock);

br_ifinfo_notify(RTM_NEWLINK, p);

dev_set_mtu(br->dev, br_min_mtu(br));

kobject_uevent(&p->kobj, KOBJ_ADD);

return 0;

err2:

br_fdb_delete_by_port(br, p, 1);

err1:

kobject_put(&p->kobj);

err0:

dev_set_promiscuity(dev, -1);

put_back:

dev_put(dev);

kfree(p);

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;

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;

}

3.    网桥数据处理流程

在我们日常开发中,最常见的一种拓扑如下图所示:

典型拓扑

其中,DUT有三个端口ath0(本地无线端口)、eth0(有线lan口)、aht1(连接远程无线端口);该三个端口通过br0网桥桥接在一起。本章作重讲述,在该拓扑下,pc1、pc2、pc3以及ROOT-AP之间,是如何通过DUT(br0)进行数据交互的。

现假设pc3向pc1发送一个数据包,数据首先会由eth0网卡接收,此后网卡向CPU发送接收中断。当CPU执行当前指令后(如果开中断的话),马上跳到网卡的驱动程去。eht0的网卡驱动首先生成一个skb结构,然后对以太网层进行分析,最后驱动将该skb结构放到当前CPU的输入队列中,唤醒软中断。如果没有其它中断的到来,那么软中断将调用netif_receive_skb函数。关于网卡驱动和中断响应不是本文讨论的重点,所以我们还是从netif_receive_skb说起。

/**

*  netif_receive_skb - process receive buffer from network

*  @skb: buffer to process

*

*  netif_receive_skb() is the main receive data processing function.

*  It always succeeds. The buffer may be dropped during processing

*  for congestion control or by the protocol layers.

*

*  This function may only be called from softirq context and interrupts

*  should be enabled.

*

*  Return values (usually ignored):

*  NET_RX_SUCCESS: no congestion

*  NET_RX_DROP: packet was dropped

*/

int netif_receive_skb(struct sk_buff *skb)

{

struct packet_type *ptype, *pt_prev;

struct net_device *orig_dev;

struct net_device *null_or_orig;

int ret = NET_RX_DROP;

__be16 type;

if (skb->vlan_tci && vlan_hwaccel_do_receive(skb))

return NET_RX_SUCCESS;

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

#ifdef CONFIG_MAPPING

if (skb->dev)

#endif

if (netpoll_receive_skb(skb))

return NET_RX_DROP;

if (!skb->tstamp.tv64)

net_timestamp(skb);

if (!skb->iif)

skb->iif = skb->dev->ifindex;

null_or_orig = NULL;

orig_dev = skb->dev;

if (orig_dev->master) {

if (skb_bond_should_drop(skb))

null_or_orig = orig_dev; /* deliver only exact match */

else

skb->dev = orig_dev->master;

}

__get_cpu_var(netdev_rx_stat).total++;

skb_reset_network_header(skb);

skb_reset_transport_header(skb);

skb->mac_len = skb->network_header - skb->mac_header;

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

// 检查数据包是否有packet socket来接受该包(比如抓包工具),如果有则往该socket发送一份

list_for_each_entry_rcu(ptype, &ptype_all, list) {

if (ptype->dev == null_or_orig || ptype->dev == skb->dev ||

ptype->dev == orig_dev) {

if (pt_prev)

ret = deliver_skb(skb, pt_prev, orig_dev);

pt_prev = ptype;

}

}

#ifdef CONFIG_NET_CLS_ACT

skb = handle_ing(skb, &pt_prev, &ret, orig_dev);

if (!skb)

goto out;

ncls:

#endif

// 尝试交由网桥处理,如果网桥处理了,返回skb=NULL

skb = handle_bridge(skb, &pt_prev, &ret, orig_dev);

if (!skb)

goto out;

skb = handle_macvlan(skb, &pt_prev, &ret, orig_dev);

if (!skb)

goto out;

// 对数据包转到L3层处理

type = skb->protocol;

list_for_each_entry_rcu(ptype,

&ptype_base[ntohs(type) & PTYPE_HASH_MASK], list) {

if (ptype->type == type &&

(ptype->dev == null_or_orig || ptype->dev == skb->dev ||

ptype->dev == orig_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;

}

总结而言,netif_recerve_skb函数主要做三件事情:

1.  如果有socket需要(如抓包应用)skb,则将skb复制给他们;

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

3. 将skb交给网络层。

3.1  网桥处理入口:handle_bridge

static inline struct sk_buff *handle_bridge(struct sk_buff *skb,

struct packet_type **pt_prev, int *ret,

struct net_device *orig_dev)

{

struct net_bridge_port *port;

// 如果数据包是环回包,或者数据包的产生设备不属于任何网桥,则不进行网桥处理

if (skb->pkt_type == PACKET_LOOPBACK ||

(port = rcu_dereference(skb->dev->br_port)) == NULL)

return skb;

if (*pt_prev) {

*ret = deliver_skb(skb, *pt_prev, orig_dev);

*pt_prev = NULL;

}

// 调用网桥处理接口,该接口在网桥初始化时被指定为 br_handle_frame

return br_handle_frame_hook(port, skb);

}

3.2    网桥处理逻辑之br_handle_frame

/*

* Called via br_handle_frame_hook.

* Return NULL if skb is handled

* note: already called with rcu_read_lock (preempt_disabled)

*/

struct sk_buff *br_handle_frame(struct net_bridge_port *p, struct sk_buff *skb)

{

// 获取数据包MAC

const unsigned char *dest = eth_hdr(skb)->h_dest;

int (*rhook)(struct sk_buff *skb);

if (!is_valid_ether_addr(eth_hdr(skb)->h_source))

goto drop;

skb = skb_share_check(skb, GFP_ATOMIC);

if (!skb)

return NULL;

#ifdef CONFIG_ATHRS_HW_NAT

skb->ath_hw_nat_fw_flags = 1;

#endif

// 如果目的地址是01:80:c2:00:00:0X,则是发往STP的多播地址,此时可能需要进行STP处理

if (unlikely(is_link_local(dest))) {

/* Pause frames shouldn't be passed up by driver anyway */

if (skb->protocol == htons(ETH_P_PAUSE))

goto drop;

/* If STP is turned off, then forward */

if (p->br->stp_enabled == BR_NO_STP && dest[5] == 0)

goto forward;

// 在老版本的网桥实现中,这里有一个分支进行STP数据包处理。在新内核版本中(2.6,新只是相对的),STP被实现为上层协议,所以会在网桥处理后,传递到上层再进行专门处理。

if (NF_HOOK(PF_BRIDGE, NF_BR_LOCAL_IN, skb, skb->dev,

NULL, br_handle_local_finish)) //更新CAM表

return NULL; /* frame consumed by filter */

else

return skb;// 由于br_handle_local_finish返回始终为0,所以return skb将继续上层处理

}

forward:

switch (p->state) {

case BR_STATE_FORWARDING:

// 判断是否需要走三层进行转发,这个是broute表的执行函数

rhook = rcu_dereference(br_should_route_hook);

if (rhook != NULL) {

if (rhook(skb))

return skb;

dest = eth_hdr(skb)->h_dest;

}

/* 注意: fall through */

case BR_STATE_LEARNING:

if (!compare_ether_addr(p->br->dev->dev_addr, dest))

skb->pkt_type = PACKET_HOST;

// Netfilter hook点

NF_HOOK(PF_BRIDGE, NF_BR_PRE_ROUTING, skb, skb->dev, NULL,

br_handle_frame_finish);

break;

default:

drop:

kfree_skb(skb);

}

return NULL;

}

3.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 = rcu_dereference(skb->dev->br_port);

struct net_bridge *br;

struct net_bridge_fdb_entry *dst;

struct sk_buff *skb2;

// 如果网桥处于DISABLED状态,直接drop

if (!p || p->state == BR_STATE_DISABLED)

goto drop;

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

// 选择端口所属的网桥(可能有多个网桥的情况)

br = p->br;

// 更新端口-MAC映射表

br_fdb_update(br, p, eth_hdr(skb)->h_source);

if (p->state == BR_STATE_LEARNING)

goto drop;

/* The packet skb2 goes to the local host (NULL to skip). */

skb2 = NULL;

if (br->dev->flags & IFF_PROMISC)

skb2 = skb;

dst = NULL;

if (is_multicast_ether_addr(dest)) {

br->dev->stats.multicast++;

skb2 = skb;

} else if ((dst = __br_fdb_get(br, dest)) && dst->is_local) {

skb2 = skb;

/* Do not forward the packet since it's local. */

skb = NULL;

}

if (skb2 == skb)

skb2 = skb_clone(skb, GFP_ATOMIC);

if (skb2)

br_pass_frame_up(br, skb2); // 如果skb2非空,则向上传递报文

if (skb) {

if (dst)

br_forward(dst->dst, skb);

else

br_flood_forward(br, skb); // 多播或端口-MAC表中无记录,需要洪泛发送(每个端口均发送)

}

out:

return 0;

drop:

kfree_skb(skb);

goto out;

}

3.4  网桥处理逻辑之 br_forward

void br_forward(const struct net_bridge_port *to, struct sk_buff *skb)
{
    if (should_deliver(to, skb)) {
        __br_forward(to, skb);
        return;
    }

kfree_skb(skb);
}

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;

// Netfilter hook处理
    NF_HOOK(PF_BRIDGE, NF_BR_FORWARD, skb, indev, skb->dev,
            br_forward_finish);
}

void br_flood_forward(struct net_bridge *br, struct sk_buff *skb)

{

br_flood(br, skb, __br_forward);

}

static void br_flood(struct net_bridge *br, struct sk_buff *skb,

void (*__packet_hook)(const struct net_bridge_port *p,

struct sk_buff *skb))

{

struct net_bridge_port *p;

struct net_bridge_port *prev;

prev = NULL;

/* backup multicast address. by HouXB, 07Dec10 */

#ifdef CONFIG_TP_MULTICAST

#define IS_MULTICAST_ADDR(ptr)  ((ptr[0] == 0x01) && (ptr[1] == 0x00) && (ptr[2] == 0x5e) ? 1 : 0)

mac_addr multi_mac_addr;

unsigned char *pmac = multi_mac_addr.addr;

memset(pmac, 0, 6/*ETH_ALEN*/);

if(IS_MULTICAST_ADDR(skb_mac_header(skb)))

{

//backup multicast address

memcpy(pmac, skb_mac_header(skb), 6/*ETH_ALEN*/);

}

#endif

// 遍历所有端口,从每个端口发送一份出去, should_deliver会排除进来的端口

list_for_each_entry_rcu(p, &br->port_list, list) {

if (should_deliver(p, skb)) {

if (prev != NULL) {

struct sk_buff *skb2;

if ((skb2 = skb_clone(skb, GFP_ATOMIC)) == NULL) {

br->dev->stats.tx_dropped++;

kfree_skb(skb);

return;

}

#ifdef CONFIG_TP_MULTICAST

if(IS_MULTICAST_ADDR(pmac))

{

//restore multicast address

memcpy(skb_mac_header(skb), pmac, 6/*ETH_ALEN*/);

}

#endif

__packet_hook(prev, skb2);

}

prev = p;

}

}

if (prev != NULL) {

#ifdef CONFIG_TP_MULTICAST

if(IS_MULTICAST_ADDR(pmac))

{

//restore multicast address

memcpy(skb_mac_header(skb), pmac, 6/*ETH_ALEN*/);

}

#endif

__packet_hook(prev, skb);

return;

}

kfree_skb(skb);

}

3.5  网桥处理逻辑之 br_forward_finish

int br_forward_finish(struct sk_buff *skb)

{

// Netfilter hook: NF_BR_POST_ROUTING

return NF_HOOK(PF_BRIDGE, NF_BR_POST_ROUTING, skb, NULL, skb->dev,

br_dev_queue_push_xmit);

}

int br_dev_queue_push_xmit(struct sk_buff *skb)

{

/* drop mtu oversized packets except gso */

if (packet_length(skb) > skb->dev->mtu && !skb_is_gso(skb))

kfree_skb(skb);

else {

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

if (nf_bridge_maybe_copy_header(skb))

kfree_skb(skb);

else {

skb_push(skb, ETH_HLEN);

dev_queue_xmit(skb); // 进入驱动

}

}

return 0;

}

3.6  网桥处理逻辑之br_pass_frame_up

static void br_pass_frame_up(struct net_bridge *br, struct sk_buff *skb)

{

struct net_device *indev, *brdev = br->dev;

// 数据统计

brdev->stats.rx_packets++;

brdev->stats.rx_bytes += skb->len;

indev = skb->dev;

// 特别注意:此处将skb的dev强制修改为网桥dev

skb->dev = brdev;

// Netfilter hook :NF_BR_LOCAL_IN

NF_HOOK(PF_BRIDGE, NF_BR_LOCAL_IN, skb, indev, NULL,

netif_receive_skb);

}

这段代码非常简单,对net_bridge的数据统计进行更新以后,强制将skb的dev修改为网桥的dev,最后通过NF_HOOK在NF_BR_LOCAL_IN挂接点上调用回了netif_receive_skb方法。

在netif_receive_skb函数中,调用了handle_bridge函数,重新触发了网桥处理流程,现在发往网桥虚拟设备的数据包又回到了netif_receive_skb,那么网桥的处理过程会不会又被调用呢?答案是否定的。回顾网桥入口函数handle_bridge方法,判断是否执行网桥处理流程的判断:

// 如果数据包是环回包,或者数据包的产生设备不属于任何网桥,则不进行网桥处理

if (skb->pkt_type == PACKET_LOOPBACK ||

(port = rcu_dereference(skb->dev->br_port)) == NULL)

return skb;

见上文程序段,br_pass_frame_up函数将skb->dev赋成了br->dev,实际上skb->dev变成了网桥建立的虚拟设备;这个设备是网桥本身而不是桥组的某一端口(它不属于任何网桥设备,因为前面提到过网桥不能添加一个网桥设备做端口),故而在进行网桥处理判断时,不能进入网桥处理流程 ,从而进入上层协议栈处理。

3.7  网桥处理流程小结

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

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

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

3、  进入网桥后,如果网桥的状态为Disable,则将包丢弃不处理;

4、  报文源地址无效(广播,多播,以及00:00:00:00:00:00),丢包;

5、  如果是STP的BPDU包,交给上层协议栈;

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

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

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

2)    单播并且在端口-MAC映射表中能找到端口映射的,只需要网映射端口转发一份即可;

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

最后,再回顾一下网桥数据处理主要函数关系图:

图 网桥处理流程示意图

4.    网桥端口-MAC映射表维护

众所周知,网桥需要维护一个MAC地址-端口映射表,端口是指网桥自身提供的端口,而MAC地址是指与端口相连的另一端主机的MAC地址。当网桥收到一个报文时,先获取它的源MAC,更新数据库,然后读取该报文的目标MAC地址,查找该数据库,如果找到,根据找到条目的端口进行转发;否则会把数据包向除入口端口以外的所有端口转发。

通常网桥端口-MAC映射表又被称为网桥转发数据库或CAM,为了简化叙述,统一使用数据库代替。

4.1  数据库的创建和销毁

数据库使用kmem_cache_create函数进行创建,使用kmem_cache_desctory进行销毁。回顾网桥初始化时,会调用br_fdb_init进行数据库初始化:

int __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);

if (!br_fdb_cache)

return -ENOMEM;

get_random_bytes(&fdb_salt, sizeof(fdb_salt));

return 0;

}

销毁:

void br_fdb_fini(void)

{

kmem_cache_destroy(br_fdb_cache);

}

4.2  数据库更新

当网桥收到一个数据包时,它会获取该数据的源MAC地址,然后对数据库进行更新。如果该MAC地址不在数库中,则创建一个新表项。如果存在,更新它的过期时间。数据库使用hash表的结构方式,便于高效查询。数据库更新函数:

void br_fdb_update(struct net_bridge *br, struct net_bridge_port *source,

const unsigned char *addr)

{

// 使用hash算法,找到skb的MAC所属的表

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;

/* ignore packets unless we are using this port */

if (!(source->state == BR_STATE_LEARNING ||

source->state == BR_STATE_FORWARDING))

return;

fdb = fdb_find(head, addr);

if (likely(fdb)) { // 如果skb的MAC已经存在于数据库中,更新过期时间

/* 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 { // 如果skb的MAC不在数据中,则新建一条记录

spin_lock(&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(&br->hash_lock);

}

}

4.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);

hlist_add_head_rcu(&fdb->hlist, head);

fdb->dst = source;

fdb->is_local = is_local;   // 0

fdb->is_static = is_local;  // 0

fdb->ageing_timer = jiffies;

}

return fdb;

}

4.4  查找数据项

网桥的数据项查找与一般的查找类似,但略有不同。前面提到,如果要更新一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;

}

/* No locking or refcounting, assumes caller has no preempt (rcu_read_lock) */

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;

}

5.    网桥Netfilter

之前我们在专门讲过Linux的Netfilter框架(虽然当时主要是针对IP层),所以这里就不再详细讲解网桥Netfilter的过程。在网桥处理逻辑中,我们已经看到了各个hook点的调用关系,和IP的Netfilter是一致的。关于这部分内容,这里就不重复讲述;这里要讲的,是与IP的Netfilter不同的一些东西。

5.1  Netfilter初始化

直接上代码:

int __init br_netfilter_init(void)

{

int ret;

// 注册hook options

ret = nf_register_hooks(br_nf_ops, ARRAY_SIZE(br_nf_ops));

if (ret < 0)

return ret;

#ifdef CONFIG_SYSCTL

brnf_sysctl_header = register_sysctl_paths(brnf_path, brnf_table);

if (brnf_sysctl_header == NULL) {

printk(KERN_WARNING

"br_netfilter: can't register to sysctl.\n");

nf_unregister_hooks(br_nf_ops, ARRAY_SIZE(br_nf_ops));

return -ENOMEM;

}

#endif

printk(KERN_NOTICE "Bridge firewalling registered\n");

return 0;

}

static struct nf_hook_ops br_nf_ops[] __read_mostly = {

{ .hook = br_nf_pre_routing,

.owner = THIS_MODULE,

.pf = PF_BRIDGE,

.hooknum = NF_BR_PRE_ROUTING,

.priority = NF_BR_PRI_BRNF, }, // 优先级为0

{ .hook = br_nf_local_in,

.owner = THIS_MODULE,

.pf = PF_BRIDGE,

.hooknum = NF_BR_LOCAL_IN,

.priority = NF_BR_PRI_BRNF, },

{ .hook = br_nf_forward_ip,

.owner = THIS_MODULE,

.pf = PF_BRIDGE,

.hooknum = NF_BR_FORWARD,

.priority = NF_BR_PRI_BRNF - 1, }, //优先级为-1, ip高于arp

{ .hook = br_nf_forward_arp,

.owner = THIS_MODULE,

.pf = PF_BRIDGE,

.hooknum = NF_BR_FORWARD,

.priority = NF_BR_PRI_BRNF, },

{ .hook = br_nf_local_out,

.owner = THIS_MODULE,

.pf = PF_BRIDGE,

.hooknum = NF_BR_LOCAL_OUT,

.priority = NF_BR_PRI_FIRST, },

{ .hook = br_nf_post_routing,

.owner = THIS_MODULE,

.pf = PF_BRIDGE,

.hooknum = NF_BR_POST_ROUTING,

.priority = NF_BR_PRI_LAST, },

{ .hook = ip_sabotage_in,

.owner = THIS_MODULE,

.pf = PF_INET,

.hooknum = NF_INET_PRE_ROUTING,

.priority = NF_IP_PRI_FIRST, },

{ .hook = ip_sabotage_in,

.owner = THIS_MODULE,

.pf = PF_INET6,

.hooknum = NF_INET_PRE_ROUTING,

.priority = NF_IP6_PRI_FIRST, },

};

回想IP的Netfilter,每个nf_hook_ops都属于某个特定的表。但Bridge下的Netfilter,在Netfilter初始化的时候,注册了一系列nf_hook_ops,它们不属于任何表,且它们的优先级为0,默认都会被执行。这些hook函数不执行具体的匹配规则,但是会做一些特殊的处理,如调用IP层的hook。这部分功能将在后文讲述Bridge与IP联动的时候讲述。

5.2  Ebtables表介绍

在网桥的Netfilter下,内建了三张表:broute、nat和filter。其中broute主要用于判断某数据包是否应该进入网络层进行处理(跳过网桥处理)。与传统Netfilter下的表注册不一样,broute注册没有注册nf_hook_ops,所以不能通过NF_HOOK()调用;相反其调用方式是直接通过在适当的位置调用其表执行函数。

broute

static struct ebt_entries initial_chain = {

.name      = "BROUTING",

.policy       = EBT_ACCEPT,

};

static struct ebt_replace_kernel initial_table =

{

.name      = "broute",

.valid_hooks  = 1 << NF_BR_BROUTING,

.entries_size = sizeof(struct ebt_entries),

.hook_entry   = {

[NF_BR_BROUTING]  = &initial_chain,

},

.entries   = (char *)&initial_chain,

};

static struct ebt_table broute_table =

{

.name      = "broute",

.table     = &initial_table,

.valid_hooks  = 1 << NF_BR_BROUTING, // 非传统的几处hook点,专门为brout表定义的一个假hook点

.check     = check,

.me    = THIS_MODULE,

};

static int __init ebtable_broute_init(void)

{

int ret;

ret = register_pernet_subsys(&broute_net_ops);

if (ret < 0)

return ret;

/* see br_input.c */

rcu_assign_pointer(br_should_route_hook, ebt_broute)

return 0;

}

static int ebt_broute(struct sk_buff *skb)

{

int ret;

ret = ebt_do_table(NF_BR_BROUTING, skb, skb->dev, NULL,

dev_net(skb->dev)->xt.broute_table);

if (ret == NF_DROP)

return 1; /* route it */

return 0; /* bridge it */

}

Nat

static struct nf_hook_ops ebt_ops_nat[] __read_mostly = {

{

.hook      = ebt_nat_out,

.owner     = THIS_MODULE,

.pf    = PF_BRIDGE,

.hooknum   = NF_BR_LOCAL_OUT,

.priority  = NF_BR_PRI_NAT_DST_OTHER, // 100

},

{

.hook      = ebt_nat_out,

.owner     = THIS_MODULE,

.pf    = PF_BRIDGE,

.hooknum   = NF_BR_POST_ROUTING,

.priority  = NF_BR_PRI_NAT_SRC, //300

},

{

.hook      = ebt_nat_in,

.owner     = THIS_MODULE,

.pf    = PF_BRIDGE,

.hooknum   = NF_BR_PRE_ROUTING,

.priority  = NF_BR_PRI_NAT_DST_BRIDGED, // -300

},

};

static struct ebt_table frame_nat =

{

.name      = "nat",

.table     = &initial_table,

.valid_hooks  = NAT_VALID_HOOKS,

.check     = check,

.me    = THIS_MODULE,

};

static int __init ebtable_nat_init(void)

{

int ret;

ret = register_pernet_subsys(&frame_nat_net_ops);

if (ret < 0)

return ret;

ret = nf_register_hooks(ebt_ops_nat, ARRAY_SIZE(ebt_ops_nat));

if (ret < 0)

unregister_pernet_subsys(&frame_nat_net_ops);

return ret;

}

Nat表的注册和之前讲过Iptables相关表注册是一致的,都是初始化表结构,初始化nf_hook_ops,让后分别注册。值得注意的是,在注册nf_hook_ops的时候,各个nf_hook_ops的优先级是不一样的。优先级定义:

enum nf_br_hook_priorities {

NF_BR_PRI_FIRST = INT_MIN,

NF_BR_PRI_NAT_DST_BRIDGED = -300,

NF_BR_PRI_FILTER_BRIDGED = -200,

NF_BR_PRI_BRNF = 0,

NF_BR_PRI_NAT_DST_OTHER = 100,

NF_BR_PRI_FILTER_OTHER = 200,

NF_BR_PRI_NAT_SRC = 300,

NF_BR_PRI_LAST = INT_MAX,

};

filter

和nat表类似,不赘述。

5.3  Bridge与IP联动

Bridge和IP在透明防火墙中是需要联动的,因为IP层可以做更多的事情。虽然,这些事情也是可以在Bridge中实现的,但是模块化及KISS原则将Bridge从这些复杂的事情中分割出来,仅做它自己该处理的事情;如果需要IP层帮助,则直接调用IP层的hook即可。

下面,我们通过“read the fucking source code”,了解Bridge与IP层到底是如何联动的。

5.3.1    NF_HOOK宏相关

#define NF_HOOK(pf, hook, skb, indev, outdev, okfn) \

NF_HOOK_THRESH(pf, hook, skb, indev, outdev, okfn, INT_MIN) // INT_MIN最小的整数

#define NF_HOOK_THRESH(pf, hook, skb, indev, outdev, okfn, thresh)        \

({int __ret;                                 \

if ((__ret=nf_hook_thresh(pf, hook, (skb), indev, outdev, okfn, thresh, 1)) == 1)\             // NF_ACCEPT == 1

__ret = (okfn)(skb); // 执行hook后的回调函数                           \

__ret;})

unsigned int nf_iterate(struct list_head *head,

struct sk_buff *skb,

unsigned int hook,

const struct net_device *indev,

const struct net_device *outdev,

struct list_head **i,

int (*okfn)(struct sk_buff *),

int hook_thresh)

{

unsigned int verdict;

/*

* The caller must not block between calls to this

* function because of risk of continuing from deleted element.

*/

list_for_each_continue_rcu(*i, head) { // 遍历所有nf_hook_ops

struct nf_hook_ops *elem = (struct nf_hook_ops *)*i;

// 如果设置的thresh值高于nf_hook_ops注册的优先级,则跳过该ops

if (hook_thresh > elem->priority)

continue;

/* Optimization: we don't need to hold module

reference here, since function can't sleep. --RR */

verdict = elem->hook(hook, skb, indev, outdev, okfn); // 执行hook函数

if (verdict != NF_ACCEPT) {

#ifdef CONFIG_NETFILTER_DEBUG

if (unlikely((verdict & NF_VERDICT_MASK)

> NF_MAX_VERDICT)) {

NFDEBUG("Evil return from %p(%u).\n",

elem->hook, hook);

continue;

}

#endif

if (verdict != NF_REPEAT)

return verdict;

*i = (*i)->prev;

}

}

return NF_ACCEPT;

}

通过上述分析,如果通过NF_HOOK()进入,这在该hook点注册的所有nf_hook_ops都会被执行,毕竟NF_HOOK指定了thresh值是最小整数。相反,如果要控制thresh的值,来过滤一部分nf_hook_ops,则需要显示调用NF_HOOK_THRESH(),并指定thresh的值。

5.3.2    NF_BR_PRE_ROUTING 注册举例

进一步分析NF_BR_PRE_ROUTING这个hook点的数据流情况。回顾5.1节和5.2节,网桥在NF_BR_PRE_ROUTING点上,注册了两个nf_hook_ops:

一个是默认的hook处理:

{ .hook = br_nf_pre_routing,

.owner = THIS_MODULE,

.pf = PF_BRIDGE,

.hooknum = NF_BR_PRE_ROUTING,

.priority = NF_BR_PRI_BRNF, }, // 优先级为0

另一个是nat表注册的:

{

.hook      = ebt_nat_in,

.owner     = THIS_MODULE,

.pf    = PF_BRIDGE,

.hooknum   = NF_BR_PRE_ROUTING,

.priority  = NF_BR_PRI_NAT_DST_BRIDGED, //优先级 -300

},

其中,ebt_nat_in是传统Netfilter表处理hook回调函数,通过调用do_tables遍历表规则,对数据处理:

static unsigned int

ebt_nat_in(unsigned int hook, struct sk_buff *skb, const struct net_device *in

, const struct net_device *out, int (*okfn)(struct sk_buff *))

{

return ebt_do_table(hook, skb, in, out, dev_net(in)->xt.frame_nat);

}

但是,br_nf_pre_routing所做的事情却有所不同:

static unsigned int br_nf_pre_routing(unsigned int hook, struct sk_buff *skb,

const struct net_device *in,

const struct net_device *out,

int (*okfn)(struct sk_buff *))

{

struct iphdr *iph;

__u32 len = nf_bridge_encap_header_len(skb);

if (unlikely(!pskb_may_pull(skb, len)))

goto out;

if (skb->protocol == htons(ETH_P_IPV6) || IS_VLAN_IPV6(skb) ||

IS_PPPOE_IPV6(skb)) { // 如果是IPv6的数据,则交由IPv6的hook处理

#ifdef CONFIG_SYSCTL

if (!brnf_call_ip6tables)

return NF_ACCEPT;

#endif

nf_bridge_pull_encap_header_rcsum(skb);

return br_nf_pre_routing_ipv6(hook, skb, in, out, okfn);

}

#ifdef CONFIG_SYSCTL

if (!brnf_call_iptables)

return NF_ACCEPT;

#endif

if (skb->protocol != htons(ETH_P_IP) && !IS_VLAN_IP(skb) &&

!IS_PPPOE_IP(skb)) // 如果不是IP数据,则通过交由后续规则或上层处理

return NF_ACCEPT;

nf_bridge_pull_encap_header_rcsum(skb);

if (!pskb_may_pull(skb, sizeof(struct iphdr)))

goto inhdr_error;

iph = ip_hdr(skb);

if (iph->ihl < 5 || iph->version != 4)

goto inhdr_error;

if (!pskb_may_pull(skb, 4 * iph->ihl))

goto inhdr_error;

iph = ip_hdr(skb);

if (ip_fast_csum((__u8 *) iph, iph->ihl) != 0)

goto inhdr_error;

len = ntohs(iph->tot_len);

if (skb->len < len || len < 4 * iph->ihl)

goto inhdr_error;

pskb_trim_rcsum(skb, len);

nf_bridge_put(skb->nf_bridge);

if (!nf_bridge_alloc(skb))

return NF_DROP;

if (!setup_pre_routing(skb))

return NF_DROP;

store_orig_dstaddr(skb);

NF_HOOK(PF_INET, NF_INET_PRE_ROUTING, skb, skb->dev, NULL,

br_nf_pre_routing_finish);

return NF_STOLEN;

inhdr_error:

//      IP_INC_STATS_BH(IpInHdrErrors);

out:

return NF_DROP;

}

5.3.3    br_nf_pre_routing_finish

static int br_nf_pre_routing_finish(struct sk_buff *skb)

{

struct net_device *dev = skb->dev;

struct iphdr *iph = ip_hdr(skb);

struct nf_bridge_info *nf_bridge = skb->nf_bridge;

struct rtable *rt;

int err;

if (nf_bridge->mask & BRNF_PKT_TYPE) {

skb->pkt_type = PACKET_OTHERHOST;

nf_bridge->mask ^= BRNF_PKT_TYPE;

}

nf_bridge->mask ^= BRNF_NF_BRIDGE_PREROUTING;

if (dnat_took_place(skb)) { // 如果做了DNAT,则交由ip层进行route

if ((err = ip_route_input(skb, iph->daddr, iph->saddr, iph->tos, dev))) {

struct flowi fl = {

.nl_u = {

.ip4_u = {

.daddr = iph->daddr,

.saddr = 0,

.tos = RT_TOS(iph->tos) },

},

.proto = 0,

};

struct in_device *in_dev = in_dev_get(dev);

/* If err equals -EHOSTUNREACH the error is due to a

* martian destination or due to the fact that

* forwarding is disabled. For most martian packets,

* ip_route_output_key() will fail. It won't fail for 2 types of

* martian destinations: loopback destinations and destination

* 0.0.0.0. In both cases the packet will be dropped because the

* destination is the loopback device and not the bridge. */

if (err != -EHOSTUNREACH || !in_dev || IN_DEV_FORWARD(in_dev))

goto free_skb;

if (!ip_route_output_key(dev_net(dev), &rt, &fl)) {

/* - Bridged-and-DNAT'ed traffic doesn't

*   require ip_forwarding. */

if (((struct dst_entry *)rt)->dev == dev) {

skb_dst_set(skb, (struct dst_entry *)rt);

goto bridged_dnat;

}

/* we are sure that forwarding is disabled, so printing

* this message is no problem. Note that the packet could

* still have a martian destination address, in which case

* the packet could be dropped even if forwarding were enabled */

__br_dnat_complain();

dst_release((struct dst_entry *)rt);

}

free_skb:

kfree_skb(skb);

return 0;

} else {

if (skb_dst(skb)->dev == dev) {

bridged_dnat:

/* Tell br_nf_local_out this is a

* bridged frame */

nf_bridge->mask |= BRNF_BRIDGED_DNAT;

skb->dev = nf_bridge->physindev;

nf_bridge_push_encap_header(skb);

NF_HOOK_THRESH(PF_BRIDGE, NF_BR_PRE_ROUTING,

skb, skb->dev, NULL,

br_nf_pre_routing_finish_bridge,

1);

return 0;

}

memcpy(eth_hdr(skb)->h_dest, dev->dev_addr, ETH_ALEN);

skb->pkt_type = PACKET_HOST;

}

} else {

rt = bridge_parent_rtable(nf_bridge->physindev);

if (!rt) {

kfree_skb(skb);

return 0;

}

dst_hold(&rt->u.dst);

skb_dst_set(skb, &rt->u.dst);

}

skb->dev = nf_bridge->physindev;

nf_bridge_push_encap_header(skb);

NF_HOOK_THRESH(PF_BRIDGE, NF_BR_PRE_ROUTING, skb, skb->dev, NULL,

br_handle_frame_finish, 1);

return 0;

}

由5.3.1节的分析知,NF_HOOK_THRESH()中thresh设置为1,将从优先级为1的nf_hook_ops执行;整个过程相当于,在Bridge prerouting hook点上,先执行完优先级小于0的hook操作,然后转入IP层执行所有IP prerouting hook点上的hook操作,最后(依据IP层执行)再转回Bridge prerouting hook点,从优先级为1处继续执行:

图 Bridge 与IP联动示意图

事实上,Bridge与IP的联动过程比较复杂,本文只是示例了其核心的机制,很多细节的控制并没有一一说明。关于Bridge与IP的联动过程中,更多实现细节留给读者自行分析吧。

关于其它网桥hook点上,Bridge与IP的联动本文将略去不再讲述,因为其原理和方法大致是一致的,只是不同的处理细节不一致。关于Bridge与IP的联动全貌,可以参考帖子:

针对我司Linux平台的实现,Bridge与IP的联动关系如下图:

图 Linux平台Bridge 与IP联动

6.    网桥STP

6.1  STP简介

生成树协议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监听和学习的过程。

6.2  STP 实现

6.2.1    stp_proto注册

在早期的版本中,网桥的STP数据包是在网桥处理过程中,按照特定的组播地址进行识别,然后在网桥处理过程中完成相应的数据处理。后来的实现中,将其实现为一种单独的协议,并最终由IEEE802.2协议进行封包传递。

在网桥初始化的时候,为STP注册了协议,并指定其接收数据的函数为br_stp_proto:

static const struct stp_proto br_stp_proto = {

.rcv   = br_stp_rcv, // 接收函数

};

br_init()方法中,注册stp协议:

err = stp_proto_register(&br_stp_proto);

int stp_proto_register(const struct stp_proto *proto)

{

int err = 0;

mutex_lock(&stp_proto_mutex);

if (sap_registered++ == 0) {

sap = llc_sap_open(LLC_SAP_BSPAN, stp_pdu_rcv); //在LLC上注册数据处理函数。LLC_SAP_BSPAN (0x42):Bridge Spanning Tree Proto

if (!sap) {

err = -ENOMEM;

goto out;

}

}

if (is_zero_ether_addr(proto->group_address))

rcu_assign_pointer(stp_proto, proto);

else

rcu_assign_pointer(garp_protos[proto->group_address[5] -

GARP_ADDR_MIN], proto); //按照组播地址下标5的序号,将协议加入到garp_protos中

out:

mutex_unlock(&stp_proto_mutex);

return err;

}

// llc_sap_open 将创建一个llc_sap结构,并加入到LLC协议链表中

struct llc_sap *llc_sap_open(unsigned char lsap,

int (*func)(struct sk_buff *skb,

struct net_device *dev,

struct packet_type *pt,

struct net_device *orig_dev))

{

struct llc_sap *sap = NULL;

write_lock_bh(&llc_sap_list_lock);

if (__llc_sap_find(lsap)) /* SAP already exists */

goto out;

sap = llc_sap_alloc();

if (!sap)

goto out;

sap->laddr.lsap = lsap;  // 上层协议标识

sap->rcv_func = func; // 上层协议入口函数

llc_add_sap(sap);

out:

write_unlock_bh(&llc_sap_list_lock);

return sap;

6.2.2    STP数据流程

既然STP协议是附在LLC之上的,那么还得从LLC的接收说起。LLC在初始化的时候,注册了其数据接收函数为llc_rev():

int llc_rcv(struct sk_buff *skb, struct net_device *dev,

struct packet_type *pt, struct net_device *orig_dev)

{

struct llc_sap *sap;

struct llc_pdu_sn *pdu;

int dest;

int (*rcv)(struct sk_buff *, struct net_device *,

struct packet_type *, struct net_device *);

……

// 获取报文头部

pdu = llc_pdu_sn_hdr(skb);

if (unlikely(!pdu->dsap)) /* NULL DSAP, refer to station */

goto handle_station;

sap = llc_sap_find(pdu->dsap); // 查找所属上层协议

if (unlikely(!sap)) {/* unknown SAP */

dprintk("%s: llc_sap_find(%02X) failed!\n", __func__,

pdu->dsap);

goto drop;

}

/*

* First the upper layer protocols that don't need the full

* LLC functionality

*/

rcv = rcu_dereference(sap->rcv_func);

if (rcv) {

struct sk_buff *cskb = skb_clone(skb, GFP_ATOMIC);

if (cskb)

rcv(cskb, dev, pt, orig_dev); // 执行上层协议的接收函数

}

dest = llc_pdu_type(skb);

if (unlikely(!dest || !llc_type_handlers[dest - 1]))

goto drop_put;

llc_type_handlers[dest - 1](sap, skb);

……

}

在STP协议注册过程中,指定了LLC到STP的入口函数是stp_pdu_rcv:

static int stp_pdu_rcv(struct sk_buff *skb, struct net_device *dev,

struct packet_type *pt, struct net_device *orig_dev)

{

const struct ethhdr *eh = eth_hdr(skb);

const struct llc_pdu_un *pdu = llc_pdu_un_hdr(skb);

const struct stp_proto *proto;

if (pdu->ssap != LLC_SAP_BSPAN ||

pdu->dsap != LLC_SAP_BSPAN ||

pdu->ctrl_1 != LLC_PDU_TYPE_U)

goto err;

if (eh->h_dest[5] >= GARP_ADDR_MIN && eh->h_dest[5] <= GARP_ADDR_MAX) {

// 读取对用的proto

proto = rcu_dereference(garp_protos[eh->h_dest[5] -

GARP_ADDR_MIN]);

if (proto &&

compare_ether_addr(eh->h_dest, proto->group_address))

goto err;

} else

proto = rcu_dereference(stp_proto);

if (!proto)

goto err;

proto->rcv(proto, skb, dev); // 真正STP协议的执行函数

return 0;

……

}

真正STP协议的执行函数br_stp_rcv:

void br_stp_rcv(const struct stp_proto *proto, struct sk_buff *skb,

struct net_device *dev)

{

const unsigned char *dest = eth_hdr(skb)->h_dest;

struct net_bridge_port *p = rcu_dereference(dev->br_port);

struct net_bridge *br;

const unsigned char *buf;

if (!p)

goto err;

if (!pskb_may_pull(skb, 4))

goto err;

/* compare of protocol id and version */

buf = skb->data;

if (buf[0] != 0 || buf[1] != 0 || buf[2] != 0)

goto err;

br = p->br;

spin_lock(&br->lock);

if (br->stp_enabled != BR_KERNEL_STP) //没有开启STP功能

goto out;

if (!(br->dev->flags & IFF_UP))

goto out;

if (p->state == BR_STATE_DISABLED)

goto out;

if (compare_ether_addr(dest, br->group_addr) != 0)

goto out;

buf = skb_pull(skb, 3);

if (buf[0] == BPDU_TYPE_CONFIG) {

struct br_config_bpdu bpdu;

if (!pskb_may_pull(skb, 32))

goto out;

buf = skb->data;

bpdu.topology_change = (buf[1] & 0x01) ? 1 : 0;

bpdu.topology_change_ack = (buf[1] & 0x80) ? 1 : 0;

bpdu.root.prio[0] = buf[2];

bpdu.root.prio[1] = buf[3];

bpdu.root.addr[0] = buf[4];

bpdu.root.addr[1] = buf[5];

bpdu.root.addr[2] = buf[6];

bpdu.root.addr[3] = buf[7];

bpdu.root.addr[4] = buf[8];

bpdu.root.addr[5] = buf[9];

bpdu.root_path_cost =

(buf[10] << 24) |

(buf[11] << 16) |

(buf[12] << 8) |

buf[13];

bpdu.bridge_id.prio[0] = buf[14];

bpdu.bridge_id.prio[1] = buf[15];

bpdu.bridge_id.addr[0] = buf[16];

bpdu.bridge_id.addr[1] = buf[17];

bpdu.bridge_id.addr[2] = buf[18];

bpdu.bridge_id.addr[3] = buf[19];

bpdu.bridge_id.addr[4] = buf[20];

bpdu.bridge_id.addr[5] = buf[21];

bpdu.port_id = (buf[22] << 8) | buf[23];

bpdu.message_age = br_get_ticks(buf+24);

bpdu.max_age = br_get_ticks(buf+26);

bpdu.hello_time = br_get_ticks(buf+28);

bpdu.forward_delay = br_get_ticks(buf+30);

br_received_config_bpdu(p, &bpdu); // 进入网桥配置信息处理

}

else if (buf[0] == BPDU_TYPE_TCN) {

br_received_tcn_bpdu(p); // 进入网络变更信息处理

}

out:

spin_unlock(&br->lock);

err:

kfree_skb(skb);

}

6.2.3    核心实现

6.2.3.1     br_received_config_bpdu

void br_received_config_bpdu(struct net_bridge_port *p, struct br_config_bpdu *bpdu)

{

struct net_bridge *br;

int was_root;

br = p->br;

// 自己是根桥吗?用自己的br_ID和BPDU包中的根ID相比较

was_root = br_is_root_bridge(br);

//比桥BPDU包中的信息(bpdu)和原先的对应的信息(p),如果需要更新,返回1,相同返回0,不需更新返回-1

if (br_supersedes_port_info(p, bpdu)) {

//刷新自己的相关信息

br_record_config_information(p, bpdu);

//进行root_bridge、port的选举

br_configuration_update(br);

//设置端口状态

br_port_state_selection(br);

// 如果因为这个BPDU导致拓朴变化了,如自己以前是根桥,现在不是了,需要发送TCN包,进行通告

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

}

}

// 需要把这个BPDU包继续转发下去

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

}

} else if (br_is_designated_port(p)) { //如果收到这个BPDU包,不是“最优”的,而接收数据包的接口不是根端口,直接将转发出去就可以了

br_reply(p);

}

}

6.2.3.2     br_received_tcn_bpdu

void br_received_tcn_bpdu(struct net_bridge_port *p)

{

if (br_is_designated_port(p)) {

pr_info("%s: received tcn bpdu on port %i(%s)\n",

p->br->dev->name, p->port_no, p->dev->name);

br_topology_change_detection(p->br); // 发送变更通知

br_topology_change_acknowledge(p); // 应答变更

}

}

转自:Linux网桥实现分析-khls27-ChinaUnix博客1.    前言本文结合网络上关于Linux网桥的说明、 Linux平台的代码阅读记录,整理的一篇总结性文档。由于时间仓促,分析可能存在不足之外,望大家见谅和指正。对于接触过Lhttp://blog.chinaunix.net/uid-29547110-id-5038812.html

Linux网桥实现分析相关推荐

  1. Linux 网桥代码分析 六 网桥数据转发函数分析

    对于数据包转发函数,主要是分为两大类:数据转发到指定端口.数据扩散到所有端口. 下面就从这两方面进行分析: 一  数据转发到指定端口 对于数据转发到指定端口的功能,也可以分为两个方面:对入口流量进行的 ...

  2. linux 网桥代码分析之网桥及网桥端口的添加与删除Ⅲ

    目录 1 网桥添加 br_add_bridge() 1.1 申请并初始化 net_device.net_bridge 1.1.1 初始化网桥 br_dev_setup() 1.1.2 网桥操作函数集合 ...

  3. Linux-kernel 网桥代码分析(一)

    本文分析的kernel版本为:2.6.24.4,网桥代码目录为:linux-2.6.24.4/net/bridge. 网桥是kernel网络模块中相于独立的module,读者具有简单的kernel网络 ...

  4. TCP原理之:linux网桥

    TCP原理之:linux网桥 0. 前言 Linux内核提供了对网桥的支持,通过建立网桥设备能够将系统中的多个网络接口相连通,实现网络接口之前的通信.为了便于理解,我们可以将网桥想象成现实中的交换机, ...

  5. linux网桥的简单理解和配置

    linux网桥的简单理解和配置 Linux网桥是linux虚拟网络设备之一.网上很多分析linux网桥的文章,例如代码层面的分析.这里不牵扯复杂的分析和配置,主要是面向虚拟机链接一个用途,小白我的备忘 ...

  6. 【博客455】Linux网桥如何接管attach上来的设备的流量

    Linux网桥如何接管attach上来的设备的流量 先讲讲linux网桥 网桥是在内核中虚拟出来的,可以将主机上真实的物理网卡(如eth0,eth1),或虚拟的网卡(tap0,tap1,vnet0,v ...

  7. Linux内存技术分析(下)

    Linux内存技术分析(下) 五. 内存使用场景 out of memory 的时代过去了吗?no,内存再充足也不可任性使用. 1.内存的使用场景 · page管理 · slab(kmalloc.内存 ...

  8. Linux内存技术分析(上)

    Linux内存技术分析(上) 一.Linux存储器 限于存储介质的存取速率和成本,现代计算机的存储结构呈现为金字塔型.越往塔顶,存取效率越高.但成本也越高,所以容量也就越小.得益于程序访问的局部性原理 ...

  9. linux串口驱动分析

    linux串口驱动分析 硬件资源及描写叙述 s3c2440A 通用异步接收器和发送器(UART)提供了三个独立的异步串行 I/O(SIO)port,每一个port都能够在中断模式或 DMA 模式下操作 ...

最新文章

  1. 使用Python,OpenCV加载图像并将其显示在屏幕上?
  2. PHP 5.6 Apache配置
  3. UA CSC696H 强化学习理论选讲1 强化学习概览
  4. 查被占用的端口号由哪个程序运行
  5. 开发人员需要熟知的常用Linux命令Version、Kernel查看
  6. Django:Admin,Cookie,Session
  7. [VSCode插件推荐] REST Client: 也许是比Postman更好的选择
  8. 【渝粤教育】国家开放大学2018年春季 7407-21T药物治疗学(本) 参考试题
  9. java 枚举类型知识点记录
  10. 【扩展欧几里得】Bzoj 1477:青蛙的约会
  11. 第三季-第26课-网络并发服务器设计
  12. 机器学习系列-tensorflow-03-线性回归Linear Regression
  13. C/C++回调方式系列之一 函数指针和函数回调模式
  14. win系统服务器怎么清理内存,windows服务器内存清理
  15. 线性代数矩阵秩的8大性质、重要定理以及关系
  16. OPPO手机刷机解锁救砖解账户锁方法
  17. 高性能MMORPG通用服务端引擎设计之-基本概念篇
  18. 解决上传SFTPorg.apache.commons.net.MalformedServerReplyException: Could not parse respon
  19. Underscore使用方法
  20. 汽车SoC全生命周期功能+网络安全架构设计

热门文章

  1. 基于距离谱的DE增强蛋白质构象空间采样
  2. 用神经网络,帮助圆滚滚们找对象!
  3. Vue.js 最佳实践清单,照亮你的开发之路
  4. 如何优雅地使用 ES6 箭头函数
  5. 积跬步以至千里,积怠惰以致深渊
  6. vue中px转rem教程
  7. jQuery获取父元素及父节点的方法小结
  8. 人工智能 选择填空题
  9. 九号公司遭红杉小米顺为减持:半年共套现6.5亿元
  10. 【认证服务】验证码、社交登录、分布式session、单点登录