bridge
一、概念
-
链路层协议
-
按帧复制数据
-
具有地址学习功能
- 并不是简单地把数据从一个端口转发到其他端口
- 会根据学习到的 mac 地址来决定是否进行转发,如何转发
br_private.h // 网桥私有头文件
// mac 地址
struct mac_addr {
unsigned char addr[ETH_ALEN];
};
// 转发数据表项
struct net_bridge_fdb_entry {
struct rhash_head rhnode; // 链接到 hash 表头
struct net_bridge_port *dst; // 指向目的网桥端口
struct net_bridge_fdb_key key;
struct hlist_node fdb_node;
unsigned char is_local:1, // 是否是本地 mac 地址
is_static:1, // mac 地址是否为静态
is_sticky:1,
added_by_user:1,
added_by_external_learn:1,
offloaded:1;
/* write-heavy members should not affect lookups */
unsigned long updated ____cacheline_aligned_in_smp;
unsigned long used;
struct rcu_head rcu;
};
// 桥端口
struct net_bridge_port {
struct net_bridge *br; // 指向 该网桥端口 所绑定的 网桥
struct net_device *dev; // 指向 该网桥端口 所绑定的 网络设备
struct list_head list; // 用于将该网桥端口链接到网桥的 port_list 链表的指针
unsigned long flags;
#ifdef CONFIG_BRIDGE_VLAN_FILTERING
struct net_bridge_vlan_group __rcu *vlgrp;
#endif
struct net_bridge_port __rcu *backup_port;
/* STP */
u8 priority; // 端口优先级
u8 state; // 端口状态,在对数据进行转发时会对该 state 值进行判断
u16 port_no; // 端口号
unsigned char topology_change_ack;
unsigned char config_pending;
port_id port_id; // 端口 ID
port_id designated_port;
bridge_id designated_root;
bridge_id designated_bridge;
u32 path_cost; // 端口路径开销
u32 designated_cost;
unsigned long designated_age;
/* 网桥端口定时器 */
struct timer_list forward_delay_timer;
struct timer_list hold_timer;
struct timer_list message_age_timer;
struct kobject kobj;
struct rcu_head rcu;
#ifdef CONFIG_BRIDGE_IGMP_SNOOPING
struct bridge_mcast_own_query ip4_own_query;
#if IS_ENABLED(CONFIG_IPV6)
struct bridge_mcast_own_query ip6_own_query;
#endif /* IS_ENABLED(CONFIG_IPV6) */
unsigned char multicast_router;
struct bridge_mcast_stats __percpu *mcast_stats;
struct timer_list multicast_router_timer;
struct hlist_head mglist;
struct hlist_node rlist;
#endif
#ifdef CONFIG_SYSFS
char sysfs_name[IFNAMSIZ];
#endif
#ifdef CONFIG_NET_POLL_CONTROLLER
struct netpoll *np;
#endif
#ifdef CONFIG_NET_SWITCHDEV
int offload_fwd_mark;
#endif
u16 group_fwd_mask;
u16 backup_redirected_cnt;
};
// net bridge
struct net_bridge {
spinlock_t lock; // 自旋锁,在向 net_bridge 中增加 port 节点或改变 net_bridge 结构时使用
spinlock_t hash_lock; // 对 hash 转发库进行操作时需要使用该自旋锁
struct list_head port_list; // 网桥端口列表
struct net_device *dev; // 网桥设备
struct pcpu_sw_netstats __percpu *stats;
unsigned long options;
/* These fields are accessed on each packet */
#ifdef CONFIG_BRIDGE_VLAN_FILTERING
__be16 vlan_proto;
u16 default_pvid;
struct net_bridge_vlan_group __rcu *vlgrp;
#endif
struct rhashtable fdb_hash_tbl;
#if IS_ENABLED(CONFIG_BRIDGE_NETFILTER)
union {
struct rtable fake_rtable;
struct rt6_info fake_rt6_info;
};
#endif
u16 group_fwd_mask;
u16 group_fwd_mask_required;
/* STP */
bridge_id designated_root;
bridge_id bridge_id;
unsigned char topology_change;
unsigned char topology_change_detected;
u16 root_port; // 根端口的端口号
/* 网桥定时器 */
unsigned long max_age;
unsigned long hello_time;
unsigned long forward_delay;
unsigned long ageing_time;
/* 本地配置的网桥定时器 */
unsigned long bridge_max_age;
unsigned long bridge_hello_time;
unsigned long bridge_forward_delay;
unsigned long bridge_ageing_time;
u32 root_path_cost;
u8 group_addr[ETH_ALEN];
enum {
BR_NO_STP, /* no spanning tree */
BR_KERNEL_STP, /* old STP in kernel */
BR_USER_STP, /* new RSTP in userspace */
} stp_enabled;
#ifdef CONFIG_BRIDGE_IGMP_SNOOPING
u32 hash_max;
u32 multicast_last_member_count;
u32 multicast_startup_query_count;
u8 multicast_igmp_version;
u8 multicast_router;
#if IS_ENABLED(CONFIG_IPV6)
u8 multicast_mld_version;
#endif
spinlock_t multicast_lock;
unsigned long multicast_last_member_interval;
unsigned long multicast_membership_interval;
unsigned long multicast_querier_interval;
unsigned long multicast_query_interval;
unsigned long multicast_query_response_interval;
unsigned long multicast_startup_query_interval;
struct rhashtable mdb_hash_tbl;
struct hlist_head mdb_list;
struct hlist_head router_list;
struct timer_list multicast_router_timer;
struct bridge_mcast_other_query ip4_other_query;
struct bridge_mcast_own_query ip4_own_query;
struct bridge_mcast_querier ip4_querier;
struct bridge_mcast_stats __percpu *mcast_stats;
#if IS_ENABLED(CONFIG_IPV6)
struct bridge_mcast_other_query ip6_other_query;
struct bridge_mcast_own_query ip6_own_query;
struct bridge_mcast_querier ip6_querier;
#endif /* IS_ENABLED(CONFIG_IPV6) */
#endif
/* 网桥定时器 */
struct timer_list hello_timer;
struct timer_list tcn_timer;
struct timer_list topology_change_timer;
struct delayed_work gc_work;
struct kobject *ifobj;
u32 auto_cnt;
#ifdef CONFIG_NET_SWITCHDEV
int offload_fwd_mark;
#endif
struct hlist_head fdb_list;
};
网桥之所以是网桥,主要靠下面这两个函数:
br_fdb_insert() // 学习,插入
br_fdb_get() // 查表
什么是桥接?
简单来说,桥接就是把一台机器上的若干个网口"连接"起来。其结果是,其中一个网口收到的报文会被复制给其他网口并发送出去。以使得网口之间的报文能够相互转发。
交换机就是这样一个设备,它有若干个网口,并且这些网口是桥接起来的。于是,与交换机相连的若干主机就能够通过交换机的报文转发而互相通信。
桥接不是在物理层实现的,而是在数据链路层。所以bridge能够理解数据链路层的报文,所以实际上桥接却又不是单纯的报文转发。
交换机会关心填写在报文的数据链路层头部中的Mac地址信息(包括源地址和目的地址),以便了解每个Mac地址所代表的主机都在什么位置(与本交换机的哪个网口相连)。在报文转发时,交换机就只需要向特定的网口转发即可,从而避免不必要的网络交互。这个就是交换机的“地址学习”。
但是如果交换机遇到一个自己未学习到的地址,就不会知道这个报文应该从哪个网口转发,则只好将报文转发给所有网口(接收报文的那个网口除外)。
超时策略,需要定时忘记过去。
linux内核支持网口的桥接(目前只支持以太网接口)。???不理解
$ sudo brctl addif br0 enp3s0
$ sudo brctl addif br0 wlp5s0
can't add wlp5s0 to bridge br0: Operation not supported
CAM 表
交换机在二层转发数据要查找的表
内容:mac - prot - vlan
交换机的每一个二层端口都有 MAC 地址自动学习的功能
- 当交换机收到 PC 发来的一个帧,就会查看帧中的源 MAC 地址,并查找 CAM 表
- 如果有就什么也不做,开始转发数据
- 如果没有就存入 CAM 表,以便当其他人向这个 MAC 地址上发送数据时,可以决定向哪个端口转发数据
STP
(Spanning Tree Protocol,生成树协议),应用于计算机网络树形拓扑结构建立,
作用
主要作用是防止网桥网络中的冗余链路形成环路。
技术原理
通过在交换机之间传递一种特殊的协议报文,网桥协议数据单元(Bridge Protocol Data Unit,BPDU),来确定网络的拓扑结构。
MTU
(Maximum Transmission Unit,MTU)最大传输单元。单位字节,大部分网络设备的 MTU 都是 1500。
如果本机的 MTU 比网关的 MTU 大,大的数据包就会被拆开来传送,这样会产生很多数据包碎片,增加丢包率,降低网络速度。
用指令ping -f -l 1472 10.10.68.1
可以检测网关的 MTU ?
kobject
Linux设备模型的核心是使用Bus、Class、Device、Driver四个核心数据结构,将大量的、不同功能的硬件设备(以及驱动该硬件设备的方法),以树状结构的形式,进行归纳、抽象,从而方便Kernel的统一管理。
Bus(总线):
Linux认为,总线是CPU和一个或多个设备之间信息交互的通道。而为了方便设备模型的抽象,所有的设备都应连接到总线上。 Class(分类):
在Linux设备模型中,Class的概念非常类似面向对象程序设计中的Class(类),它主要是集合具有相似功能或属性的设备,这样就可以抽象出一套可以在多个设备之间共用的数据结构和接口函数。因而从属于相同Class的设备的驱动程序,就不再需要重复定义这些公共资源,直接从Class中继承即可。
Device(设备):
抽象系统中所有的硬件设备,描述它的名字、属性、从属的Bus、从属的Class等信息。
Device Driver(驱动):
Linux设备模型用Driver抽象硬件设备的驱动程序,它包含设备初始化、电源管理相关的接口实现。而Linux内核中的驱动开发,基本都围绕该抽象进行(实现所规定的接口函数)。
IP 地址
-
桥接后,原网卡上就不需要ip地址了。——linux使用brctl 命令行添加网桥
-
可以给网桥一个地址,这样就能远程管理网桥了。——Linux 网桥配置命令:brctl
-
给网桥设置一个 IP 地址,这个地址可以作为其下主机的网关。作为网关时,还要有路由和nat功能。
虚拟设备
网桥是一个虚拟设备
网卡四种工作模式
-
广播模式:它的 mac 地址是 0xffffff ?,只接收广播帧
-
多播模式:多播传送地址可以被组内的主机接收,而组外的主机是收不到的;但是,如果网卡设置为多播模式,它可以接收所有的多播传送帧,而不论它是不是组内成员。
-
直接模式:只接收目的地址是自己 mac 地址的帧
-
混杂模式:接收所有流过网卡的帧。处于混杂模式下的网卡的 ip 无效,也用不着 。
网卡的缺省工作模式包含广播模式和直接模式。
网卡五种端口状态
#define BR_STATE_DISABLED 0 // 什么功能都没有,只有一个逻辑设备。
#define BR_STATE_LISTENING 1 // 可以接收和发送网络传输的BPDU,包括Configureation BPDU和TCN BPDU,但不能进行数据帧的转发、不能学习。
#define BR_STATE_LEARNING 2 // 可以接收和发送BPDU,可以学习,但是不能进行数据帧的转发。
#define BR_STATE_FORWARDING 3 // 可以接收和发送BPDU、可以学习、可以进行数据帧的转发。
#define BR_STATE_BLOCKING 4 // 只能接收BPDU,不能发送BPDU,不能学习,不能转发数据帧。
网卡混杂模式的判断
-
网卡是否处于PROMISC模式,ifconfig(ip link show也是如此)并不是最直接的判断依据,换句话说就是ifconfig能看到PROMISC标记表示一定处于混杂模式,但处于混杂模式并不一定能看到PROMISC标记。内核判断网卡是否处于混杂模式是看
/sys/class/net/ifname/flags
的值,如果置位了0x100,则处于混杂模式。
无线网卡的工作模式
- Managed 模式
- 又称 state 模式
- Master 模式
- AP 模式
- Ad hoc 模式
- 点对点模式
- Monitor 模式
- 侦听模式 ≈ 混杂模式 ?
- mesh 模式
网桥的 MAC 地址
- 没有 Port ,br0 获得一个随机 MAC 地址
- 添加 Port 后,br0 只能被指定(也必须被指定)为其中一个 Port 的 MAC 地址
- 如果没有手动指定,br0 会根据 bridge 中 port 的变化,自动选择 port 最小的一个 MAC 作为自身 MAC 地址
网桥的工作原理
https://www.jianshu.com/p/9070f4bfeddf
二、转发流程
Bridge的数据在内核处理流程
Linux内核数据包bridge上转发流程
集线器、交换机、网桥区别
https://blog.csdn.net/dataiyangu/article/details/82496340
rx
- 网络报文由网卡进行接收;
- 对于linux内核来说,设备驱动程序从网卡中读取报文;
- 之后将报文送到网络协议栈。
网卡 –> 驱动程序 –> 网络协议栈
收到报文的一定是网卡,专门干这件事的;如果没有网卡,CPU从这里就开始着手接收和处理报文,那CPU的负担太重了。所以通常都是由一块专门的硬件电路来处理,即网卡。
tx
- 进程将数据送给网络协议栈(系统调用方式,用户空间–>内核空间);
- 网络协议栈处理后将报文送给驱动程序;
- 驱动程序将报文送给网卡。
*转发流程参考链接
你了解Linux 3.10 kernel bridge的转发逻辑?
Linux从用户层到内核层系列 - TCP/IP协议栈部分系列3: bridge(网桥)FDB表中MAC地址的更新
三、网桥初始化
- CAM 表的初始化
- 注册网桥相关的网络防火墙钩子函数
- 向通知链表中注册网桥的回调函数,处理网桥感兴趣的一些事件
- 设置网桥的 ioctl,以便处理应用层添加网桥、删除网桥的需求
- 注册网桥处理回调函数,在接收封包处理函数 netif_receive_skb 中用来处理网桥设备
网桥初始化函数:br_init()
module_init(br_init)
module_exit(br_deinit)
static int __init br_init(void)
{
int err;
BUILD_BUG_ON(sizeof(struct br_input_skb_cb) > FIELD_SIZEOF(struct sk_buff, cb));
err = stp_proto_register(&br_stp_proto); // stp(生成树协议)初始化
if (err < 0) {
pr_err("bridge: can't register sap for STP\n");
return err;
}
err = br_fdb_init(); // CAM 表初始化
if (err)
goto err_out;
err = register_pernet_subsys(&br_net_ops); // 为 bridge 模块这册网络命名空间
if (err)
goto err_out1;
err = br_nf_core_init();
if (err)
goto err_out2;
err = register_netdevice_notifier(&br_device_notifier); // 向通知链中注册回调函数,处理网桥感兴趣的一些事件
if (err)
goto err_out3;
err = register_switchdev_notifier(&br_switchdev_notifier);
if (err)
goto err_out4;
err = br_netlink_init(); // 进行 netlink 的初始化
if (err)
goto err_out5;
brioctl_set(br_ioctl_deviceless_stub); // 设置网桥相关的 ioctl 回调函数 br_ioctl_deviceless_stub
#if IS_ENABLED(CONFIG_ATM_LANE)
br_fdb_test_addr_hook = br_fdb_test_addr;
#endif
#if IS_MODULE(CONFIG_BRIDGE_NETFILTER)
pr_info("bridge: filtering via arp/ip/ip6tables is no longer available "
"by default. Update your scripts to load br_netfilter if you "
"need this.\n");
#endif
return 0;
err_out5:
unregister_switchdev_notifier(&br_switchdev_notifier);
err_out4:
unregister_netdevice_notifier(&br_device_notifier);
err_out3:
br_nf_core_fini();
err_out2:
unregister_pernet_subsys(&br_net_ops);
err_out1:
br_fdb_fini();
err_out:
stp_proto_unregister(&br_stp_proto);
return err;
}
int __init br_fdb_init(void)
{
// 获取一块 slab 缓存 br_fdb_cache
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;
return 0;
}
static struct notifier_block br_device_notifier = {
.notifier_call = br_device_event
};
static int br_device_event(struct notifier_block *unused, unsigned long event, void *ptr)
{
switch (event) {
case NETDEV_CHANGEMTU: /* notify after mtu change happened */ /* 需要更新 CAM 表 */
br_mtu_auto_adjust(br);
break;
case NETDEV_PRE_CHANGEADDR:
if (br->dev->addr_assign_type == NET_ADDR_SET)
break;
prechaddr_info = ptr;
err = dev_pre_changeaddr_notify(br->dev,
prechaddr_info->dev_addr,
extack);
if (err)
return notifier_from_errno(err);
break;
case NETDEV_CHANGEADDR:
spin_lock_bh(&br->lock);
br_fdb_changeaddr(p, dev->dev_addr);
changed_addr = br_stp_recalculate_bridge_id(br);
spin_unlock_bh(&br->lock);
if (changed_addr)
call_netdevice_notifiers(NETDEV_CHANGEADDR, br->dev);
break;
case NETDEV_CHANGE: // 需要更改网桥端口状态
br_port_carrier_check(p, ¬ified);
break;
case NETDEV_FEAT_CHANGE:
netdev_update_features(br->dev);
break;
case NETDEV_DOWN: // 需要更改网桥端口状态
spin_lock_bh(&br->lock);
if (br->dev->flags & IFF_UP) {
br_stp_disable_port(p);
notified = true;
}
spin_unlock_bh(&br->lock);
break;
case NETDEV_UP: // 需要更改网桥端口状态
if (netif_running(br->dev) && netif_oper_up(dev)) {
spin_lock_bh(&br->lock);
br_stp_enable_port(p);
notified = true;
spin_unlock_bh(&br->lock);
}
break;
case NETDEV_UNREGISTER:
br_del_if(br, dev);
break;
case NETDEV_CHANGENAME:
err = br_sysfs_renameif(p);
if (err)
return notifier_from_errno(err);
break;
case NETDEV_PRE_TYPE_CHANGE:
/* Forbid underlaying device to change its type. */
return NOTIFY_BAD;
case NETDEV_RESEND_IGMP:
/* Propagate to master device */
call_netdevice_notifiers(event, br->dev);
break;
}
}
// 我们想增加新的 ioctl,可以增加 case
int br_ioctl_deviceless_stub(struct net *net, unsigned int cmd, void __user *uarg)
{
switch (cmd) {
case SIOCGIFBR: /* Bridging support */
case SIOCSIFBR: /* Set bridging options */
return old_deviceless(net, uarg);
case SIOCBRADDBR: /* create new bridge device */
case SIOCBRDELBR: /* remove bridge device */
{
char buf[IFNAMSIZ];
if (!ns_capable(net->user_ns, 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;
}
添加网桥
int br_add_bridge(struct net *net, const char *name)
{
struct net_device *dev;
int res;
// 申请 net_device 的内存、调用 br_dev_setup 对网桥设备进行初始化
dev = alloc_netdev(sizeof(struct net_bridge), name, NET_NAME_UNKNOWN,
br_dev_setup);
if (!dev)
return -ENOMEM;
dev_net_set(dev, net);
dev->rtnl_link_ops = &br_link_ops;
res = register_netdev(dev); // 注册网桥设备
if (res)
free_netdev(dev);
return res;
}
/* 删除网桥
1. 首先判断能否删除网桥(如果设备不是网桥或者网桥设备是 up 状态时不能删除网桥)
2. 若符合删除的条件,则调用 del_br 进行删除
*/
int br_del_bridge(struct net *net, const char *name)
{
struct net_device *dev;
int ret = 0;
rtnl_lock();
dev = __dev_get_by_name(net, name);
if (dev == NULL)
ret = -ENXIO; /* Could not find device */
else if (!(dev->priv_flags & IFF_EBRIDGE)) {
/* Attempt to delete non bridge device! */
ret = -EPERM;
}
else if (dev->flags & IFF_UP) {
/* Not shutdown yet. */
ret = -EBUSY;
}
else
br_dev_delete(dev, NULL);
rtnl_unlock();
return ret;
}
/* Delete bridge device */
void br_dev_delete(struct net_device *dev, struct list_head *head)
{
struct net_bridge *br = netdev_priv(dev);
struct net_bridge_port *p, *n;
/* 调用 del_nbp,循环删除掉该网桥下的所有网桥端口 Delete port(interface) */
list_for_each_entry_safe(p, n, &br->port_list, list) {
del_nbp(p);
}
br_recalculate_neigh_suppress_enabled(br);
br_fdb_delete_by_port(br, NULL, 0, 1);
cancel_delayed_work_sync(&br->gc_work);
/* 从 linux 系统中删除网桥相关联的 kobject */
br_sysfs_delbr(br->dev);
/* 注销网桥设备 */
unregister_netdevice_queue(br->dev, head);
}
添加网桥端口
int br_add_if(struct net_bridge *br, struct net_device *dev,
struct netlink_ext_ack *extack)
{
struct net_bridge_port *p;
int err = 0;
unsigned br_hr, dev_hr;
bool changed_addr;
pr_info("c1. brctl addif <bridge> <device>-br_add_if\n");
/* Don't allow bridging non-ethernet like devices, or DSA-enabled
* master network devices since the bridge layer rx_handler prevents
* the DSA fake ethertype handler to be invoked, so we do not strip off
* the DSA switch tag protocol header and the bridge layer just return
* RX_HANDLER_CONSUMED, stopping RX processing for these frames.
*/
if ((dev->flags & IFF_LOOPBACK) ||
dev->type != ARPHRD_ETHER || dev->addr_len != ETH_ALEN ||
!is_valid_ether_addr(dev->dev_addr) ||
netdev_uses_dsa(dev))
return -EINVAL;
/* No bridging of bridges */
if (dev->netdev_ops->ndo_start_xmit == br_dev_xmit) {
NL_SET_ERR_MSG(extack,
"Can not enslave a bridge to a bridge");
return -ELOOP;
}
/* Device has master upper dev */
if (netdev_master_upper_dev_get(dev))
return -EBUSY;
/* No bridging devices that dislike that (e.g. wireless) */
if (dev->priv_flags & IFF_DONT_BRIDGE) {
NL_SET_ERR_MSG(extack,
"Device does not allow enslaving to a bridge");
return -EOPNOTSUPP;
}
// 分配一个新的网桥端口并对其初始化
p = new_nbp(br, dev);
if (IS_ERR(p))
return PTR_ERR(p);
// 调用设备通知链,告诉网络有这样一个设备
call_netdevice_notifiers(NETDEV_JOIN, dev);
err = dev_set_allmulti(dev, 1);
if (err) {
kfree(p); /* kobject not yet init'd, manually free */
goto err1;
}
err = kobject_init_and_add(&p->kobj, &brport_ktype, &(dev->dev.kobj),
SYSFS_BRIDGE_PORT_ATTR);
if (err)
goto err2;
// 把链路家到sysfs
err = br_sysfs_addif(p);
if (err)
goto err2;
err = br_netpoll_enable(p);
if (err)
goto err3;
// 注册主设备接收函数
err = netdev_rx_handler_register(dev, br_handle_frame, p);
if (err)
goto err4;
dev->priv_flags |= IFF_BRIDGE_PORT;
// 向上级设备添加主链路
err = netdev_master_upper_dev_link(dev, br->dev, NULL, NULL, extack);
if (err)
goto err5;
err = nbp_switchdev_mark_set(p);
if (err)
goto err6;
// 禁用网络设备上的大型接收卸载
dev_disable_lro(dev);
list_add_rcu(&p->list, &br->port_list);
/*更新桥上的端口数,如果有更新,再进一步将其设为混杂模式*/
nbp_update_port_count(br);
// 把dev的mac添加到转发数据库中
if (br_fdb_insert(br, p, dev->dev_addr, 0))
netdev_err(dev, "failed insert local address bridge forwarding table\n");
// 初始化该桥端口的vlan
err = nbp_vlan_init(p);
if (err) {
netdev_err(dev, "failed to initialize vlan filtering on this port\n");
goto err7;
}
return 0;
}
哪些设备不能作为网桥端口
- 回环设备不能作为网桥端口
- 非以太网设备不能作为网桥端口
- 网桥设备不能作为网桥端口
- 已经加入到桥组的设备不能再次加入桥组
四、编译
sudo make -C /lib/modules/5.4.0-29-generic/build/ M=/usr/src/linux-headers-5.4.0-29-generic/net/bridge clean
sudo make -C /lib/modules/5.4.0-29-generic/build/ M=/usr/src/linux-headers-5.4.0-29-generic/net/bridge modules
sudo rmmod bridge
sudo insmod bridge.ko
sudo tail -f /var/log/kern.log
五、brctl
ethernet bridge administration,以太网桥管理
sudo insmod bridge.ko --> module_init(br_init)
sudo brctl addbr br0
六、网桥实验
实验一:通过bridge-utils工具创建网桥并实现网络连接