前言

Linux bridge 是一个工作在 Layer 2 的虚拟设备,它本身不能接收或传输任何东西,除非你将一个或多个真实设备绑定到它上面。

bridge 主要由四个部分组成:

  • 网络接口:用于将 bridge 中的流量转发给网络中的其他主机。
  • 控制平面(Control Plane):用于运行生成树协议(STP),该协议计算最小生成树,以防止环路使网络崩溃。
  • 转发平面(Forwarding Plane):用于处理端口输入的输入帧,通过基于 MAC learning database 进行转发决策,将输入帧转发到网络端口。
  • MAC 地址表(MAC learning database):用于识别局域网中的主机位置。

对于每个单播 mac 地址,网桥都会维护一个 MAC 地址表,以根据 MAC 地址来决定要转发的端口,如果找不到给定 mac 地址的条目,它将向所有端口广播该帧,除了它收到了帧。

bridge 主要由三个配置子系统执行:

  • ioctl:该接口用于创建/销毁 bridge,以及向/从 bridge 添加/删除接口。
  • sysfs:管理 bridge 和端口的特定参数。
  • netlink:使用 AF_NETLINK address family 的基于异步队列的通信也可以用于于 bridge 进行交互。

在此文章中,只讨论 ioctl。

实验

创建一个 bridge

bridge-utils 提供的 brctl 工具可以看出,可以使用 ioctl 的命令 SIOCBRADDBR 创建网桥。

  1. root@henryxzx:~# strace brctl addbr br10
  2. execve("/sbin/brctl", ["brctl", "addbr", "br10"], 0x7fffd8e25720 /* 24 vars */) = 0
  3. ...
  4. ioctl(3, SIOCBRADDBR, "br10") = 0
  5. exit_group(0) = ?
  6. +++ exited with 0 +++

此时没有设备可以处理 ioctl 命令,因此 ioctl 命令由根方法处理: br_ioctl_deviceless_stub,依次调用br_add_bridge。此方法调用 alloc_netdev ,这是一个最终调用[alloc_netdev_mqs](https://elixir.free-electrons.com/linux/v3.10.105/source/net/core/dev.c#L5660)的宏。

br_ioctl_deviceless_stub
  |- br_add_bridge
      |- alloc_netdev
           |- alloc_netdev_mqs  // creates the network device
              |- br_dev_setup // sets br_dev_ioctl handler

alloc_netdev 使用 br_dev_setup 初始化新的设备。这还包括设置特定于网桥的 ioctl handler. 我们查看源码,它将处理 ioctl 命令以添加/删除接口。

int br_dev_ioctl(struct net_device *dev, struct ifreq *rq, int cmd) {
    ...
    switch(cmd) {
    case SIOCBRADDIF:
    case SIOCBRDELIF:
        return add_del_if(br, rq->ifr_ifindex, cmd == SIOCBRADDIF);
    ...
    }
    ..
}

添加一个网络接口

br_dev_ioctl 可以看出,可以通过 ioctl 的命令 SIOCBRADDIF 在 bridge 中添加一个接口,通过以下操作确认:

root@henryxzx:~# strace brctl addif br10 veth0
execve("/sbin/brctl", ["brctl", "addif", "br10", "veth0"], 0x7ffd13545e08 /* 24 vars */) = 0
...
ioctl(4, SIOCGIFINDEX, {ifr_name="veth0", }) = 0
close(4)                                = 0
ioctl(3, SIOCBRADDIF)                   = 0
exit_group(0)                           = ?
+++ exited with 0 +++

br_add_if
br_add_if 方法通过分配新的 net_bridge_port 对象来创建/设置 bridge 的新接口。

/* Truncated version */
int br_add_if(struct net_bridge *br, struct net_device *dev)
{
    struct net_bridge_port *p;
    /* Don't allow bridging non-ethernet like devices */
    ...
    /* No bridging of bridges */
    ...
    p = new_nbp(br, dev);
    ...
    call_netdevice_notifiers(NETDEV_JOIN, dev);
    err = dev_set_promiscuity(dev, 1);
    err = kobject_init_and_add(&p->kobj, &brport_ktype, &(dev->dev.kobj),
                   SYSFS_BRIDGE_PORT_ATTR);
    ...
    err = netdev_rx_handler_register(dev, br_handle_frame, p);
    /* Make entry in forwarding database*/
    if (br_fdb_insert(br, p, dev->dev_addr, 0))
        ...
    ...
}

brctl_add_if 应该注意到的一些东西:

  • 因为 bridge 是 Layer 2 设备,所以只能将以太网的设备添加到网桥。
  • 无法将 bridge 添加进另一个 bridge。(禁止套娃
  • 加入网桥的新接口设置为 混杂模式 :dev_set_promiscuity(dev, 1)

可以从内核日志中确认:

root@henryxzx:~# grep -r 'promiscuous' /var/log/kern.log
Jul 26 06:25:37 henryxzx kernel: [270665.510625] device veth0 entered promiscuous mode

最后,br_add_if 方法调用 netdev_rx_handler_register, 将接口 rx_handler 设置为 br_handle_frame
此方法执行完成后,bridge 中将拥有一个接口。


以太帧处理

image.png
对于帧的处理会在 __netif_receive_skb 中开始,该代码调用接口的 rx_handler ,当接口加入 bridge 时,同时将其设置为 br_handle_frame
br_handle_frame 进行最开始的处理,任何前缀为 01-80-C2-00-00 都是控制报文,需要特殊处理,通过 br_handle_frame的注释中看出:

    /*
    * See IEEE 802.1D Table 7-10 Reserved addresses
    *
    * Assignment                 Value
    * Bridge Group Address        01-80-C2-00-00-00
    * (MAC Control) 802.3        01-80-C2-00-00-01
    * (Link Aggregation) 802.3            01-80-C2-00-00-02
    * 802.1X PAE address        01-80-C2-00-00-03
    *
    * 802.1AB LLDP         01-80-C2-00-00-0E
    *
    * Others reserved for future standardization
    */

这是 br_handle_frame_finish 的梗概:

/* note: already called with rcu_read_lock */
int br_handle_frame_finish(struct sk_buff *skb)
{
    struct net_bridge_port *p = br_port_get_rcu(skb->dev);
    ...
    /* insert into forwarding database after filtering to avoid spoofing */
    br = p->br;
    br_fdb_update(br, p, eth_hdr(skb)->h_source, vid);
    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_broadcast_ether_addr(dest))
        skb2 = skb;
    else if (is_multicast_ether_addr(dest)) {
        ...
    } else if ((dst = __br_fdb_get(br, dest, vid)) &&
            dst->is_local) {
        skb2 = skb;
        /* Do not forward the packet since it's local. */
        skb = NULL;
    }
    if (skb) {
        if (dst) {
            br_forward(dst->dst, skb, skb2);
        } else
            br_flood_forward(br, skb, skb2);
    }
    if (skb2)
        return br_pass_frame_up(skb2);
out:
    return 0;
    ...
}
  • Forwarding database 中的条目将更新为帧的来源。
  • 如果目标地址是多播地址,并且如果禁用了多播,则将丢弃该数据包。否则使用 br_multicast_rcv接收到消息
  • 如果混杂模式打开,则将从本地传送数据包。
  • 对于单播地址,尝试使用 forwarding database (brfdb_get) 来确定端口。
  • 如果目的地是本地,则 skb 设置为 null,将不转发数据包。
  • 如果目的地不是本地的,则根据我们是否在转发数据库中找到条目,将帧转发( br_forward )或泛洪到所有端口( br_flood_forward )。
  • 之后,如果需要,则将数据包本地传递(br_pass_frame_up)( 基于当前主机为目标主机或网络设备处于混杂模式)。

br_forward 可以克隆然后传递 (如果这在本地转发,则调用 deliver_clone), 或直接将消息转发到目的接口调用 __br_forward.

bt_flood_forward 通过遍历 br_flood 方法中的列表在每个接口上转发帧。

参考

Anatomy of a Linux bridge
Understanding Linux Networking Internals - Christian Benvenuti
Linux kernel v3.10.105 source code
Linux Bridge - how it works