
1. 项目概述1.1 这项目到底在做什么最近在折腾 Linux 网络虚拟化碰到一个特别典型的场景我在一台没有多余物理网卡的服务器上想跑一个 Linux 虚拟机做网络实验要求虚拟机跟宿主机处在同一个二层网络里虚拟机要能跟宿主机互访还要能跟局域网里的其他机器正常通信。手头没有物理交换机也没法直接在宿主机上插第二块网卡那就得靠 Linux 自带的虚拟化网络功能把这层“桥接网络”搭出来。方案很直接在宿主机上用ip tuntap创建一个 tun/tap 设备把它当成一块“虚拟物理网卡”接进系统再创建一个 Linux bridge网桥把这个虚拟网卡和宿主机真实的物理网卡一起“塞”进网桥里。这样虚拟网卡就相当于通过网桥跟物理网卡连在了一起虚拟机进程把数据包写到虚拟网卡上网桥就会像一台二层交换机一样把包从物理网卡转发出去。整个链路是通的虚拟机在网络层面就跟一台直接插在局域网交换机上的物理机没有区别。这活儿听起来简单但实际操作里涉及的东西其实不少tun/tap 驱动是内核里怎么工作的、网桥接口为什么需要 up、ARP 表为什么会乱、虚拟机进程该把哪个接口当作自己的网卡……每一项都有讲究。这篇文章就完全围绕“Linux 上用 tun/tap 虚拟网卡 网桥实现二层互通”这个主题把原理、步骤、踩坑全部展开适合正在学习 Linux 网络虚拟化、打算玩 KVM/QEMU 虚拟网络、或者被公司里“我没物理交换机但想搭个虚拟网络”这类需求卡住的朋友。1.2 先说清楚你能得到什么读完这篇文章你能完完整整复现出下面这个网络拓扑宿主机有一个真实物理网卡eth0IP 为192.168.1.10/24网关192.168.1.1。创建一个网桥br0把eth0加进br0。创建一个 tap 设备tap0也加进br0。一个用户态进程比如 QEMU 虚拟机或者你手写的一个从/dev/net/tun读写数据包的程序绑定tap0。虚拟机内部配置 IP192.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 地址说明宿主机物理网卡eth0192.168.1.10/24原有物理网卡后面要加进 br0宿主机网桥br0192.168.1.10/24建好后 IP 从 eth0 移到 br0 上虚拟网卡tap0不配 IPtap 设备本身不配 IP只作二层口虚拟机网卡eth0192.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 上要保留 IPeth0 上却要清掉这是我见过新手犯错误最多的地方。拿到一块物理网卡 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/tapioctl时这几个标志要理解清楚标志含义咱们的场景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或UP2: 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 是 100eth0 的 cost 可能是 4。cost 值会影响网桥的生成树协议STP路径开销但因为我们这里只有两个口而且网络里几乎没有环路STP 基本不会触发所以不用管。如果以后你搞多个网桥口而且网络里存在物理环路那必须考虑 STP否则广播风暴会刷爆网络。到这一步理论上已经有一个“空”的网桥环境了eth0 和 tap0 被桥接在一起。但由于 tap0 没有用户态程序读写它就像一个插头没插任何设备的网口在这个口上不会出现任何数据包。下一步就是让一个用户态进程来“插上”这个口。4.3 用 QEMU 验证整条链路让虚拟机接入 tap0如果你只是想验证网络通不通最省事的办法是用 QEMU 跑一个极小的 Linux 虚拟机。QEMU 天然支持 tap 设备它会帮我们管理/dev/net/tun的描述符只要指定ifnametap0就行。先确保你安装了 qemu-system-x86_64。然后准备一个最小的 Linux 镜像比如 Alpine Linux 的 virt ISO 或者一个已经做好的 cloud 镜像启动命令大概是qemu-system-x86_64 \ -enable-kvm \ -m 512 \ -kernel vmlinuz \ -initrd initrd.img \ -append consolettyS0 \ -netdev tap,idnet0,ifnametap0,scriptno,downscriptno \ -device virtio-net-pci,netdevnet0 \ -nographic参数说明-netdev tap,idnet0,ifnametap0,scriptno,downscriptno告诉 QEMU 使用现成的 tap0而不是自己创建。scriptno表示 QEMU 在启动时不要执行默认的/etc/qemu-ifup脚本来配置网卡。因为我们已经手动把 tap0 加进 br0 了不需要 QEMU 再干预。-device virtio-net-pci,netdevnet0给虚拟机一块 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, btap0, 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(fGot 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名和物理网卡名 BRbr0 TAPtap0 PHYeth0 IP192.168.1.10 MASK24 GATEWAY192.168.1.1 # 如果已经存在就先清理 ip link show $BR /dev/null 21 ip link del $BR ip link show $TAP /dev/null 21 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] DescriptionSetup bridge with tap device Afternetwork.target [Service] Typeoneshot ExecStart/usr/local/sbin/br-setup.sh RemainAfterExityes [Install] WantedBymulti-user.target但是注意如果这台机器的网络是由 NetworkManager 或 systemd-networkd 管理的它们可能在你脚本执行后又把 eth0 拉回原来的配置导致网桥配置被覆盖。这种情况下最稳妥的方案是用 NetworkManager 的 nmcli 创建网桥连接或者用 systemd-networkd 的 netdev 文件。这些高级配置后面有时间再展开如果只是实验环境rc.local 足够了。4.6 用 tcpdump 验证网桥转发是否正常配置完成后不要急着开心先抓包确认下转发链路正确。在宿主机上分别抓 eth0 和 tap0tcpdump -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 命令行里有没有scriptno。如果不加这个参数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 来说最好显式给每块虚拟网卡指定一个全局唯一 MACqemu-system-x86_64 ... -device virtio-net-pci,mac52:54:00:12:34:56,netdevnet05.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 转发路径已经优化得很不错在没有大量小包冲击的情况下跑满千兆是没问题的。如果你在做高性能转发实验建议开启多队列 tapIFF_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,idnet0,ifnametap0,scriptno,downscriptno,vhoston \ -device virtio-net-pci,netdevnet0这个选项不影响桥接逻辑只影响虚拟机的数据通路性能对实验环境来说属于锦上添花。6.5 保留一条“逃生通道”最后一条经验不是技术是习惯。在我做这类网络配置之前必定会做两件事开 tmux确认物理机上有另一个能登录的会话。因为配置网桥、切换 IP、改路由这些操作任何一个失误都可能导致远程连接直接断掉。宁可多花三十秒开个 tmux也不要等断了再求运维帮忙。这个习惯帮我免掉了无数次“手抖删错 IP”的尴尬。整套操作下来你会对 Linux 的网络协议栈有一个全新的认识一块“网卡”本质上就是一个收发数据包的接口tun/tap 只是把“接口”的另一头从物理线缆换成了一个文件描述符。网桥也没那么神秘它就是内核里的一台虚拟交换机。当你亲眼看到虚拟机发出的 ARP 广播穿越网桥到达物理网络的时候那种“把虚拟世界拉进现实”的感觉还是很有成就感的。