1. 项目概述
1.1 这项目到底在做什么
最近在折腾 Linux 网络虚拟化,碰到一个特别典型的场景:我在一台没有多余物理网卡的服务器上,想跑一个 Linux 虚拟机做网络实验,要求虚拟机跟宿主机处在同一个二层网络里,虚拟机要能跟宿主机互访,还要能跟局域网里的其他机器正常通信。手头没有物理交换机,也没法直接在宿主机上插第二块网卡,那就得靠 Linux 自带的虚拟化网络功能把这层“桥接网络”搭出来。
方案很直接:在宿主机上用ip tuntap创建一个 tun/tap 设备,把它当成一块“虚拟物理网卡”接进系统;再创建一个 Linux bridge(网桥),把这个虚拟网卡和宿主机真实的物理网卡一起“塞”进网桥里。这样,虚拟网卡就相当于通过网桥跟物理网卡连在了一起,虚拟机进程把数据包写到虚拟网卡上,网桥就会像一台二层交换机一样,把包从物理网卡转发出去。整个链路是通的,虚拟机在网络层面就跟一台直接插在局域网交换机上的物理机没有区别。
这活儿听起来简单,但实际操作里涉及的东西其实不少:tun/tap 驱动是内核里怎么工作的、网桥接口为什么需要 up、ARP 表为什么会乱、虚拟机进程该把哪个接口当作自己的网卡……每一项都有讲究。这篇文章就完全围绕“Linux 上用 tun/tap 虚拟网卡 + 网桥实现二层互通”这个主题,把原理、步骤、踩坑全部展开,适合正在学习 Linux 网络虚拟化、打算玩 KVM/QEMU 虚拟网络、或者被公司里“我没物理交换机但想搭个虚拟网络”这类需求卡住的朋友。
1.2 先说清楚你能得到什么
读完这篇文章,你能完完整整复现出下面这个网络拓扑:
- 宿主机有一个真实物理网卡
eth0,IP 为192.168.1.10/24,网关192.168.1.1。 - 创建一个网桥
br0,把eth0加进br0。 - 创建一个 tap 设备
tap0,也加进br0。 - 一个用户态进程(比如 QEMU 虚拟机,或者你手写的一个从
/dev/net/tun读写数据包的程序)绑定tap0。 - 虚拟机内部配置 IP
192.168.1.20/24,网关同样是192.168.1.1。
最后的结果就是:虚拟机 ping 宿主机、ping 局域网其他机器、甚至通过网关访问外网,都能通。而且整个过程完全没有用到 NAT,就是纯二层桥接,网络行为跟真实插网线一模一样。
需要提醒的是,我这里用的“虚拟网卡”指的是 tun/tap 设备这一层。你在网上搜“虚拟网卡”还能搜到 vmware 的 vmnet、hyper-v 的虚拟交换机之类的东西,思路都是同一套:让虚拟机共享宿主机的物理网络。但咱们这篇只聚焦 Linux 原生方案,不依赖任何商业虚拟化软件,纯开源、纯命令行就能搞定。
2. 整体设计思路:为什么偏偏用 tun/tap + 网桥
2.1 先理解 tun、tap 分别是啥
很多人一上来就搞混 tun 和 tap,其实记住一句话就行:
- tun 设备工作在 IP 层(三层),传输的是 IP 数据包。
- tap 设备工作在以太网层(二层),传输的是完整的以太网帧。
咱们要跟网桥配合、实现“虚拟网卡连接网桥”的效果,必须用 tap。因为网桥是一台二层设备,它认识的是 MAC 地址,转发的是以太网帧。如果用 tun,走的是 IP 层,网桥上完全看不到二层的 MAC 信息,二层桥接就无从谈起。
从实现原理上来说,tun/tap 都是 Linux 内核提供的一种“虚拟网络设备”。你可以把它想象成一个“管道”:一头连接内核网络协议栈,另一头连接用户态程序。当用户态程序往这个设备文件写入数据时,内核会认为这些数据是从一块真实的网卡上收到的数据包,进而送入协议栈或网桥进行处理;反之,内核要把数据包发送给这块“网卡”时,数据会出现在设备文件里,用户态程序只要去 read 就能拿到。
拿 tap 设备举例,当你创建一个tap0并把一个虚拟机进程的文件描述符连接到它上面后,虚拟机里发出的每个以太网帧,都会原封不动地出现在宿主机这个/dev/net/tun文件描述符的读取端。宿主机的网桥就会认为“这是从一块真实网卡收到的一帧”,按正常二层逻辑去处理。
2.2 为什么用网桥而不是直接用路由转发
如果不用网桥,你可能会想到另一种方案:给虚拟网卡配个独立 IP,然后在宿主机上开 IP forward,再配置 iptables 规则做 NAT。这种方案叫“用户态 NAT 网络”或“路由网络”,很多虚拟化软件默认也这么干,比如 VirtualBox 的 NAT 网络。
但 NAT 方案有个很明显的局限:虚拟机不在二层网络里。外部设备看到的虚拟机流量,源 IP 全是宿主机的 IP,无法直接通过 IP 访问虚拟机,也无法在局域网里做广播、组播,很多依赖二层协议的服务(比如 DHCP 自动获取 IP、mDNS 设备发现、Windows 文件邻居发现)都工作不了。
网桥方案则直接把虚拟网卡“物理接入”局域网。网桥本身对二层帧几乎是透明的,除了会学习 MAC 地址、按 MAC 表转发外,不会对帧做任何修改。虚拟机就像把网线插在了跟宿主机同一台交换机上,所有二层协议都正常工作。这种方式最符合“虚拟网卡”这个词给人的直觉:它就是要扮演一块真正的物理网卡。
咱们这个项目里选择 tun/tap + 网桥,正是因为目标就是一个纯二层的透明桥接。你要是只想让虚拟机偷偷上网,那开 NAT 就够用了;但如果你想把虚拟机当成一台正儿八经的局域网设备来用,网桥是唯一正确的选择。
2.3 拓扑设计与IP规划
在设计阶段,先把 IP 规划想清楚,后面能省一堆事:
| 设备 | 接口 | IP 地址 | 说明 |
|---|---|---|---|
| 宿主机物理网卡 | eth0 | 192.168.1.10/24 | 原有物理网卡,后面要加进 br0 |
| 宿主机网桥 | br0 | 192.168.1.10/24 | 建好后 IP 从 eth0 移到 br0 上 |
| 虚拟网卡 | tap0 | 不配 IP | tap 设备本身不配 IP,只作二层口 |
| 虚拟机网卡 | eth0 | 192.168.1.20/24 | 虚拟机内部的网卡,走 tap0 收发帧 |
这里有一个非常容易踩坑的点:当你把 eth0 加进 br0 之后,eth0 上原来配的 IP 会失效。因为从内核角度看,eth0 已经变成了一个“二层口”,它的职责是收发以太网帧,而 IP 地址是三层概念,只能配置在三层接口上。所以你必须把原本配在 eth0 上的 IP 地址挪到 br0 上,并把 eth0 的 IP 清空。这个“IP 迁移”步骤忘了的话,网络会立即中断,SSH 会话都会断开。
至于 tap0 为什么不用配 IP,原因也一样:它被加进网桥后就只是网桥的一个二层端口,IP 地址对它没有意义。如果你一不小心给 tap0 配了 IP,反而会引起路由混乱,导致数据包走错路径。
整个设计做完以后,包转发路径大概是这样的:
- 虚拟机发送一个目标为
192.168.1.1的 ICMP 请求,封装成以太网帧,源 MAC 是虚拟机虚拟网卡的 MAC,目的 MAC 是网关的 MAC。 - 帧通过虚拟机内部的虚拟网卡,传递给宿主机侧的
tap0。 - 网桥
br0收到了来自tap0端口的帧,查询自己的 MAC 地址表,发现目标 MAC 对应的端口是eth0,于是把帧从eth0转发出去。 - 物理交换机收到帧,照常转发给网关。
回程路径完全反过来。整个过程里,虚拟机根本不知道宿主机的存在,它只知道自己挂在了一个叫“局域网”的二层网络上。这正是网桥桥接最漂亮的地方。
3. 核心细节解析:tun/tap 设备与网桥的原理
3.1 内核里的一块“假网卡”是怎么造出来的
tun/tap 设备是由内核的tun驱动模块提供的。这个模块会在系统里创建一个控制设备/dev/net/tun,用户态程序通过open()打开它,然后调用ioctl()来创建或连接一个虚拟网卡。整个交互流程大概是:
open("/dev/net/tun", O_RDWR):拿到一个文件描述符。- 准备好一个
struct ifreq,设置ifr_name(比如tap0),并设置IFF_TAP | IFF_NO_PI标志。 - 调用
ioctl(fd, TUNSETIFF, &ifr):告诉内核“我要创建(或打开)一个叫 tap0 的 tap 设备”。 - 之后,
read(fd, buf, len)会从内核收到一个完整的以太网帧;write(fd, buf, len)会向内核发送一个以太网帧。
这里有三个关键点得提醒下:
IFF_NO_PI标志非常重要。默认情况下,从 tun/tap 设备读取的数据前面会带一个 4 字节的“包信息头”(packet info),里面记录了这个包是从哪个协议族来的、是不是 GSO 包等。你如果只是转发原始以太网帧,这个头是多余的,必须用IFF_NO_PI去掉它。网桥不认这个头,带着它直接塞给网桥会导致帧格式错误。- tap 设备创建后默认是 down 状态。必须执行
ip link set tap0 up才能让网桥从它上面收发帧。很多人漏了这一步,结果虚拟机怎么 ping 都不同,但ip link一看,tap0 状态是 DOWN,网桥不会从 DOWN 端口转发任何数据。 - 只要打开
/dev/net/tun描述符的程序退出或关闭描述符,这个 tap 设备就会自动销毁。所以如果你想长时间保持一个虚拟网卡存在,必须有一个进程一直持有这个文件描述符。这也是为什么 QEMU 会自己创建 tap 设备然后持续监控的原因。你要是自己写脚本临时建了个 tap,脚本一退出,tap 就没了,属于正常现象。
3.2 网桥到底是怎么“桥”的
Linux 网桥(bridge)其实是内核里的一个虚拟二层交换机,由bridge模块实现。你可以用ip link add br0 type bridge创建一个网桥,然后用ip link set dev eth0 master br0把物理网卡“塞”进网桥。
从数据处理的角度看,网桥做三件事:
- MAC 地址学习:从某个端口收到帧后,记录源 MAC 地址和端口的对应关系,放进 MAC 地址表。这样以后转发时就能精确找到目标端口。
- 按 MAC 地址表转发:收到帧后查 MAC 表,目标 MAC 在表里就只往对应端口转发;目标 MAC 不在表里,就向除接收端口外的所有端口泛洪。
- 广播/组播处理:目标 MAC 是全 F 的广播帧或者组播帧,直接向所有端口转发。
这里有件事必须强调:网桥本身作为一个网络接口,它自己也有一个 MAC 地址,也能配置 IP 地址。你在br0上配了192.168.1.10/24之后,宿主机访问外部网络的流量就会从br0接口发出。网桥会在底层自动使用它的一个端口来发送这些帧,通常是某个“端口选择策略”选出来的,一般情况下不需要你操心。
网桥的工作方式跟物理交换机几乎一样。你可以把这块虚拟网桥想象成一台 2 口的迷你交换机:一个口连 eth0,一个口连 tap0。虚拟机发来的广播帧会被网桥原样从 eth0 转发到物理交换机,物理交换机再把广播扩散给局域网内其他主机。这就是二层互通的本质。
3.3 为什么 bridge 上要保留 IP,eth0 上却要清掉
这是我见过新手犯错误最多的地方。拿到一块物理网卡 eth0,上面明明配着 IP192.168.1.10,很多人执行完ip link set eth0 master br0,然后发现 SSH 断了,网络不通,一脸懵。
原因很简单:eth0 被加进网桥后,它就从“接口”变成了“端口”。“端口”在 Linux 网络里属于二层概念,它只能干一件事:收发帧,不能处理 IP。而原来的 IP 地址还留在 eth0 上,内核的路由表里也有一条“去 192.168.1.0/24 网段的包从 eth0 出”这样的路由。可是现在 eth0 已经不具备 IP 处理能力了,数据包发不出去,自然网络中断。
解决办法就是做一次 IP 搬家:
# 先把 eth0 的 IP 清掉 ip addr del 192.168.1.10/24 dev eth0 # 把 eth0 加进 br0 ip link set eth0 master br0 # 创建 br0 并在上面配 IP ip link add br0 type bridge ip addr add 192.168.1.10/24 dev br0 # 把两个口都 up 起来 ip link set eth0 up ip link set br0 up注意操作顺序:先建 br0,再清 eth0 的 IP,再把 eth0 加进 br0,最后给 br0 配 IP。这样能最大限度减少网络中断时间。如果顺序反了,先清 IP 再建桥,中间会有几秒钟断网,SSH 操作的话可能就掉线了。
另外,br0和eth0的 MAC 地址尽量保持一致。Linux 网桥接口默认会使用第一个加入网桥的端口的 MAC 地址作为自己的 MAC。如果你先把 eth0 加进 br0,那么 br0 的 MAC 就会自动变成 eth0 的 MAC,这样局域网交换机上看到的 MAC 就不会变,不用重新学习。如果你先建 br0 再往里面加其他设备,br0 的 MAC 可能是随机生成的,那局域网里就会出现一个陌生 MAC 地址,路由器或交换机可能因此要重新更新 ARP 表,极端情况下会短暂丢包。所以建议在创建 br0 后直接显式把 MAC 设为跟 eth0 一样,省心。
# 获取 eth0 的 MAC MAC=$(ip link show eth0 | awk '/ether/ {print $2}') # 创建 br0,并设置 MAC ip link add br0 type bridge ip link set br0 address $MAC3.4 关于 IFF_TAP、IFF_TUN、IFF_NO_PI 的选择问题
如果让你手写代码去操作 tun/tap,ioctl时这几个标志要理解清楚:
| 标志 | 含义 | 咱们的场景 |
|---|---|---|
| IFF_TUN | 创建三层 tun 设备,传输 IP 数据包 | 不用 |
| IFF_TAP | 创建二层 tap 设备,传输以太网帧 | 必须用 |
| IFF_NO_PI | 读写时去掉 4 字节包信息头 | 必须用 |
| IFF_MULTI_QUEUE | 多队列模式,可绑定多个文件描述符 | 一般不用,除非搞多队列性能优化 |
| IFF_VNET_HDR | 使用 virtio 网络头,加速虚拟化场景 | 如果配合 QEMU 高性能虚拟网卡,可以考虑 |
我最常用的是IFF_TAP | IFF_NO_PI。这两个组合在一起,就是一块纯粹的、没有额外头部的二层以太网虚拟网卡。如果你在写 QEMU 的-netdev tap参数,QEMU 内部其实也会自动加上这两个标志,不需要你手动组装。
有一点特别注意:IFF_VNET_HDR这个标志是给虚拟化场景用的。它让 tap 设备支持在帧前附带一个 virtio 网络头,这样虚拟机(特别是使用 virtio-net 驱动的)可以批量收发大数据包,减少内存拷贝。但如果你不是在跑虚拟机,而是在做普通的包转发实验,千万别开这个标志,否则读出来的数据前面会多一个 12 字节的头,普通的 tcpdump 看着都会很奇怪。
4. 实操过程:手把手把虚拟网卡连上新建的网桥
4.1 环境准备与前置检查
开始操作之前,先确认你的宿主机满足三个条件:
- Linux 内核已经加载了
tun模块和bridge模块。绝大多数发行版默认都支持,但有些精简版的嵌入式系统或容器里可能没有。 - 有 root 权限。创建 tap 设备和网桥都涉及系统级网络配置,普通用户搞不了。
- 没有其他程序占用要用的接口名(比如
tap0、br0)。
用下面几条命令快速检查:
# 检查 tun 模块是否可用 ls -l /dev/net/tun # 如果这个文件不存在,尝试加载模块 modprobe tun modprobe bridge # 再确认下 ls -l /dev/net/tun如果/dev/net/tun不存在,而且modprobe tun报错,那恭喜你,可能你的内核编译时把 tun 驱动编译成模块了,但模块没被加载。这种情况在云服务器上很少见,但某些 Docker 容器里就很常见——容器默认没有/dev/net/tun设备,需要在启动容器时加--device=/dev/net/tun参数,并且给容器CAP_NET_ADMIN权限。
另外,在做任何网络变更前,强烈建议你用 screen/tmux 开一个会话,或者确保你有服务器控制台(比如 IPMI、VNC)的访问权限。因为接下来我们要把 eth0 的 IP 搬到 br0 上,一旦操作失误,SSH 会直接断掉。如果你是通过远程 SSH 连服务器,而且没有备用通道,强烈建议先在服务器上开一个 tmux 会话,这样即使 SSH 断了,tmux 里的脚本还会继续执行,不至于半路卡死。
4.2 手工创建 tap 设备并接入网桥
我先把最简单、不依赖任何程序的方式写出来。这种方式适合理解概念,也适合快速搭建测试环境。注意:手工创建的 tap 设备,在没有进程持有它的时候,会在系统重启后消失,而且你也没法直接往里面读写数据(因为没人打开描述符)。但如果你只想让 tap0 作为一个“端口”存在,测试网桥的转发逻辑,那么这种方式足够。
先创建网桥,然后创建 tap,再把他们连起来:
# 1. 创建网桥 br0 ip link add br0 type bridge # 2. 把 eth0 的 IP 搬到 br0(先清后加) ip addr del 192.168.1.10/24 dev eth0 ip link set eth0 master br0 # 3. 给 br0 配置原来的 IP ip addr add 192.168.1.10/24 dev br0 # 4. 创建 tap0,并设为 up ip tuntap add dev tap0 mode tap ip link set tap0 up # 5. 把 tap0 加进 br0 ip link set tap0 master br0 # 6. 把 br0 也 up 起来 ip link set br0 up # 7. 查看最终结果 ip link show br0 ip link show tap0 bridge link执行完以后,bridge link的输出里应该能看到两个接口都挂在 br0 下,状态都是MASTER或UP:
2: eth0 state UP : <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 master br0 state forwarding priority 32 cost 4 3: tap0 state UP : <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 master br0 state forwarding priority 32 cost 100这里有个细节:tap0 在bridge link里的 cost 是 100,eth0 的 cost 可能是 4。cost 值会影响网桥的生成树协议(STP)路径开销,但因为我们这里只有两个口,而且网络里几乎没有环路,STP 基本不会触发,所以不用管。如果以后你搞多个网桥口,而且网络里存在物理环路,那必须考虑 STP,否则广播风暴会刷爆网络。
到这一步,理论上已经有一个“空”的网桥环境了:eth0 和 tap0 被桥接在一起。但由于 tap0 没有用户态程序读写,它就像一个插头没插任何设备的网口,在这个口上不会出现任何数据包。下一步就是让一个用户态进程来“插上”这个口。
4.3 用 QEMU 验证整条链路:让虚拟机接入 tap0
如果你只是想验证网络通不通,最省事的办法是用 QEMU 跑一个极小的 Linux 虚拟机。QEMU 天然支持 tap 设备,它会帮我们管理/dev/net/tun的描述符,只要指定ifname=tap0就行。
先确保你安装了 qemu-system-x86_64。然后准备一个最小的 Linux 镜像(比如 Alpine Linux 的 virt ISO 或者一个已经做好的 cloud 镜像),启动命令大概是:
qemu-system-x86_64 \ -enable-kvm \ -m 512 \ -kernel vmlinuz \ -initrd initrd.img \ -append "console=ttyS0" \ -netdev tap,id=net0,ifname=tap0,script=no,downscript=no \ -device virtio-net-pci,netdev=net0 \ -nographic参数说明:
-netdev tap,id=net0,ifname=tap0,script=no,downscript=no:告诉 QEMU 使用现成的 tap0,而不是自己创建。script=no表示 QEMU 在启动时不要执行默认的/etc/qemu-ifup脚本来配置网卡。因为我们已经手动把 tap0 加进 br0 了,不需要 QEMU 再干预。-device virtio-net-pci,netdev=net0:给虚拟机一块 virtio 网卡,网卡后端连接到 net0 这个 tap 后端。-nographic:直接在终端里看虚拟机控制台。
如果一切正常,虚拟机起来后,你可以在虚拟机里执行:
ip link set eth0 up ip addr add 192.168.1.20/24 dev eth0 ip route add default via 192.168.1.1 dev eth0 ping -c 3 192.168.1.10 # 检查宿主机 ping -c 3 192.168.1.1 # 检查网关只要网关允许同网段的 ICMP,虚拟机应该能直接 ping 通宿主机和网关。
这里有个经验之谈:QEMU 启动时如果 tap0 没有处于 up 状态,网桥默认是不会把流量转发到这个端口上的。你先手动ip link set tap0 up,再启动 QEMU,能省很多排查时间。另外,QEMU 结束以后,tap0 会自动关闭吗?不会。但 QEMU 关闭后,它打开的描述符会关闭,如果这个 tap0 是 QEMU 自己创建的,那么 tap0 会被销毁;但这里我们是用ip tuntap add手工创建的,QEMU 死后 tap0 依然存在,但没有任何程序持有它的描述符,它就变成了一个“僵尸”网口,不再收发数据。你可以继续留着它,也可以把它删掉:
ip link set tap0 nomaster ip link delete tap04.4 用手写 Python 脚本模拟 tap 设备收发帧
如果不想装 QEMU,或者你想更直观地理解“从 /dev/net/tun 读到的是什么”,可以写一个简单的 Python 脚本打开 tap 设备,读取以太网帧并打印出来。下面的脚本基于 Python 的 fcntl 实现 ioctl,不需要装任何第三方库:
#!/usr/bin/env python3 import os import fcntl import struct TUNSETIFF = 0x400454ca IFF_TAP = 0x0002 IFF_NO_PI = 0x1000 # 打开 tun 设备 fd = os.open("/dev/net/tun", os.O_RDWR) # 构造 ifreq 结构:16 字节名字 + 2 字节 flags + 补齐到 40 字节 ifr = struct.pack("16sH", b"tap0", IFF_TAP | IFF_NO_PI) fcntl.ioctl(fd, TUNSETIFF, ifr) print("tap0 opened, waiting for frames...") while True: frame = os.read(fd, 2048) # 简单打印长度和前 14 字节(以太网头) print(f"Got frame: {len(frame)} bytes") print(frame[:14].hex())把脚本保存为tap_reader.py,用 root 权限执行后,它会打开现成的tap0,并持续读取从网桥/虚拟机发过来的以太网帧。此时如果你从虚拟机里 ping 宿主机,脚本里就能看到包含 ARP 请求、ICMP 请求等内容的原始帧。
注意:TUNSETIFF这个 ioctl 的数值在不同体系结构上可能不一样。我给的0x400454ca是针对常见的 x86_64 和 aarch64 的。如果你是交叉编译或者跑在别的架构上,可以通过查询内核头文件获取准确值。做实验的话,用 QEMU 或者直接用tcpdump -i tap0来看帧,其实更方便,不需要自己写代码。
4.5 让配置重启后不失效:写进 systemd 或自定义脚本
上面这些ip命令都是临时的,重启后全部失效。如果你要长期使用这套虚拟网络,得把配置持久化。最简单的做法是写一个 shell 脚本,在开机时执行。
我习惯在/usr/local/sbin/br-setup.sh里放这些内容:
#!/bin/bash # 网桥名、tap名和物理网卡名 BR=br0 TAP=tap0 PHY=eth0 IP=192.168.1.10 MASK=24 GATEWAY=192.168.1.1 # 如果已经存在,就先清理 ip link show $BR >/dev/null 2>&1 && ip link del $BR ip link show $TAP >/dev/null 2>&1 && ip link del $TAP # 创建网桥 ip link add $BR type bridge ip link set $BR address $(ip link show $PHY | awk '/ether/ {print $2}') # 创建 tap ip tuntap add dev $TAP mode tap # 把物理网卡加进网桥 ip link set $PHY master $BR # 把 tap 加进网桥 ip link set $TAP master $BR # IP 从物理网卡挪到网桥 ip addr add $IP/$MASK dev $BR # 把所有接口 up ip link set $PHY up ip link set $TAP up ip link set $BR up # 添加默认路由(如果原来 eth0 是默认路由所在) ip route add default via $GATEWAY dev $BR # 可选:关闭 STP,避免没有必要的拓扑变化 ip link set $BR type bridge stp_state 0你还需要确保这个脚本在系统启动时执行。最简单的做法是把执行命令加进/etc/rc.local,或者创建一个 systemd service:
[Unit] Description=Setup bridge with tap device After=network.target [Service] Type=oneshot ExecStart=/usr/local/sbin/br-setup.sh RemainAfterExit=yes [Install] WantedBy=multi-user.target但是注意,如果这台机器的网络是由 NetworkManager 或 systemd-networkd 管理的,它们可能在你脚本执行后又把 eth0 拉回原来的配置,导致网桥配置被覆盖。这种情况下最稳妥的方案是用 NetworkManager 的 nmcli 创建网桥连接,或者用 systemd-networkd 的 netdev 文件。这些高级配置后面有时间再展开,如果只是实验环境,rc.local 足够了。
4.6 用 tcpdump 验证网桥转发是否正常
配置完成后,不要急着开心,先抓包确认下转发链路正确。在宿主机上分别抓 eth0 和 tap0:
tcpdump -i tap0 -n -e -XX tcpdump -i eth0 -n -e -XX然后在虚拟机里ping -c 1 192.168.1.1。你会发现 tap0 和 eth0 上都能看到同样的 ICMP 请求和应答帧,源 MAC 和目的 MAC 都是完整的以太网地址。这说明网桥把帧从 tap0 原样转到了 eth0,再从 eth0 原样转回了 tap0。
这个验证非常关键,它证实了“虚拟网卡连接到了网桥上”这整条链路的真实性。我见过很多人配置完,虚拟机也能上网,但其实走的是路由/NAT 路径,根本不是桥接,一抓包就露馅了。真正桥接时,你在 tap0 上应该能看到广播帧、ARP 请求,甚至可以看到虚拟机发出的 DHCP 广播。如果这些二层帧都能穿透到物理网络,那你的桥接就完全成功了。
5. 常见问题与排查技巧实录
5.1 为什么 tap0 创建了但网桥一直不转发
这种情况最常见的原因是 tap0 没有 up。网桥驱动在转发逻辑里有一个端口状态判断:只有端口状态为 forwarding(即接口 up 且网桥端口状态 forwarding)时,才会从该端口收发帧。执行ip link set tap0 up之后,端口状态才会从 disabled 变成 forwarding。可以随时用bridge link show查看各端口的转发状态。
另外,IFF_NO_PI没设置也会带来奇怪问题。如果你用的是一个自己写的程序忘记设置这个标志,那么从 tap0 读到的每个包前面都会多 4 个字节,网桥和 tcpdump 会把这些多余的字节当成以太网头的一部分,于是 MAC 地址全部错乱,流量自然不通。这种情况非常隐蔽,因为 tcpdump 还能抓到“包”,但包的头部字段全是乱的。
提示:如果你用 QEMU 启动虚拟机,但网络死活不通,先确认 QEMU 命令行里有没有
script=no。如果不加这个参数,QEMU 默认会调用/etc/qemu-ifup脚本,这个脚本在大多数发行版里根本不存在,于是 tap 设备虽然创建了,但没有被加进网桥,最终虚拟机也无法通信。
5.2 宿主机 IP 迁移后 SSH 断连怎么办
这个问题基本每个人都会碰到一次。如果你在远程机器上做实验,执行ip addr del 192.168.1.10/24 dev eth0的瞬间,你的 SSH 会话就会因为网络路径失效而卡住。为什么要先说这个问题?因为一旦 SSH 断开,你没法继续在终端里执行后续命令。
预防措施分两步:
- 操作前一定开 tmux/screen。哪怕 SSH 断了,tmux 里的命令序列还会继续执行。
- 如果已经断了,只能用带外手段(服务器控制台、IPMI、VNC)重新登录,然后执行修复。
修复思路很简单:只要把 IP 配回 br0,并把默认路由指到 br0,网络就恢复了。
ip addr add 192.168.1.10/24 dev br0 ip route add default via 192.168.1.1 dev br0还有一种更稳的做法:不要把 eth0 的 IP 直接删掉,而是先用一个脚本把 IP 和路由都记下来,等 br0 创建好后再原样恢复。或者干脆用ip addr change把地址改绑,但能用脚本解决的别折腾,直接上 tmux 最省心。
5.3 ARP 缓存混乱导致虚拟机 ping 不同网关
如果你把虚拟机的 IP 设成192.168.1.20,网关是192.168.1.1,宿主机 IP 是192.168.1.10,正常情况下三者之间 ARP 都能正常解析。但如果你之前给 tap0 或者 br0 配过别的 IP,或者虚拟机 IP 与其他设备冲突,就会出现“能 ping 通宿主机,但 ping 不通网关”的现象。
排查方法很简单:
# 在虚拟机里查看 ARP 表 ip neigh # 在宿主机上查看 ARP 表 ip neigh如果 ARP 表里网关的 MAC 地址不对,或者状态是 FAILED,先手动清空再重新触发:
ip neigh flush all还有一种更隐蔽的情况:网桥默认开启了“mac address learning”,但如果你在两个不同的端口上看到了同一个 MAC 地址(比如虚拟机的 MAC 和宿主机的 MAC 一样),网桥会不断更新 MAC 表,导致数据帧在两个端口之间来回震荡。这种问题常见于从模板克隆虚拟机时没重新生成 MAC 地址。解决方法是确保每台虚拟机的 MAC 地址是唯一的。拿 QEMU 来说,最好显式给每块虚拟网卡指定一个全局唯一 MAC:
qemu-system-x86_64 ... -device virtio-net-pci,mac=52:54:00:12:34:56,netdev=net05.4 删除网桥和 tap 的干净姿势
实验完了,想把环境恢复原状,千万别直接ip link delete br0就走人。顺序很重要:
- 先把物理网卡从网桥摘下来。
- 再删 tap0。
- 最后删网桥,并把 IP 恢复到 eth0。
# 1. 把 eth0 移出网桥 ip link set eth0 nomaster # 2. 删除 tap0 ip link set tap0 nomaster ip link del tap0 # 3. 删除网桥 ip link del br0 # 4. 给 eth0 重新配 IP 和路由 ip addr add 192.168.1.10/24 dev eth0 ip route add default via 192.168.1.1 dev eth0 ip link set eth0 up如果直接删网桥,eth0 会自动脱离网桥,但 eth0 上可能残留master br0的配置,导致后续配置混乱。所以最好还是按顺序来。另外,删除 tap0 之前,确保没有 QEMU 或自定义脚本还占着它的描述符,否则删除会报Device or resource busy。这时先杀掉持有该设备进程,再删除。
5.5 虚拟网卡在容器环境里创建失败
你在 Docker 容器里跑同样命令,大概率碰到Cannot open device /dev/net/tun或Operation not permitted。这是因为容器默认没有/dev/net/tun设备节点,也没有CAP_NET_ADMIN能力。需要在启动容器时显式加上:
docker run -it --rm \ --device=/dev/net/tun \ --cap-add NET_ADMIN \ ubuntu:22.04 bash如果容器里还要创建网桥,那还需要--cap-add NET_RAW,并且可能要--network host模式。在容器内搞网络虚拟化不推荐,除非你真的知道自己在干什么。我建议把 tun/tap 和网桥的测试放在宿主机或专门的虚拟机里做,容器环境限制太多,排查起来非常头疼。
5.6 为什么从 tap0 抓到重复的广播包
在网桥 + tap 的场景里,如果在宿主机上用 tcpdump 抓 tap0,同时虚拟机也在跑 tcpdump,你可能抓到一模一样的广播帧,感觉自己看到了环路。其实这不是环路,是正常的:广播帧本来就会被网桥泛洪到所有端口。虚拟机发出的同一个广播帧,会在 tap0 上被抓到一次,如果网桥把它转发给了 eth0,物理网络上也会出现一次,你再从 eth0 上抓也能看到。这属于二层广播的正常行为,不用慌。
但是,如果你在 tap0 和 eth0 上都抓到了同一个单播帧且重复出现多次,那可能是网桥的 MAC 表学习出了问题,或者真的有环路。检查方法:bridge fdb show查看 MAC 表,确认同一个 MAC 是否对应多个端口。如果有怀疑,可以临时开启 STP 观察一段时间。
6. 踩坑后的独家心得
6.1 不要忽略 MTU 问题
tap 设备默认 MTU 是 1500,桥接后,网桥的 MTU 会自动选择端口范围内的最小值。如果你在物理网卡上启用了 jumbo frame(比如 MTU 9000),那你在 tap0 上也最好把 MTU 设成对应的值,否则虚拟机发出的 9000 字节大帧,到了 tap0 上就会被分片或丢弃,表现为“能 ping 通但 ‘ping -s 8000’ 不通”。遇到这种情况,检查一下全链路接口的 MTU,保持一致即可:
ip link set tap0 mtu 9000 ip link set br0 mtu 90006.2 用bridge link而不是brctl show排查端口状态
老教程里喜欢用brctl,但现在的发行版基本都改用ip命令和bridge命令了。bridge link show能看到每个端口的上线状态、转发状态、cost,比brctl输出信息更详细。如果你排查问题,第一步永远是bridge link和ip addr。
6.3 网桥转发性能比大部分人想象得好
很多人以为软件网桥性能很差。实际上 Linux 内核的 bridge 转发路径已经优化得很不错,在没有大量小包冲击的情况下,跑满千兆是没问题的。如果你在做高性能转发实验,建议开启多队列 tap(IFF_MULTI_QUEUE),并配合smp_affinity把中断分配到多个 CPU 核心上。不过对于一般的虚拟化场景,单队列已经足够。
6.4 如果你还要跑 KVM,记得给虚拟机网卡开 vhost-net
虽然咱们这篇讲的是手工搭桥,但既然你已经在玩 KVM/QEMU,顺手提一句:如果用-netdev tap配合 virtio 网卡,QEMU 其实可以开启 vhost-net 加速,把数据包收发从 QEMU 用户态下沉到内核态,网络性能会有明显提升。开启方式很简单:
qemu-system-x86_64 \ -netdev tap,id=net0,ifname=tap0,script=no,downscript=no,vhost=on \ -device virtio-net-pci,netdev=net0这个选项不影响桥接逻辑,只影响虚拟机的数据通路性能,对实验环境来说属于锦上添花。
6.5 保留一条“逃生通道”
最后一条经验不是技术,是习惯。在我做这类网络配置之前,必定会做两件事:开 tmux,确认物理机上有另一个能登录的会话。因为配置网桥、切换 IP、改路由这些操作,任何一个失误都可能导致远程连接直接断掉。宁可多花三十秒开个 tmux,也不要等断了再求运维帮忙。这个习惯帮我免掉了无数次“手抖删错 IP”的尴尬。
整套操作下来,你会对 Linux 的网络协议栈有一个全新的认识:一块“网卡”本质上就是一个收发数据包的接口,tun/tap 只是把“接口”的另一头从物理线缆换成了一个文件描述符。网桥也没那么神秘,它就是内核里的一台虚拟交换机。当你亲眼看到虚拟机发出的 ARP 广播穿越网桥到达物理网络的时候,那种“把虚拟世界拉进现实”的感觉,还是很有成就感的。