KVM 网络配置:Linux Bridge 与虚拟机互通

使用 Linux Bridge 将 KVM 虚拟机接入物理网络,并说明桥接、NAT 和 VLAN 的选择。

技术文章约 17 分钟220 次阅读

#这篇文章解决什么问题

虚拟机要"接入网络",归结到底只有一个问题:让虚拟机产生的以太网帧能到达物理网络的另一端,也让物理网络的帧能回到虚拟机。这个问题的底座是二层(链路层)的 MAC 学习与帧转发机制——Linux 内核里有一个学习型二层交换机(Linux Bridge),它把虚拟机的虚拟网卡和宿主机物理网卡当作同一台交换机上的端口,依靠一张"MAC 地址 → 端口"的转发表(FDB,Forwarding Database)决定每一帧从哪个端口出去。NAT、Bridge、VLAN、macvtap 这些方案本质上都是"让帧走哪条路"的不同选择,先把帧的流转讲透,再去配网就不会只是背命令。

#底层机制:帧是如何被学习和转发的

#二层转发三件事:学习、查表、泛洪

Linux Bridge(内核模块 bridge)在功能上等价于一台 802.1D 学习型交换机,它维护一张 FDB 表,记录「MAC 地址 → 端口」的映射。一个帧从某个端口进入桥时,内核做三件事:

  1. 学习(Learning):记下帧的源 MAC 来自哪个端口,更新或新建 FDB 表项。
  2. 查表(Lookup):按帧的目的 MAC 查 FDB。
  3. 转发/泛洪(Forwarding/Flooding):查到了就只往对应端口发;查不到(未知单播)、广播或组播帧,就往除入端口外的所有端口发。

这套机制有两个值得注意的性质:

  • MAC 老化:FDB 表项不是永久的。Linux 桥默认的老化时间是 300 秒(5 分钟),一个 MAC 在期限内不再出现就会被删除。所以"学过的 MAC 又不见了"多半不是故障,而是老化。
  • 端口混杂模式(promiscuous):桥为了保证能收到所有帧,会让它管理的端口处于混杂模式,ip link show eno1 时能看到 PROMISC 标志——这是桥的正常行为,不是网卡被"劫持"。

#一帧从虚拟机走到物理网络的完整路径

以 virtio 网卡加 Linux Bridge 的典型栈为例,帧的旅程是:

  1. 客户机(Guest)里的应用把数据交给内核协议栈,从 virtio-net 虚拟网卡发出。virtio 是半虚拟化设备:客户机直接往共享内存环(virtqueue)里投递描述符,绕过了逐字节的设备模拟,这是它比 e1000 等全模拟网卡快得多的原因。
  2. QEMU 从共享环里取走帧,写入一个用户态可写的文件描述符——这个 fd 连接着宿主内核里的一个 tap 设备。
  3. 宿主内核把 tap 设备收到的数据当作普通以太网帧:帧"从 vnet0 接口进入"。
  4. vnet0 是 br0 的端口,桥按上面说的学习/查表/泛洪流程处理帧。如果目的 MAC 在其他 VM 的 vnet 端口上,帧在桥内部就转了;如果要出物理网络,帧从 eno1 端口发出去。
  5. eno1 的对端是物理交换机,交换机用同样的 MAC 学习机制,把帧送往真正的目标机器。

QEMU 每启动一台接桥的虚拟机,libvirt 就会自动创建一个名为 vnet0、vnet1…… 的 tap 设备并把它加入指定的桥(bridge link show 能看到)。手动等价操作是 ip tuntap add dev tap0 mode tap,但用 libvirt 时不需要自己建。

#为什么是 tap 而不是 tun

Linux 提供两类用户态虚拟网卡:tun 工作在三层,收发的是 IP 包,没有以太网头;tap 工作在二层,收发的是完整以太网帧。桥要做的是二层转发,必须看到 MAC 地址,所以只能用 tap。这解释了为什么 QEMU 桥接模式下的设备叫 tap/vnetN,而不是 tun——tun 对应的是三层隧道的场景(如 OpenVPN)。

#为什么桥接后 VM 与宿主机在同一个二层

关键在"宿主机 IP 放在哪"。桥接模式下,物理网卡 eno1 只是桥的一个端口,不再持有 IP;宿主机自己的 IP 地址落在 br0 这个虚拟的桥接口上。于是:

  • 宿主机和所有虚拟机都是 br0 的端口,或通过 br0 可达的主机;
  • 对物理交换机来说,宿主机物理端口后面挂着好几个 MAC 地址(br0 的 MAC 加每台 VM 的 MAC),交换机把它们都学到 eno1 对应的那个端口上;
  • VLAN、广播域、可达性全部由二层决定,VM 拿到局域网 IP 后,跟一台真实接在交换机上的机器没有任何区别。

所以"桥接让 VM 与宿主机在同一二层"不是概念口号,而是这个拓扑的直接结果:大家都在同一个桥(同一个广播域)里,互相用 MAC 通信,IP 只是在这之上分配的地址。另外注意:桥接是二层行为,不需要开启 IP 转发(net.ipv4.ip_forward = 1 只在做三层路由或 NAT 时才需要)——很多人误以为桥接也要开转发。

#三种接入方式的本质区别

#QEMU user 网络(SLIRP):纯用户态软件 NAT

-netdev user(旧写法 -nic user)是 QEMU 内置的用户态网络,代码名叫 SLIRP。它不创建任何宿主内核设备,NAT、DHCP、ARP 应答全在 QEMU 进程的用户态里模拟:

  • 客户机默认拿到 10.0.2.0/24 网段的地址(客户机 10.0.2.15、网关 10.0.2.2、DNS 10.0.2.3 都是虚拟出来的);
  • 客户机发出的包由 SLIRP 做地址转换后,以 QEMU 进程的身份从宿主机真实网络发出;
  • 因为不需要 root、不需要配置宿主网络,它是最省事的"能上网就跑"方案,适合本地测试。

它的边界也很硬:外部无法主动连入客户机(只能靠 hostfwd 做端口转发,如 -nic user,hostfwd=tcp::2222-:22);10.0.2.0/24 这个网段只存在于 QEMU 进程内部,宿主机上没有对应网卡,所以宿主机也无法直接 ping 通客户机;用户态转发性能有限,ICMP 等协议支持也不完整。

#libvirt 默认网络:宿主内核 NAT + dnsmasq

libvirt 安装后默认有一个叫 default 的 NAT 网络(也就是原稿说的"libvirt 默认的 default 网络")。它是宿主内核层面的 NAT,和 SLIRP 不是一回事:

  • libvirt 创建 virbr0 桥,默认 IP 192.168.122.1/24;
  • 内置 dnsmasq 给 VM 发 192.168.122.0/24 网段的 DHCP 地址;
  • 用 iptables/nftables 的 MASQUERADE(源地址伪装)把 VM 流量转发出去;
  • 因为在宿主内核里有 virbr0 这个真实接口,宿主机可以直接 ping 通 VM,这点和 SLIRP 不同。

零配置、自带隔离,但外部同样连不进来——要暴露服务得配端口转发,长期跑生产并不省心。

#Linux Bridge:二层直连

把 VM 的虚拟网卡桥到物理网卡,VM 直接出现在局域网,外部可以直接访问,没有 NAT 的转发损耗。生产环境最常见的选择,也是本文操作部分的主角。

#macvtap:桥的轻量变体

macvtap 直接让虚拟机网卡挂在物理网卡上,省掉一个桥设备,性能好;但它最大的坑是宿主机和虚拟机之间不能直接通信(宿主侧的 macvtap 接口不参与本地流量的收发),且依赖物理网卡支持。不常用,遇到"桥接后宿主机 ping 不通 VM"时先想想是不是选错了它。

#选型对照

方案虚拟机是否在局域网可否被外部主动访问宿主机↔VM 直连性能适用
QEMU user 网络(SLIRP)否(私有 10.0.2.0/24)否(只能 hostfwd)否低本地测试、无 root 环境
libvirt default(NAT)否(私有 192.168.122.0/24)否(需端口转发)是中开发、隔离环境
Linux Bridge是是是高生产、对外服务
macvtap是是否高特殊场景,一般不用

#完整可复现操作:RHEL/Rocky 上用 Linux Bridge 让 VM 接入局域网

#前置条件

  • 发行版:RHEL 8/9 或 Rocky Linux 8/9(本文以 Rocky Linux 9 + NetworkManager 1.4x 为例,命令在 RHEL 8/9 通用);
  • 已安装虚拟化组件:libvirt、qemu-kvm、virt-install、virt-viewer(或 virt-manager);
  • 物理网卡命名以 eno1 为例,先记录它当前是否配了 IP;
  • 重要:如果是远程服务器,务必保留带外通道(厂商控制台、IPMI/iDRAC、VNC 或第二个 SSH 会话),配桥期间一旦网卡配置出错,SSH 会立刻断开——原稿作者见过太多人远程配桥把自己锁在服务器外;
  • 物理交换机端口允许 MAC 学习(不要开 port-security 的单 MAC 限制)。

先看清现状:

nmcli device status
nmcli connection show
ip -br addr show eno1

记录 eno1 现有的连接名(比如 Wired connection 1)和 IP,后面要用。

#第一步:创建桥 br0 并把物理网卡挂进去

核心思路:eno1 变成桥的端口,不再持有 IP;宿主机 IP 挪到 br0 上。这是和"双网卡叠加"最大的区别,很多人卡在这里。

# 1) 创建桥 br0
sudo nmcli connection add type bridge ifname br0 con-name br0

# 2) 把物理网卡 eno1 作为桥端口(eno1 不再有独立 IP)
sudo nmcli connection add type ethernet ifname eno1 master br0 con-name eno1-bridge

# 3) 把原来的静态 IP 配置挪到桥接口上(按你的实际网段修改)
sudo nmcli connection modify br0 ipv4.method manual \
  ipv4.addresses 192.168.1.10/24 \
  ipv4.gateway 192.168.1.1 \
  ipv4.dns 192.168.1.1

# 4) 激活
sudo nmcli connection up eno1-bridge
sudo nmcli connection up br0

如果 eno1 上原来有一个激活中的连接(还记得上一步记下的 Wired connection 1 吗?),它会和 eno1-bridge 抢设备,必须停掉或删除:

# 看现在的激活状态
nmcli connection show

# 停掉旧连接(名字以实际为准);确认不再需要后再删除
sudo nmcli connection down 'Wired connection 1'

想改用 DHCP 的话,把第 3 步换成 sudo nmcli connection modify br0 ipv4.method auto 即可。

确认桥起来了:

ip -br link show br0
ip -br addr show br0

#第二步:地址规划

桥接模式下 VM 的地址规划和一台真机完全一样:

  • 手动分配:VM1=192.168.1.11/24、VM2=192.168.1.12/24,网关 192.168.1.1,DNS 192.168.1.1;
  • 用 DHCP:确认 DHCP 服务器认识 VM 的 MAC(或先绑定 MAC 再分配),否则会出现"VM 拿不到 IP"的经典问题。

宿主机不会再从 eno1 上取地址,它只会出现在 br0 的 192.168.1.10/24 上。此时拓扑是:

物理局域网 192.168.1.0/24(网关 192.168.1.1)
        |
+-------+------------------------------------------+
|        宿主机                                     |
|   eno1  (桥端口,本身不配 IP)                   |
|     +---[ br0 ]--- 192.168.1.10/24(宿主机 IP)   |
|       +-----+-----+                              |
|      vnet0      vnet1                            |
+-------+---------+--------------------------------+
        |         |
    +---+---+  +---+---+
    |  VM1  |  |  VM2  |
    +-------+  +-------+
   192.168.1.11  192.168.1.12

在 VM 里手工配 IP(以 VM 内实际网卡名为准,瞬时生效,重启前请写入客户机自身的网络配置):

sudo ip addr add 192.168.1.11/24 dev eth0
sudo ip link set eth0 up
sudo ip route add default via 192.168.1.1

VM 里的虚拟机网卡选 virtio 模型(<model type='virtio'/>),半虚拟化性能最好;默认的 e1000 是全模拟,能跑但浪费。

#第三步:把 VM 接上桥

新建 VM 的 virt-install 完整示例:

sudo virt-install \
  --name web01 \
  --memory 2048 \
  --vcpus 2 \
  --disk path=/var/lib/libvirt/images/web01.qcow2,size=20,format=qcow2 \
  --os-variant rhel9 \
  --network bridge=br0,model=virtio \
  --cdrom /var/lib/libvirt/images/rhel-9.4-x86_64-dvd.iso \
  --graphics vnc,listen=0.0.0.0

已有 VM 要改成桥接,离线修改最稳:

sudo virsh shutdown web01
sudo virsh edit web01
sudo virsh start web01

virsh edit 打开的是该 VM 的 domain XML,把网卡段从 NAT 的 network 类型改成桥的 bridge 类型:

<!-- 改之前(NAT,走 libvirt default 网络) -->
<interface type='network'>
  <source network='default'/>
  <model type='virtio'/>
</interface>
<!-- 改之后(桥接,直接进 br0) -->
<interface type='bridge'>
  <source bridge='br0'/>
  <model type='virtio'/>
</interface>

如果只想临时加一块测试网卡,不必改 XML:

sudo virsh attach-interface web01 bridge br0 --model virtio --live --config

(--config 表示写成持久配置,重启 VM 后仍在。)

#第四步:完整走一遍 libvirt 网络 XML

除了在 VM XML 里直接写 <interface type='bridge'>,更"libvirt 化"的做法是先定义一个网络对象,再让 VM 引用它。比如定义 bridge-br0 这个网络,forward 模式指到宿主机的 br0:

<network>
  <name>bridge-br0</name>
  <forward mode='bridge'/>
  <bridge name='br0'/>
</network>

注意:forward mode='bridge' 意味着 libvirt 不负责 IP 和 DHCP,br0 必须已经存在并配好地址(即第一步做完的状态)。把上面的 XML 存成 /etc/libvirt/qemu/networks/bridge-br0.xml 后:

sudo virsh net-define /etc/libvirt/qemu/networks/bridge-br0.xml
sudo virsh net-start bridge-br0
sudo virsh net-autostart bridge-br0
sudo virsh net-list --all

之后 VM 就可以用 <source network='bridge-br0'/> 或 --network network=bridge-br0,model=virtio 接入。想对比实现差异的话,看一眼 libvirt 自带的 NAT 是怎么组织的(/etc/libvirt/qemu/networks/default.xml):它就是 <forward mode='nat'/> 加 dnsmasq DHCP,这正是"默认网络"和"桥接网络"在 libvirt 配置层的分野。

#第五步:跨 VLAN 场景

VLAN 的关键在物理交换机端口的模式:

  • access(不打 tag):交换机端口已经在某个 VLAN 里,桥内所有 VM 都归属该 VLAN,宿主机无需任何 VLAN 配置;
  • trunk(打 tag 放行多个 VLAN):需要在宿主机侧把 tagged 流量和桥接结合起来,两种做法。

方式一:VLAN 子接口作为桥端口(对应 NetworkManager 的 type=vlan)。VLAN 10 的例子,把子接口 eno1.10 而不是裸 eno1 挂进桥(若此前已把裸 eno1 挂进 br0,先执行 sudo nmcli connection delete eno1-bridge 再开始):

sudo nmcli connection add type bridge ifname br0 con-name br0
sudo nmcli connection add type vlan ifname eno1.10 vlan.id 10 vlan.parent eno1 con-name eno1.10
sudo nmcli connection add type ethernet ifname eno1.10 master br0 con-name eno1.10-bridge
sudo nmcli connection modify br0 ipv4.method manual \
  ipv4.addresses 192.168.1.10/24 \
  ipv4.gateway 192.168.1.1 \
  ipv4.dns 192.168.1.1
sudo nmcli connection up eno1.10-bridge
sudo nmcli connection up br0

方式二:启用内核的 VLAN 过滤(vlan_filtering),在桥内部切分 VLAN:

sudo ip link set br0 type bridge vlan_filtering 1

# 物理端口:允许 tagged VLAN 10 进出(对端交换机 trunk)
sudo bridge vlan add dev eno1 vid 10

# VM 端口(vnet0 在 VM 启动后出现):属于 VLAN 10,且不打 tag
sudo bridge vlan add dev vnet0 vid 10 pvid untagged

无论哪种方式,桥的配置和对端交换机侧的 access/trunk 配置必须一致,否则帧会"进得来出不去"。默认每个端口还保留着 VLAN 1 的表项,严格隔离时再用 bridge vlan del 收紧要放行的 VLAN。另外注意:方式二里 vnet0 是 VM 启动后才存在的设备,VLAN 命令要在 VM 运行后再执行。

#验证:怎么确认链路是对的

#1. 桥与端口状态

# 桥本体状态
ip -br link show br0

# 列出所有设备,master br0 的就是桥端口
bridge link show

预期 br0 状态 UP,eno1、vnet0 都是 master br0,状态 forwarding。典型输出(MAC 因环境而异):

br0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 ...
8: eno1: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 master br0 state forwarding priority 32 cost 4
9: vnet0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 master br0 state forwarding priority 32 cost 100

老工具 brctl show(bridge-utils 包)也能看到同样的端口列表,新环境更常用 bridge(iproute2 提供)。

#2. 看 FDB:内核到底学到了什么

这是验证"桥在工作"最直接的证据——每台 VM 一通信,FDB 里就该出现它的 MAC 与端口绑定:

bridge fdb show br br0

典型输出里,permanent 是 br0 自身的表项,master br0 的才是学来的:

52:54:00:12:34:56 dev vnet0 master br0
1c:69:7a:aa:bb:cc dev eno1 master br0
33:33:00:00:00:01 dev br0 self permanent

第一行说明 VM 的 MAC 52:54:00:12:34:56 在 vnet0 上被学到了(桥内转发可用);第二行说明局域网里别的设备 MAC 是从 eno1 学到的(出外网路径存在)。如果 VM 发过包但 FDB 里始终没有它,说明帧根本没进桥,问题在 VM 侧或 tap 设备。

#3. 连通性测试矩阵

# 宿主机 → VM
ping -c 3 192.168.1.11

# 在 VM 里:VM → 宿主机、网关、外网
ping -c 3 192.168.1.10
ping -c 3 192.168.1.1
ping -c 3 223.5.5.5

# 从宿主机开 SSH 进 VM 再测外网(顺带验证 TCP 与网关转发)
ssh 192.168.1.11 'ping -c1 223.5.5.5'

再从局域网里任意一台机器看地址解析,确认 VM 的 MAC 是被"正常学习"而不是被 NAT 隐藏的:

ip neigh show | grep 192.168.1.11

如果看到的是 52:54:00 开头的 MAC(QEMU 默认厂商 OUI),说明 VM 确实以二层身份暴露在局域网中,桥的目标达成。

#4. 判定标准

  • br0、eno1、vnet0 状态均为 UP,且后两个 master br0、状态 forwarding;
  • FDB 里同时存在 vnet0 和 eno1 学到的新表项;
  • 四个方向的连通(host↔VM、VM↔网关、VM↔外网)全部通过;
  • 局域网其他机器能看到 VM 的真实 MAC。

#边界与坑

1. 远程配桥把 SSH 锁死。 改桥的瞬间 eno1 上旧 IP 会消失,新配置若失败就会失联。必须保留带外通道,或把配置写成脚本随时能回滚。这是原稿里最重的一条经验,别在没后路的机器上折腾。

2. 虚拟机起不来,报 bridge br0: no such device。 桥没建成功或没激活。先查 nmcli connection show br0、ip link show br0;NetworkManager 的桥连接创建后要 up 才会真正生成设备。

3. VM 拿不到 DHCP 地址。 可能性按频率排:物理交换机端口开了 port-security 或只允许单 MAC(端口的 MAC 学习被限制);DHCP 服务器不认识 VM 的新 MAC(绑定了旧地址或地址池耗尽)。排查时先手动给 VM 配静态 IP 验证二层通不通,能通就是 DHCP 环节的问题。

4. 宿主机 ping 不通 VM,但 VM 能出网。 二层是通的,问题在三层以上的过滤——重点查 firewalld 的 FORWARD 链,桥接流量可能被默认策略拦截。把桥放进受信 zone:

sudo firewall-cmd --permanent --zone=internal --change-interface=br0
sudo firewall-cmd --reload

注意 zone 名称和默认策略在发行版之间略有差异,改完立刻重测。

5. MAC 冲突/漂移。 别手动给两台 VM 复制同一个 MAC,FDB 会在两个端口之间来回抖动,丢包是必然的。libvirt 生成的 52:54:00 开头 MAC 是稳定的,保持默认即可。

6. 物理网卡还带着 IP 又进了桥。 IP 放在 br0 上、eno1 裸进桥是唯一干净的形态;有人图省事把 IP 留在 eno1 上,会出现路由表混乱、外部回包找不到正确设备的怪问题。再次强调:物理网卡本身不设 IP,IP 全在桥上。

7. macvtap 误导。 想用它的轻量,但要接受宿主机与 VM 不能直连;原稿说这是"最大的坑",实际选型中多数情况选 Linux Bridge 更稳。

8. 无线网卡不适合做桥底。 多数无线驱动/AP 只认一个 MAC(客户端模式),桥接会直接断网或用不了;桥接要建立在有线物理口上。

9. MTU 不一致。 br0、各端口、VM 网卡和对端交换机的 MTU 必须一致,否则大帧被静默丢弃,表现为"小包通、大包不通"。需要调整时在 br0 和各端口统一设(如 sudo ip link set br0 mtu 9000),前提是整条链路都支持。

10. 忘开 STP 又出现环路。 Linux Bridge 默认关闭 STP(stp_state 0)。单桥无冗余链路时不需要开;但只要出现两条以上通往同一处的路径(比如双网卡、多交换机互联),必须开 STP 防广播风暴:sudo ip link set br0 type bridge stp_state 1。

11. 生产环境把流量全塞进一个桥。 多网卡/多 VLAN 时,建议每类流量(管理、存储、业务)一个独立桥,隔离和排障都会轻松很多——这是原稿实战经验,值得照做。

#收尾

选哪种接入,取决于"帧要不要出二层":

  • 只想让 VM 能上网、不在乎外部访问 → default NAT(开发环境)或 SLIRP(临时验证);
  • 要让 VM 像真机一样出现在局域网、对外提供服务 → Linux Bridge,配合本文的建桥、接线、验证三步走;
  • 大规模多租户、需要精细隔离和 SDN 能力 → 桥接只是起点,接下来值得了解 OVS(Open vSwitch)、VXLAN 和 SR-IOV,它们解决的是"桥解决不了的可扩展性"问题。

先按本文把第一台 VM 桥进局域网并跑通验证矩阵,再往上叠加 VLAN 和更复杂的网络,就有了判断问题的坐标系。

相关阅读

Related

留言讨论

0 条

还没有留言,来说点什么吧。

KVM 网络配置:Linux Bridge 与虚拟机互通 · LXH·BLOG