本文分析的kernel版本为:2.6.24.4,网桥代码目录为:linux-2.6.24.4/net/bridge。

网桥是kernel网络模块中相于独立的module,读者具有简单的kernel网络设备驱动开发和kerenl网络协议的基础知识即可。我在2007年就开始接触网桥了,当时有位同事为了测试网桥的功能,还特地查看了网桥的代码,还特意转告我一定要看看这部分的代码,他说比较简单,也很容易看个明白。我当时在做Linux系统的测试工作,还未正式进行开发工作,虽然把代码查看了一翻,但由于经验关系,看得一窍不通。两年过去了,在Linux的开发过程了,接触了kernel的机会也很多。去年3月份,阅读了kernel中网络子模块的部分代码。最近由于工作的需要,阅读了项目中网络驱动部分的代码,就这样,目光转向了Linux网桥代码。遂有写此文之愿。

第一部分: 网桥的报文处理功能分析

1.1  Linux网桥的配置实例
      在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地址必须分配在同一个网段。

1.2  网桥的数据结构
      网桥的核心数据结构主要有:struct net_bridge和struct net_bridge_port这两个结构,当然还有通用的网络设备结构struct net_device。为了简单起见,我们以上述为例子,描述出此时它的静态结构。

每个网桥由struct net_bridge结构来维护,它主要的成员有:port_list,dev和hash。Port_list是一个双向链表,它元素的结构为struct net_bridge_port,每个加入到网桥的设备都在里面占一个元素结点。Dev指针指向net_device变量,它存放网络设备br0的信息。Hash是MAC地址的hash表,MAC地址的hash值为数组结构的下标,每个数组元素为链表,每个元素就是唯一的struct net_bridge_fdb_entry结构,以MAC地址为标识符。

1.3  网桥数据包入口
      网桥是一种2层网络互连设备,而不是一种网络协议。它在协议结构上并没有占有一席之地,因此不能通过向协议栈注册协议的方式来申请网桥数据包的处理。相反,网桥接口(如上述的eth1)的数据包和一般接口(如eth0)在格式上完全是一样的,不同之处是网桥在2层上就对它进行了转了,而一般接口要在3层才能根据路由信息来决定是否要转发,如何转发。那么一个网络接口,在驱动处理完数据包后,怎么才知道该接口分配在一个网桥里面呢?其实很简单,当brctl工具通过ioctl系统调用时,kernel为该添加的设备生成一个bridge_port结构并放到port_list链中,同时将该bridge_port的值赋予设备net_device的br_port指针。因此,要识别接口是否属于某个网桥,只需判断net_device的br_port指针是否不为空即可。

现假设PC1向PC2发送其个数据包,数据首先会由eth1网卡接收,此后网卡向CPU发送接收中断。当CPU执行当前指令后(如果开中断的话),马上跳到网卡的驱动程去。Eth1的网卡驱动首先生成一个skb结构,然后对以太网层进行分析,最后驱动将该skb结构放到当前CPU的输入队列中,唤醒软中断。如果没有其它中断的到来,那么软中断将调用netif_receive_skb函数。代码和分析如下所述:

[linux-2.6.24.4/net/core/dev.c]

int netif_receive_skb(struct sk_buff *skb)   {   
   //当网络设备收到网络数据包时,最终会在软件中断环境里调用此函数   
   //检查该数据包是否有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;   
     }    
   }   
   // 先试着将该数据包让网桥函数来处理,如果该数据包的入口接口确实是网桥接口,   
   // 则按网桥方式来处理,并且handle_bridge返回NULL,表示网桥已处理了。   
   // 如果不是网桥接口的数据包,则不应该让网桥来处理,handle_bridge返回skb,   
   // 后面代码会让协议栈来处理上层协议。   
   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)&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;   
     }   
   }   
}   
int netif_receive_skb(struct sk_buff *skb) {
   //当网络设备收到网络数据包时,最终会在软件中断环境里调用此函数
   //检查该数据包是否有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;
     } 
   }
   // 先试着将该数据包让网桥函数来处理,如果该数据包的入口接口确实是网桥接口,
   // 则按网桥方式来处理,并且handle_bridge返回NULL,表示网桥已处理了。
   // 如果不是网桥接口的数据包,则不应该让网桥来处理,handle_bridge返回skb,
   // 后面代码会让协议栈来处理上层协议。
   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)&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;
     }
   }
}

1.4  handle_bridge处理函数
[linux-2.6.24.4/net/core/dev.c]

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_hook在网桥模块的init函数被初始化为   
  //br_handle_frame   
  return br_handle_frame_hook(port, skb);   
}

1.5  网桥处理逻辑
[linux-2.6.24.4/net/bridge/br_input.c]

struct sk_buff *br_handle_frame(struct net_bridge_port *p, struct sk_buff *skb)   {   
  //所有网桥通信的数据包都会进入到这里,谓之为网桥处理函数   
  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是share的,则拷贝一份   
  skb = skb_share_check(skb, GFP_ATOMIC);   
  if (!skb)   
    return NULL;   
  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;   
    //如果该数据包的目标地址为STP协议的组播地址,并且该网桥启用STP功能,   
    //则,结束该数据包的处理,它将会在第(2)处理得到处理   
    if (p->br->stp_enabled != BR_NO_STP) {   
      if (NF_HOOK(PF_BRIDGE, NF_BR_LOCAL_IN, skb, skb->dev,   
                  NULL, br_handle_local_finish))   
        return NULL;   
      else  
        return skb;   
    }   
    // 如果该包是发往网桥组播的,但该网桥没有启用STP功能,则在下面处理,   
    // 并返回已处理的标识(返回NULL)来通知代码(2)处不需再处理。   
  }   
  switch (p->state) {   
    case BR_STATE_FORWARDING:   
      rhook = rcu_dereference(br_should_route_hook);   
      if (rhook != NULL) {   
        if (rhook(skb))   
          // 如果该接口处于Forwarding状态,并且该报文必需要走L3层   
          // 进行转发,则直接返回,让代码(2)进行处理。   
          // br_should_route_hook钩子函数在ebtable里面设置为ebt_broute函数,   
          //它根据用户的规则来决定该报文是否要能通过L3来转发。   
          return skb;   
        dest = eth_hdr(skb)->h_dest;   
     }   
      /* fall through */  
    case BR_STATE_LEARNING:   
      if (!compare_ether_addr(p->br->dev->dev_addr, 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,表明数据包是要发送该接口,而非是因为打开混杂模式   
        //而接收到的。   
        skb->pkt_type = PACKET_HOST;   
        // 接着由br_handle_frame_finish函数继续处理。   
        NF_HOOK(PF_BRIDGE, NF_BR_PRE_ROUTING, skb, skb->dev, NULL,   
                br_handle_frame_finish);   
      break;   
  default:   
    //其它状态下的端口,不能处理数据包,直接丢弃。   
  drop:   
    kfree_skb(skb);   
  }   
  // 该数据包要么被网桥处理了,要么处理时出错,不需要上层协议处理,   
  // 返回NULL,代码(2)处不会处理该报文。   
  return NULL;   
}   
struct sk_buff *br_handle_frame(struct net_bridge_port *p, struct sk_buff *skb) {
  //所有网桥通信的数据包都会进入到这里,谓之为网桥处理函数
  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是share的,则拷贝一份
  skb = skb_share_check(skb, GFP_ATOMIC);
  if (!skb)
    return NULL;
  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;
    //如果该数据包的目标地址为STP协议的组播地址,并且该网桥启用STP功能,
    //则,结束该数据包的处理,它将会在第(2)处理得到处理
    if (p->br->stp_enabled != BR_NO_STP) {
      if (NF_HOOK(PF_BRIDGE, NF_BR_LOCAL_IN, skb, skb->dev,
                  NULL, br_handle_local_finish))
        return NULL;
      else
        return skb;
    }
    // 如果该包是发往网桥组播的,但该网桥没有启用STP功能,则在下面处理,
    // 并返回已处理的标识(返回NULL)来通知代码(2)处不需再处理。
  }
  switch (p->state) {
    case BR_STATE_FORWARDING:
      rhook = rcu_dereference(br_should_route_hook);
      if (rhook != NULL) {
        if (rhook(skb))
          // 如果该接口处于Forwarding状态,并且该报文必需要走L3层
          // 进行转发,则直接返回,让代码(2)进行处理。
          // br_should_route_hook钩子函数在ebtable里面设置为ebt_broute函数,
          //它根据用户的规则来决定该报文是否要能通过L3来转发。
          return skb;
        dest = eth_hdr(skb)->h_dest;
     }
      /* fall through */
    case BR_STATE_LEARNING:
      if (!compare_ether_addr(p->br->dev->dev_addr, 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,表明数据包是要发送该接口,而非是因为打开混杂模式
        //而接收到的。
        skb->pkt_type = PACKET_HOST;
        // 接着由br_handle_frame_finish函数继续处理。
        NF_HOOK(PF_BRIDGE, NF_BR_PRE_ROUTING, skb, skb->dev, NULL,
                br_handle_frame_finish);
      break;
  default:
    //其它状态下的端口,不能处理数据包,直接丢弃。
  drop:
    kfree_skb(skb);
  }
  // 该数据包要么被网桥处理了,要么处理时出错,不需要上层协议处理,
  // 返回NULL,代码(2)处不会处理该报文。
  return NULL;
}

1.6  br_handle_frame_finish函数
[linux-2.6.24.4/net/bridge/br_input.c]

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;   
  if (!p || p->state == BR_STATE_DISABLED)   
    goto drop;   
  //对所有报的源MAC地址进行学习,这是网桥的特点之一,   
  //通过对源地址的学习来建立MAC地址到端口的映射。   
  br = p->br;   
  br_fdb_update(br, p, eth_hdr(skb)->h_source);   
  if (p->state == BR_STATE_LEARNING)   
    goto drop;   
  // skb2指针表明,有数据要发往本机的网络接口,即p->br->dev接口。   
  skb2 = NULL;   
  // 如果应用程序要dump本机接口的数据,那么该数据包应往主机发一份,   
  // 一个明显的例子就是在用户在运行tcpdump –I br0或类似的程序。   
  if (br->dev->flags & IFF_PROMISC)   
    skb2 = skb;   
  dst = NULL;   
  if (is_multicast_ether_addr(dest)) {   
    // 如果该报文是一个L2多播报文(如arp请求),那么它应该转发到   
    // 该网桥的所有接口。   
    // 这同样是网桥的一个特点,广播和组播报文要转发到它的所有接口。   
    br->statistics.multicast++;   
    skb2 = skb;   
  } else if ((dst = __br_fdb_get(br, dest)) && dst->is_local) {   
    // __br_fdb_get函数先查MAC-端口映射表,这一步是网桥的关键。   
    // 这个报文应从哪个接口转发出去就看它了。   
    // 如果这个报文应发往本机,那么skb置空。不需要再转发了,   
    // 因为发往本机接口从逻辑上来说本身就是一个转发。   
    skb2 = skb;   
    skb = NULL;   
  }   
  if (skb2 == skb)   
    skb2 = skb_clone(skb, GFP_ATOMIC);   
  // skb2不为空,表明要发往本机,br_pass_frame_up函数来完成发往   
  // 本机的工作。   
  if (skb2)   
    br_pass_frame_up(br, skb2);   
  if (skb) {   
    if (dst)   
      // 由br_forward函数从dst所指向的端口将该报文发出去。   
      br_forward(dst->dst, skb);   
    else  
      // 此报文是广播或组播报文,由br_flood_forward函数把报文向所有   
      // 端口转发出去。   
      br_flood_forward(br, skb);   
  }   
  out:   
    return 0;   
  drop:   
    kfree_skb(skb);   
  goto out;   
}   
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;
  if (!p || p->state == BR_STATE_DISABLED)
    goto drop;
  //对所有报的源MAC地址进行学习,这是网桥的特点之一,
  //通过对源地址的学习来建立MAC地址到端口的映射。
  br = p->br;
  br_fdb_update(br, p, eth_hdr(skb)->h_source);
  if (p->state == BR_STATE_LEARNING)
    goto drop;
  // skb2指针表明,有数据要发往本机的网络接口,即p->br->dev接口。
  skb2 = NULL;
  // 如果应用程序要dump本机接口的数据,那么该数据包应往主机发一份,
  // 一个明显的例子就是在用户在运行tcpdump –I br0或类似的程序。
  if (br->dev->flags & IFF_PROMISC)
    skb2 = skb;
  dst = NULL;
  if (is_multicast_ether_addr(dest)) {
    // 如果该报文是一个L2多播报文(如arp请求),那么它应该转发到
    // 该网桥的所有接口。
    // 这同样是网桥的一个特点,广播和组播报文要转发到它的所有接口。
    br->statistics.multicast++;
    skb2 = skb;
  } else if ((dst = __br_fdb_get(br, dest)) && dst->is_local) {
    // __br_fdb_get函数先查MAC-端口映射表,这一步是网桥的关键。
    // 这个报文应从哪个接口转发出去就看它了。
    // 如果这个报文应发往本机,那么skb置空。不需要再转发了,
    // 因为发往本机接口从逻辑上来说本身就是一个转发。
    skb2 = skb;
    skb = NULL;
  }
  if (skb2 == skb)
    skb2 = skb_clone(skb, GFP_ATOMIC);
  // skb2不为空,表明要发往本机,br_pass_frame_up函数来完成发往
  // 本机的工作。
  if (skb2)
    br_pass_frame_up(br, skb2);
  if (skb) {
    if (dst)
      // 由br_forward函数从dst所指向的端口将该报文发出去。
      br_forward(dst->dst, skb);
    else
      // 此报文是广播或组播报文,由br_flood_forward函数把报文向所有
      // 端口转发出去。
      br_flood_forward(br, skb);
  }
  out:
    return 0;
  drop:
    kfree_skb(skb);
  goto out;
}

1.7  通过br_pass_frame_up函数将报文发往本机接口。
[linux-2.6.24.4/net/bridge/br_input.c]

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;   
  //br->dev是一个虚拟的网络设备,这是网桥局域网通往本机的必经之道。   
  //请注意,br->dev是本机和网桥相连的接口。当报文经网桥处理后,发现   
  //该报文应该发往本机,那就使用netif_receive_skb函数将该报文向上层   
  //协议投递。并且要将skb->dev设置为本机接口即br->dev,并且所有数据在   
  //它的入口接口indev的驱动中已处理完毕,因此可直接通知上层协议来处理。   
  NF_HOOK(PF_BRIDGE, NF_BR_LOCAL_IN, skb, indev, NULL,   
          netif_receive_skb);   
}   
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;
  //br->dev是一个虚拟的网络设备,这是网桥局域网通往本机的必经之道。
  //请注意,br->dev是本机和网桥相连的接口。当报文经网桥处理后,发现
  //该报文应该发往本机,那就使用netif_receive_skb函数将该报文向上层
  //协议投递。并且要将skb->dev设置为本机接口即br->dev,并且所有数据在
  //它的入口接口indev的驱动中已处理完毕,因此可直接通知上层协议来处理。
  NF_HOOK(PF_BRIDGE, NF_BR_LOCAL_IN, skb, indev, NULL,
          netif_receive_skb);
}

1.8  通过br_forward函数将报文从另一个端口转发出去
view plaincopy to clipboardprint?
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);   
}   
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);
}

Should_deliver函数来测试是否应将该包转发出去,它由出口端的状态和报文的入口端口信息来决定,它的定义如下:

[linux-2.6.24.4/net/bridge/br_forward.c]

static inline int should_deliver(const struct net_bridge_port *p,   
                                 const struct sk_buff *skb)   {   
  //1) 入口端口和出口端口不能相同,如果是相同的话,那么源主机和目标   
  // 主机在同一端口的子网段中,也即源主机和目标主机在同一广播域里面,   
  // 目标主机和网桥都会同时收到该报文,因此网桥无需多此一举。   
  //2) 如果出口端口的状态不是Forwarding,则不能转发出去。如果一个网桥   
  // 没有启用STP功能,并且网络接口的状态为UP,那么它网桥端口的状态   
  // 为Forwarding。如果启用STP,每个端口都有一个严格的状态,规定那些   
  // 端口在什么情况下才能成为Forwarding状态,否则容易造成环路,产生   
  // 网络风暴。   
  return (skb->dev != p->dev && p->state == BR_STATE_FORWARDING);   
}   
static inline int should_deliver(const struct net_bridge_port *p,
                                 const struct sk_buff *skb) {
  //1) 入口端口和出口端口不能相同,如果是相同的话,那么源主机和目标
  // 主机在同一端口的子网段中,也即源主机和目标主机在同一广播域里面,
  // 目标主机和网桥都会同时收到该报文,因此网桥无需多此一举。
  //2) 如果出口端口的状态不是Forwarding,则不能转发出去。如果一个网桥
  // 没有启用STP功能,并且网络接口的状态为UP,那么它网桥端口的状态
  // 为Forwarding。如果启用STP,每个端口都有一个严格的状态,规定那些
  // 端口在什么情况下才能成为Forwarding状态,否则容易造成环路,产生
  // 网络风暴。
  return (skb->dev != p->dev && p->state == BR_STATE_FORWARDING);
}

若报文的确需要转发,因为目标主机是在另一个子网段,而且没有其它网相连的网格端口可抵达该子网段(这里考虑到启用STP功能,如果搞不清楚可略过)。将调用__br_forward函数实施这一转发功能。

[linux-2.6.24.4/net/bridge/br_forward.c]

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;   
  skb_forward_csum(skb);   
  // 通过br_forward_finish函数最终完成转发功能   
  NF_HOOK(PF_BRIDGE, NF_BR_FORWARD, skb, indev, skb->dev,   
          br_forward_finish);   
}   
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;
  skb_forward_csum(skb);
  // 通过br_forward_finish函数最终完成转发功能
  NF_HOOK(PF_BRIDGE, NF_BR_FORWARD, skb, indev, skb->dev,
          br_forward_finish);
}

[linux-2.6.24.4/net/bridge/br_forward.c]

int br_forward_finish(struct sk_buff *skb)    {   
  return NF_HOOK(PF_BRIDGE, NF_BR_POST_ROUTING, skb, NULL, skb->dev, br_dev_queue_push_xmit);   
}   
   
int br_forward_finish(struct sk_buff *skb) {
  return NF_HOOK(PF_BRIDGE, NF_BR_POST_ROUTING, skb, NULL, skb->dev, br_dev_queue_push_xmit);
}

Br_dev_queue_push_xmit在调用dev_queue_xmit函数前做些必要的检查工作。例如,报文的长度比出口端口的MTU还大,则丢掉该报文。

[linux-2.6.24.4/net/bridge/br_forward.c]

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 {   
      // 网桥在处理数据包里,只需拆包来获得目标MAC地址,而不需要   
      // 更改数据包的任何内容。但在入口网卡的驱动中已将以太网头部   
      // 剥掉,现在需要将它套上。Skb_push函数实现这一功能。   
      skb_push(skb, ETH_HLEN);   
      // 放到网卡输出队列里,该网卡驱动将它送出去。   
      dev_queue_xmit(skb);   
    }   
  }   
  return 0;   
}

1.9  br_flood_forward 函数把报文转发到网桥所有出口端口
[linux-2.6.24.4/net/bridge/br_forward.c]

void br_flood_forward(struct net_bridge *br, struct sk_buff *skb)   {   
  br_flood(br, skb, __br_forward);   
}  
void br_flood_forward(struct net_bridge *br, struct sk_buff *skb) {
  br_flood(br, skb, __br_forward);
}

__br_forward代码已在前面分析过,它从指定的出口端口转发该报文。

而br_flood函数,把__br_forward函数作为回调函数,依次遍网桥的所有出口端,调用__br_forward函数将该报文转发出去。一个广播报文从某一端口进入,应该其余的端口都应该转发出去,但入口端口就不需要了。下面的代码看似把报文从所有端口都转发一份,其实不然,should_deliver会阻止这样的事情发生。

[linux-2.6.24.4/net/bridge/br_forward.c]

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;   
  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->statistics.tx_dropped++;   
          kfree_skb(skb);   
          return;   
        }   
        __packet_hook(prev, skb2);   
      }   
      prev = p;   
    }   
  }   
  if (prev != NULL) {   
    __packet_hook(prev, skb);   
    return;   
  }   
  kfree_skb(skb);   
}   
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;
  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->statistics.tx_dropped++;
          kfree_skb(skb);
          return;
        }
        __packet_hook(prev, skb2);
      }
      prev = p;
    }
  }
  if (prev != NULL) {
    __packet_hook(prev, skb);
    return;
  }
  kfree_skb(skb);
}

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

2.1 数据库的创建和销毁
     数据库使用kmem_cache_create函数进行创建,使用kmem_cache_desctory进行销毁。

[linux-2.6.24.4/net/bridge/br_fdb.c]

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

[linux-2.6.24.4/net/bridge/br_fdb.c]

void br_fdb_fini(void)   {   
  kmem_cache_destroy(br_fdb_cache);   
}   
void br_fdb_fini(void) {
  kmem_cache_destroy(br_fdb_cache);
}

2.2 数据库更新
      当网桥收到一个数据包时,它会获取该数据的源MAC地址,然后对数据库进行更新。如果该MAC地址不在数库中,则创新一个数据项。如果存在,更新它的年龄。数据库使用hash表的结构方式,便于高效查询。下面是hash功能代码的分析:

[linux-2.6.24.4/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;   
  /* 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)) {   
    // 接收到的MAC地址竟然是自己端口的MAC地址,确实不应该有这样的   
    // 事情发生。   
    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 {   
      // 收到该MAC地址的报文,更新它的年龄。   
      fdb->dst = source;   
      fdb->ageing_timer = jiffies;   
    }   
  } else {   
    spin_lock(&br->hash_lock);   
    if (!fdb_find(head, addr))   
      // 这是新的MAC地址,在数据库里为之创建一个数据项。   
      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);   
  }   
}   
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;
  /* 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)) {
    // 接收到的MAC地址竟然是自己端口的MAC地址,确实不应该有这样的
    // 事情发生。
    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 {
      // 收到该MAC地址的报文,更新它的年龄。
      fdb->dst = source;
      fdb->ageing_timer = jiffies;
    }
  } else {
    spin_lock(&br->hash_lock);
    if (!fdb_find(head, addr))
      // 这是新的MAC地址,在数据库里为之创建一个数据项。
      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);
  }
}

2.3 创建数据项
     在更新函数里面已为某一MAC找到了它所属于的Hash链表,因此,创建函数只需要在该链上添加一个数据项即可。

[linux-2.6.24.4/net/bridge/br_fdb.c]

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

2.4 查找数据项
      网桥的数据项查找与一般的查找类似,但略有不同。前面提到,如果要更新一MAC地址,不管该地址是否已经过期了,只需遍历该MAC地址对应的Hash链表,然后更新年龄,此时它肯定不过期了。但网桥要转发数据时,除了要找到该目标MAC的出口端口外,还要判断该记录是否过期了。因此,数据项的查找有两种,一种用于更新,另一用于转发。

[linux-2.6.24.4/net/bridge/br_fdb.c]

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

 [linux-2.6.24.4/net/bridge/br_fdb.c]

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

除了__br_fdb_get函数多调用了has_expired外,其余无一不同。Has_expired函数来决定该数据项是否是过期的,代码如下:

[linux-2.6.24.4/net/bridge/br_fdb.c]

static inline int has_expired(const struct net_bridge *br,   
                              const struct net_bridge_fdb_entry *fdb)   {   
  // 如果该数据项是静态的,即不是学习过来的,它永远不会过期。因为它就是   
  // 网桥自己端口的地址。   
  // 如果现在时间,与该数据项的最近更新时间和可保留时间之和相等,   
  // 或者更早,则为过期。   
  return !fdb->is_static   
         && time_before_eq(fdb->ageing_timer + hold_time(br), jiffies);   
}   
// 数据项的可保留时间根据拓扑结构是否改变来决定,改变则为forward_delay,   
// 否则为ageing_time。   
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) {
  //

Linux-kernel 网桥代码分析(一)相关推荐

  1. Linux内核汇编代码分析

    Linux内核汇编代码分析 1.vmlinux.lds.S文件分析 1.2 vmlinux.lds.S文件总体框架 1.3 代码段 1.4 只读数据段 1.5 init段 1.6 数据段 1.7 未初 ...

  2. Linux内核学习(六):linux kernel的Kconfig分析

    Linux内核学习(六):linux kernel的Kconfig分析 前面我们知道了makefile文件,makefile文件会结合配置文件.config来进行操作.这里就再来看看生成内核.conf ...

  3. Linux Kernel 远程代码执行漏洞(CVE-2022-47939)

    Linux Kernel 远程代码执行漏洞(CVE-2022-47939) CVE-2022-47939 据Security Affairs消息,近期披露的一个严重 Linux 内核漏洞会影响 SMB ...

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

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

  5. Linux Kernel Oops异常分析

    0.linux内核异常常用分析方法 异常地址是否在0附近,确认是否是空指针解引用问题 异常地址是否在iomem映射区,确认是否是设备访问总线异常问题,如PCI异常导致的地址访问异常 异常地址是否在st ...

  6. linux内核oops错误码说明,Linux Kernel Oops异常分析

    0.linux内核异常常用分析方法 异常地址是否在0附近,确认是否是空指针解引用问题 异常地址是否在iomem映射区,确认是否是设备访问总线异常问题,如PCI异常导致的地址访问异常 异常地址是否在st ...

  7. Linux查看历史信息代码,使用 GIT 获得Linux Kernel的代码并查看,追踪历史记录

    Linux kernel  的官方 GIT地址是: 可以从这个地址拿到 kernel 的 代码仓库. 1. 拿代码仓库 git clone git://git.kernel.org/pub/scm/l ...

  8. 如何为linux kernel贡献代码

    和一般github项目可以直接提交pr不同,linux kernel项目庞大,管理繁琐,如果我们想要对linux kernel提出改进,并希望最终改进能进入主分支,则需要严格按照提交patch的流程 ...

  9. Fuchsia X86 kernel启动代码分析

    Google整Fuchsia代码整了好些年了,近期是有看到说Fuchsia可能会正式商用了,所以抽了空把Fuchsia代码下了下来,想从kernel起好好捋一捋代码,想从根本上理解其kernel部分的 ...

最新文章

  1. Util应用程序框架公共操作类(八):Lambda表达式公共操作类(二)
  2. Linux 高级I/O之poll函数及简单服务器客户端编程
  3. CodeForces - 1312E Array Shrinking(区间dp)
  4. gitlab 将管理员权限移交给ldap账户_CDPDC中Atlas集成FreeIPA的LDAP认证
  5. ble芯片 全称_蓝牙芯片都有哪些厂商?一文解答
  6. xp做打印服务器 找不到驱动,XP系统安装打印机驱动提示找不到指定的模块怎么办...
  7. 系统最小的服务最小的权限最大的安全。
  8. VC2010 MFC文档类菜单快捷键无法加载问题
  9. 计算机二级office函数日期,Excel函数-日期和文本函数-计算机二级Office
  10. try catch中getRequestDispatcher跳转
  11. ThinkJS框架入门详细教程(二)新手入门项目
  12. kubernetes12(kubernetes的储存)
  13. 使用git小乌龟拉取,更新,上传资料文档
  14. HTTP的REST服务简介
  15. QQ桌球瞄准器开发(4)透明度、颜色、线宽与母球大小
  16. 5位数的数字黑洞是多少_揭秘数字黑洞6174
  17. 修改idea64.exe.vmoptions导致双击打不开idea的解决办法
  18. IDEA如何设置author头注解
  19. Python的scrapy之爬取6毛小说网
  20. 微信支付 V3 提示验签失败

热门文章

  1. mysql 表锁的概念_MySQL 锁的一些简单概念
  2. Java 算法 身份证排序
  3. python基于dict、defaultdict、Counter的累加器
  4. docker安装ping命令
  5. react 16 对外暴露function_【第 25 期】React 架构的演变 从同步到异步(一)
  6. 基于Ajax+div的“左边菜单、右边内容”页面效果实现
  7. POJ 1426 Find The Multiple BFS
  8. Meteor 加入账户系统
  9. JDBC学习笔记(1)
  10. rpm方式安装MySQL-5.6