前言
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 创建网桥。
root@henryxzx:~# strace brctl addbr br10execve("/sbin/brctl", ["brctl", "addbr", "br10"], 0x7fffd8e25720 /* 24 vars */) = 0...ioctl(3, SIOCBRADDBR, "br10") = 0exit_group(0) = ?+++ 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_ifbr_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 中将拥有一个接口。
以太帧处理

对于帧的处理会在 __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
