Linux Bridge - Part 2

概览

在前一篇文章中,我描述了Linux 网桥(bridge)的配置,并展示了一个实验,其中使用Wireshark来分析流量。在本文中,我将讨论当创建一个网桥时会发生什么,以及Linux 网桥(bridge)的工作原理。

与网桥(bridge)相关的源代码可以在这里找到。

网桥设备

摘自《深入理解LINUX网络内幕》:

在Linux中,网桥(bridge)是一个虚拟设备。因此,除非你将其与一个或多个实际设备绑定,否则它无法接收或传输任何数据。

阅读上一篇文章后,有人问我,既然网桥(bridge)是一个第2层设备,为什么我们在运行ifconfig br0时会看到一个IP地址与之相关联呢?答案是,Linux的实现将网桥(bridge)和路由器(router)的功能结合在了一起。我们知道,网桥(bridge)有一个上行链路,它可以连接到路由器。而在Linux网桥(bridge)中,这种连接被内置于内核内部。这个路由器实际上就是网桥(bridge)所绑定的实际设备。我们稍后会详细讨论这部分内容。为了理解网桥(bridge),我们可以简单地将这个IP地址视为网桥(bridge)的默认网关。如果我们想要一个私有网桥(bridge)的话,这个IP地址并不是必需的。

注:在Linux网络模型中,网桥(bridge)虽然本质上是二层设备,但其设计允许它拥有IP地址并执行层三的功能,主要是因为Linux将网桥(bridge)与路由器的部分功能进行了融合。这样做的目的是为了提供更加灵活的网络配置能力。然而,对于纯粹的层二桥接需求,这个IP地址可以忽略,因为桥接器的主要任务是在同一广播域内的设备之间转发数据包。

网桥数据结构

net_bridge 结构体的定义可以在这里找到。

下面仅列出了一些重要的字段:

struct net_bridge
{spinlock_t          lock;struct list_head    port_list;struct net_device   *dev;spinlock_t          hash_lock;struct hlist_head   hash[BR_HASH_SIZE];bridge_id           bridge_id;...
}

port_list 是桥接器所拥有的端口列表。每个桥接器最多可以拥有 BR_MAX_PORTS(1024)个端口。dev 是指向表示桥接设备的 net_device 结构的指针。hash 是一个具有 BR_HASH_SIZE(256)个条目的转发哈希表。以便于快速查找和转发数据包。

创建网桥设备

可以通过命令 brctl addbr br0 来创建一个网桥。最终,这会调用带有请求 SIOCBRADDBRioctl 函数。

通过strace跟踪创建网桥的系统调用:

# strace brctl addbr br0
execve("/sbin/brctl", ["brctl", "addbr", "br0"], [/* 17 vars */]) = 0
...
ioctl(3, SIOCBRADDBR, "br0")            = 0
...

如果我们搜索 SIOCBRADDBR 在源代码中的实现,我们会发现它是由 br_ioctl_deviceless_stub 函数处理的,同时处理的还有另外三个请求 - SIOCGIFBR, SIOCSIFBRSIOCBRDELBR

int br_ioctl_deviceless_stub(struct net *net, unsigned int cmd, void __user
*uarg) {switch (cmd) {case SIOCGIFBR:case SIOCSIFBR:...case SIOCBRADDBR:case SIOCBRDELBR:...}...
}

该函数在初始化函数 br_init 中进行注册。

int __init br_init(void) {
...
brioctl_set(br_ioctl_deviceless_stub);
...
}

br_ioctl_deviceless_stub 调用 br_add_bridge,后者分配一个表示网桥的 net_device 结构,并使用 register_netdev 将此设备添加到内核的接口列表中。

int br_add_bridge(struct net *net, const char *name)
{struct net_device *dev;int res;dev = alloc_netdev(sizeof(struct net_bridge), name, NET_NAME_UNKNOWN,br_dev_setup);...res = register_netdev(dev);...return res;
}

alloc_netdev 被定义为一个宏,它是 alloc_netdev_mqs 的简化版本,而后者实际上是一种通用方法,用于分配 net_device 结构体,并非特指网桥。网桥作为私有数据存储在 net_device 结构中。(私有数据是指附加在 net_device 结构后的内存段。)alloc_netdev_mqs 所接受的回调函数用于设置这部分私有数据。在网桥的情况下,这个回调函数是 br_dev_setup

struct net_device *alloc_netdev_mqs(..., void (*setup)(struct net_device *), ...) {struct net_device *dev;size_t alloc_size;...alloc_size = sizeof(struct net_device);if (sizeof_priv) {/* ensure 32-byte alignment of private area */alloc_size = ALIGN(alloc_size, NETDEV_ALIGN);alloc_size += sizeof_priv;}/* ensure 32-byte alignment of whole construct */alloc_size += NETDEV_ALIGN - 1;p = kzalloc(alloc_size, GFP_KERNEL | __GFP_NOWARN | __GFP_REPEAT);...dev = PTR_ALIGN(p, NETDEV_ALIGN);...setup(dev);...return dev
}

br_dev_setup 设置 net_device 的私有数据区域。它将这一段内存转换为 net_bridge 类型,并初始化每一个字段。

void br_dev_setup(struct net_device *dev) {struct net_bridge *br = netdev_priv(dev);...dev->priv_flags = IFF_EBRIDGE;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;...
}

总结一下,创建一个网桥的调用栈如下:

ioctl(3, SIOCBRADDBR, "br0")|- br_ioctl_deviceless_stub|- br_add_bridge|- alloc_netdev|- br_dev_setup|- register_netdev

经过这一步骤后,我们就有了一个 Linux 网桥设备。但是,此时还没有任何接口绑定到它上面,这意味着网桥还不能传输或接收任何数据。

添加接口

创建网桥之后,我们可以向它添加接口(端口)。尽管《深入理解LINUX网络内幕》一书中提到网桥必须绑定到一个“真实设备”,我认为这句话只对了一半。确实,一个网桥设备必须绑定到一个网络接口。然而,这并不一定意味着它必须绑定到一个“真实设备”,也就是说,它不必是实际物理网卡的接口。即使是 tap 接口,也可以绑定到网桥上使其正常工作。

用于绑定接口的命令是 brctl addif br0 tap0。这也被称为“从属(enslave)”,即我们让 tap0 成为 br0 的从属接口。

使用strace跟踪网桥添加端口的系统调用:

# strace brctl addif br0 tap0
execve("/sbin/brctl", ["brctl", "addif", "br0", "tap0"], [/* 17 vars */]) = 0
brk(NULL)                               = 0xf28000
...
ioctl(4, SIOCGIFINDEX, {ifr_name="tap0", }) = 0
close(4)                                = 0
ioctl(3, SIOCBRADDIF)                   = 0

它发出两个 ioctl 请求 - SIOCGIFINDEXSIOCBRADDIF。第一个用于查询 tap0 接口的索引,第二个用于将 tap0 接口添加到网桥。我们只关注第二个。

SIOCBRADDIFbr_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 SIOCBRADDIF:case SIOCBRDELIF:return add_del_if(br, rq->ifr_ifindex, cmd == SIOCBRADDIF);}...
}

add_del_ifbr_add_ifbr_del_if 的包装器。在添加接口的情况下,会调用 br_add_if。以下是 br_add_if 中的一些重要步骤。

int br_add_if(struct net_bridge *br, struct net_device *dev) {struct net_bridge_port *p;... (Validation)p = new_nbp(br, dev);...err = netdev_rx_handler_register(dev, br_handle_frame, p);...err = netdev_master_upper_dev_link(dev, br->dev);...list_add_rcu(&p->list, &br->port_list);nbp_update_port_count(br);...if (br_fdb_insert(br, p, dev->dev_addr, 0))netdev_err(dev, "failed insert local address bridge forwarding table\n");...return err;
}

基本上,br_add_if 执行以下操作:

  1. 进行一系列验证,确保该设备可以被绑定到网桥下。部分规则包括: a) 不允许类似非以太网的设备;

    b) 已经被绑定的设备不允许再次绑定;

    c) 设备本身不能是网桥;

    d) 设备中不应存在 IFF_DONT_BRIDGE 标志;等等。

  2. 分配并初始化一个 net_bridge_port 结构体。稍后,该端口会被添加到网桥的 port_list 中。

  3. 为设备注册一个接收处理函数 br_handle_frame。发送到该设备的帧将由这个函数处理。我们后续会看到这个处理器具体做什么。

  4. 绑定设备,即让网桥成为这个设备的主控者。

  5. 将该设备的以太网地址作为本地条目添加到转发表中。

值得注意的是,在旧版本中,br_add_if 明确地将设备置于混杂模式。在 4.0 版本的内核中,这一点由 nbp_update_port_count 函数处理。

下图来自“Linux 网桥剖析”,展示了上述提及的函数之间的关系。

转发数据库

转发数据库存储了MAC地址与端口的映射关系。实现上,它使用了一个大小为 BR_HASH_SIZE(256)的哈希表(实际上是一个数组)作为转发数据库。数组中的每一项存储了一个单向链表的头指针(一个桶),该链表存储了所有哈希值落入该桶的MAC地址条目。参考上述 net_bridge 结构体中的 struct hlist_head hash[BR_HASH_SIZE]。MAC地址的哈希值由 br_mac_hash 计算得出。关于哈希算法的细节,我将略过不提。

表项结构

net_bridge_fdb_entry 是上述提到的链表的元素类型。

struct net_bridge_fdb_entry
{struct hlist_node       hlist;struct net_bridge_port  *dst;struct rcu_head         rcu;unsigned long           updated;unsigned long           used;mac_addr                addr;unsigned char           is_local:1,is_static:1,added_by_user:1,added_by_external_learn:1;__u16                   vlan_id;
};

查找

由于实现方式采用了哈希表,查找过程与任何哈希表的查找相同。当网桥需要确定特定MAC地址的数据帧应转发到哪个端口时,它会在转发数据库中查找。因此,哈希表的键就是MAC地址。首先,它通过 br_mac_hash 获取MAC地址的哈希值,然后从该表项获取链表。接下来,它遍历整个链表,将MAC地址与每个元素进行比较,直到找到匹配的那一项。

更新条目

添加、更新和删除条目的操作都是典型的哈希表操作。我不会重复介绍它们是如何工作的,而是专注于这些操作何时发生。

当一个接口被添加到网桥上(即调用 br_add_if 时),会调用 br_fdb_insert 将被绑定设备的MAC地址插入到转发数据库中。 当本地端口上的设备更改其MAC地址(例如,通过命令 ifconfig eth0 hw ether 11:22:33:44:55:66),转发表中的条目会被更新。 当条目过期时,会删除该条目。通常情况下,如果一段时间内未使用某个条目,则该条目会过期。默认过期时间为5分钟,但这可以配置。定期调用 br_fdb_cleanup 来清理已过期的条目。

帧处理

入站数据首先由 netif_receive_skb 处理,这是一个通用的、与设备无关的函数。它会调用接收数据所在设备的 rx_handler。还记得在 br_add_if 中,我们为被绑定的设备注册了一个接收处理器 br_handle_frame 吗?在这里,这个处理器将被调用来处理设备上收到的数据。

br_handle_frame 首先进行一些基本检查,以确保这个帧是一个有效的以太网帧。然后它检查目的地是否为一个预留地址,这意味着这是一个控制帧。如果是的话,需要进行特殊处理。否则,它会调用 br_handle_frame_finish 来处理这个帧。

rx_handler_result_t br_handle_frame(struct sk_buff **pskb) {const unsigned char *dest = eth_hdr(skb)->h_dest;... (Validation code)if (unlikely(is_link_local_ether_addr(dest))) {/** 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*/... (Special processing for control frame)}...NF_HOOK(NFPROTO_BRIDGE, NF_BR_PRE_ROUTING, skb, skb->dev, NULL,br_handle_frame_finish);...
}

br_handle_frame_finish 执行以下步骤:

  1. 学习源MAC地址并更新转发数据库。

  2. 如果目标地址是一个多播地址,那么调用 br_multicast_rcv 进行一些处理。

  3. 如何转发这个帧的决策取决于目标地址是广播地址、多播地址还是单播地址。规则如下:

注:如果接口处于混杂模式,那么无论目标地址是什么,这个帧都将被本地传递。而且,不一定需要开启混杂模式,因为在任何情况下,这个网桥处理器都会转发它。

下图来自“Linux 网桥剖析”,可能也有助于理解帧的处理过程。

结论

在这篇文章中,我们探讨了Linux网桥的实现方式,以及当我们配置网桥时所发生的事件,还有网桥如何处理帧的过程。这里有一个实验你可以尝试。首先,添加一个网桥。然后启动两个连接到这个网桥的虚拟机。通过命令 brctl showmacs br0 查看网桥的MAC地址表。看看两台虚拟机之间是否可以互相ping通。你还可以使用 tcpdump 或者 Wireshark 来捕获流量。回想一下每一步操作时发生了什么。

参考文献

[1] Benvenuti, Christian. 《深入理解LINUX网络内幕》. “O’Reilly Media, Inc.”, 2006.

[2] Linux网桥的剖析

[3] Linux网桥 - 其工作原理

Ref

https://hechao.li/2018/01/31/linux-bridge-part2/

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.xdnf.cn/news/1474895.html

如若内容造成侵权/违法违规/事实不符,请联系一条长河网进行投诉反馈,一经查实,立即删除!

相关文章

C++初学者指南-4.诊断---valgrind

C初学者指南-4.诊断—Valgrind Valgrind(内存错误检测工具) 检测常见运行时错误 读/写释放的内存或不正确的堆栈区域使用未初始化的值不正确的内存释放,如双重释放滥用内存分配函数内存泄漏–非故意的内存消耗通常与程序逻辑缺陷有关&#xf…

Java版Flink使用指南——将消息写入到RabbitMQ的队列中

大纲 新建工程新增依赖 编码自动产生数据写入RabbitMQ 测试 在 《Java版Flink使用指南——从RabbitMQ中队列中接入消息流》一文中,我们介绍了如何使用Java在Flink中读取RabbitMQ中的数据,并将其写入日志中。本文将通过代码产生一些数据,然后…

Vine: 一种全新定义 Vue 函数式组件的解决方案

7月6日的 vue confg 大会上 ShenQingchuan 大佬介绍了他的 Vue Vine 项目, 一种全新定义 Vue 函数式组件的解决方案。 和 React 的函数式组件有异曲同工之妙,写起来直接起飞了。 让我们来快速体验一下 vine, 看看到底给我们带来了哪些惊喜吧…

分别通过LS和RML进行模型参数辨识matlab仿真

目录 1.程序功能描述 2.测试软件版本以及运行结果展示 3.核心程序 4.本算法原理 4.1 最小二乘法(LS)参数辨识 4.2 递归最大似然估计(RML)参数辨识 5.完整程序 1.程序功能描述 分别通过LS和RML进行模型参数辨识matlab仿真,仿真输出参数辨识的误差&#xff0c…

网络基础:BGP协议

BGP(边界网关协议,Border Gateway Protocol)是一种用于在不同自治系统(Autonomous Systems,AS)之间交换路由信息的路径向量协议。BGP是互联网的核心路由协议之一,负责管理和维护互联网范围内的路…

MySQL安全加固及等保测评

登录后复制 Mysql基础命令 create USER new_userlocalhost IDENTIFIED BY password; //创建用户 alter user root% identified with mysql_native_password by ********; //修改密码 rename user root% to root192.168.1.1; //重命名 flush privileges; …

Java面试题--JVM大厂篇之深入解析G1 GC——革新Java垃圾回收机制

目录 引言: 正文: 一、G1 GC的区域划分及其作用 1. 伊甸园区(Eden Region) 2. 幸存者区(Survivor Region) 3. 老年代区(Old Generation Region) 二、区域划分的优势: 三、图片解析: 结…

昇思25天学习打卡营第20天|LSTM+CRF序列标注

学AI还能赢奖品?每天30分钟,25天打通AI任督二脉 (qq.com) LSTMCRF序列标注 概述 序列标注指给定输入序列,给序列中每个Token进行标注标签的过程。序列标注问题通常用于从文本中进行信息抽取,包括分词(Word Segmentation)、词性标…

Python | Leetcode Python题解之第220题存在重复元素III

题目&#xff1a; 题解&#xff1a; class Solution(object):def containsNearbyAlmostDuplicate(self, nums, k, t):from sortedcontainers import SortedSetst SortedSet()left, right 0, 0res 0while right < len(nums):if right - left > k:st.remove(nums[left]…

D - Go Stone Puzzle(abc361)

分析&#xff1a;因为n很小&#xff0c;可以逐一搜索&#xff0c;用一个队列将每种情况列出来&#xff0c;用bfs寻找从s到t的最短路径 #include <bits/stdc.h> using namespace std; int n; string s, t; map<string, int> dis; void bfs() { dis[s] 0; …

RocketMQ NettyRemotingServer、NettyRemotingClient 实例化、初始化、启动源码解析

&#x1f52d; 嗨&#xff0c;您好 &#x1f44b; 我是 vnjohn&#xff0c;在互联网企业担任后端开发&#xff0c;CSDN 优质创作者 &#x1f4d6; 推荐专栏&#xff1a;Spring、MySQL、Nacos、Java&#xff0c;后续其他专栏会持续优化更新迭代 &#x1f332;文章所在专栏&#…

【5G VoNR】VoNR流程简述

博主未授权任何人或组织机构转载博主任何原创文章&#xff0c;感谢各位对原创的支持&#xff01; 博主链接 本人就职于国际知名终端厂商&#xff0c;负责modem芯片研发。 在5G早期负责终端数据业务层、核心网相关的开发工作&#xff0c;目前牵头6G技术研究。 博客内容主要围绕…

go-redis源码解析:如何实现sentinel高可用

go-redis里&#xff0c;sentinel只用来获取master和从节点的ip地址&#xff0c;在获取master和replica节点ip时&#xff0c;如果sentinel不可用&#xff0c;那么会换其他的sentinel重试&#xff0c;并将可用的sentinel换到第一个 1. 用于获取master节点 先通过读锁获取c.senti…

模板进阶:非类型模板参数,类模板特化,模板的编译分离

1. 非类型模板参数 模板参数分类类型形参与非类型形参。 类型形参即&#xff1a;出现在模板参数列表中&#xff0c;跟在class或者typename之类的参数类型名称。 非类型形参&#xff0c;就是用一个常量作为类(函数)模板的一个参数&#xff0c;在类(函数)模板中可将该参数当成常…

【Unity数据交互】如何Unity中读取Ecxel中的数据

&#x1f468;‍&#x1f4bb;个人主页&#xff1a;元宇宙-秩沅 &#x1f468;‍&#x1f4bb; hallo 欢迎 点赞&#x1f44d; 收藏⭐ 留言&#x1f4dd; 加关注✅! &#x1f468;‍&#x1f4bb; 本文由 秩沅 原创 &#x1f468;‍&#x1f4bb; 专栏交流&#x1f9e7;&…

2024/7/7周报

文章目录 摘要Abstract文献阅读题目问题本文贡献问题描述图神经网络Framework实验数据集实验结果 深度学习MAGNN模型相关代码GNN为什么要用GNN&#xff1f;GNN面临挑战 总结 摘要 本周阅读了一篇用于多变量时间序列预测的多尺度自适应图神经网络的文章&#xff0c;多变量时间序…

Vulkan 学习(1)---- Vulkan 基本概念和发展历史

目录 Vulkan及其演化史Vulkan 基本概念基本术语 Vulkan 的原理Vulkan应用程序Vulkan的编程模型硬件初始化窗口展示表面资源设置流水线设置描述符和描述符缓冲池基于SPIR-V的着色器流水线管理指令的记录队列的提交 Vulkan及其演化史 目前主流的图形渲染API有OpenGL、OpenGL ES、…

ROS——多个海龟追踪一个海龟实验

目标 通过键盘控制一个海龟&#xff08;领航龟&#xff09;的移动&#xff0c;其余生成的海龟通过监听实现追踪定期获取领航龟和其余龟的坐标信息&#xff0c;通过广播告知其余龟&#xff0c;进行相应移动其余龟负责监听 疑惑点&#xff08;已解决&#xff09; int main(int…

【感谢告知】本账号内容调整,聚焦于Google账号和产品的使用经验和问题案例分析

亲爱的各位朋友&#xff1a; 感谢您对本账号的关注和支持&#xff01; 基于对朋友们需求的分析和个人兴趣的转变&#xff0c;该账号从今天将对内容做一些调整&#xff0c;有原来的内容改为Google&#xff08;谷歌&#xff09;账号和产品的使用经验&#xff0c;以及相关问题的…

判断是否为完全二叉树

目录 分析 分析 1.完全二叉树的概念&#xff1a;对于深度为K的&#xff0c;有n个结点的二叉树&#xff0c;当且仅当其每一个结点都与深度为K的满二叉树中编号从1至n的结点一一对应时称之为完全二叉树。 要注意的是满二叉树是一种特殊的完全二叉树。 2.思路&#xff1a;可以采…