6548 字
33 分钟
KVM虚拟化入门(四):从零理解 Linux 桥接网络——拓扑、工具与故障排查

环境:Dell PowerEdge R730 / Debian 13 宿主机 / KVM+libvirt / 2 台虚拟机 / 桥接 + NAT 混合网络

目标:从零讲清楚这台服务器上每一个网络组件,以及最近两次故障的排查思路

前置阅读:本文配套 KVM虚拟化入门(三) 中的 DHCP 掉线排查,那次排查暴露出我对网络底层理解不足,于是有了这篇补课笔记。

1. 我的服务器长什么样——完整网络拓扑#

先把整台机器的网络画出来。这张图是真实的,每块网卡、每个 IP 都来自实际运行的配置:

┌─────────────────────────────────────────────────────────────────────┐
│ Dell PowerEdge R730 │
│ Debian 13 宿主机 │
│ │
│ ┌──────────┐ │
│ │ 路由器 │ │
│ │ 192.168. │ │
│ │ 43.1 │ │
│ │ (DHCP) │ │
│ └─────┬────┘ │
│ │ 网线 │
│ │ │
│ ┌────────────┴────────────┐ │
│ │ eno1 (物理网卡) │ │
│ │ MAC: 44:a8:42:0f:12:fe │ │
│ └────────────┬────────────┘ │
│ │ │
│ ┌────────────┴────────────┐ │
│ │ br0 (虚拟交换机/网桥) │ │
│ │ MAC: 16:47:c9:c5:4f:ff │ │
│ │ IP: 192.168.43.101 │ ← 宿主机自己 │
│ └──┬─────────┬─────────┬──┘ │
│ │ │ │ │
│ ┌───┴───┐ ┌───┴───┐ │ │
│ │ vnet1 │ │ vnet2 │ │ │
│ │ arch │ │debian │ │ │
│ │ 桥接网 │ │ 13 │ │ │
│ │ 卡 │ │ 桥接网 │ │ │
│ └───┬───┘ │ 卡 │ │ │
│ │ └───┬───┘ │ │
│ ┌──────┴──────┐ ┌─┴──────┐ │ │
│ │ arch-vm │ │debian13│ │ │
│ │ (Arch) │ │ (Deb) │ │ │
│ │ │ │ │ │ │
│ │ vnet0 ──────┼─┼──> NAT │ │ │
│ │ (192.168. │ │ 192.168│ │ │
│ │ 122.x) │ │ .43.19 │ │ │
│ └─────────────┘ └────────┘ │ │
│ ┌────────────────────────────────┘ │
│ ▼ │
│ ┌───────────┐ │
│ │ virbr0 │ ← libvirt 内置虚拟交换机(NAT 模式) │
│ │ 192.168. │ │
│ │ 122.1/24 │ │
│ │ (dnsmasq │ │
│ │ DHCP) │ │
│ └───────────┘ │
└─────────────────────────────────────────────────────────────────────┘

对照表#

组件类型MACIP说明
eno1物理网卡44:a8:42:0f:12:fe无 IP真的插网线的那块
br0虚拟交换机16:47:c9:c5:4f:ff192.168.43.101宿主机自己的”网卡”
vnet1虚拟网线fe:54:00:d0:c8:a3arch-vm → br0
vnet2虚拟网线fe:54:00:e8:20:e9debian13 → br0
virbr0NAT 交换机52:54:00:e7:d2:b1192.168.122.1libvirt 默认 NAT
vnet0虚拟网线fe:54:00:f2:15:45arch-vm → virbr0
enp1s0VM 内网卡52:54:00:e8:20:e9192.168.43.19debian13 内部视角

等一下——你可能会发现 vnet2 的 MAC 和 debian13 内的 enp1s0 的 MAC 一样。没错,vnet 就是 VM 内部网卡在宿主机上的”镜像”,两者是同一根”虚拟网线”的两端。

2. 核心概念:网桥到底是个什么东西?#

2.1 类比:物理交换机#

想象你桌上有一个 5 口千兆交换机:

交换机
├── 口1: 你的台式机 (MAC: AA:AA)
├── 口2: 你的笔记本 (MAC: BB:BB)
├── 口3: 路由器 (MAC: CC:CC)
├── 口4: 空闲
└── 口5: 空闲

交换机的工作很简单:看到以太网帧就转发。台式机发一个 DHCP 广播,交换机会把它复制到口2、口3。路由器回复 DHCP Offer 给 MAC AA:AA,交换机会只转发给口1(因为它知道 AA 在口1)。

Linux 的 br0 就是一个用软件模拟的交换机。区别只在于它的”端口”不是物理 RJ45 插座,而是网络接口:

br0 (软件交换机)
├── eno1 (物理网卡,"上联口")
├── vnet1 (arch-vm 的虚拟网线)
└── vnet2 (debian13 的虚拟网线)

2.2 宿主机怎么上网?为什么 br0 有 IP,eno1 没有?#

你可能注意到 br0 本身也有一个 IP (192.168.43.101)。交换机不是只转发吗?怎么还能有 IP?而且物理网卡 eno1 明明插着网线,为什么是 br0 的”从属”,自己却没 IP?

先看看我机器上的真实状态:

Terminal window
# br0:有 IP,能上网
ip addr show br0 | grep inet
# inet 192.168.43.101/24 scope global dynamic ← br0 的 IP
# eno1:也有 IP,但 ping 不通!
ip addr show eno1 | grep inet
# inet 192.168.43.69/24 scope global dynamic ← eno1 的 IP(残留)
ping -I eno1 192.168.43.1
# Destination Host Unreachable ← 有 IP 但用不了!
# 实际出去的源地址是谁?
ip route get 8.8.8.8
# 8.8.8.8 via 192.168.43.1 dev br0 src 192.168.43.101
# ^^^^^^^^^^^^^^^^^ 走的是 br0 的 IP!

2.2.1 物理交换机的类比#

拿一个真实的 8 口 TP-Link 交换机举例:

物理交换机
├── 口1→口8: 转发以太网帧,这些端口没有 IP 地址
└── 交换机本身: 也没有 IP(非管理型交换机)

如果是一个网管交换机(比如 Cisco),它有一个专门的”管理接口”(通常是 VLAN 1 或专用管理口),这个接口有 IP 用于 SSH/Web 管理,但它跟数据转发端口是逻辑分离的。

Linux 网桥的设计跟网管交换机一模一样:

br0 (软件交换机)
├── [数据面] 转发端口之间的以太网帧
│ ├── eno1 端口: 不参与 IP 层,只转发帧
│ ├── vnet1 端口: 不参与 IP 层,只转发帧
│ └── vnet2 端口: 不参与 IP 层,只转发帧
└── [管理面] br0 本身: 有 IP,宿主机通过它上网

2.2.2 eno1 为什么是 br0 的”从属”(slave)#

当一个物理网卡被加入网桥时,内核会做两件事:

入桥前入桥后
eno1独立网卡,处理 L2+L3,可以配 IP、跑 DHCP降级为纯 L2 端口,只转发帧
br0不存在接管所有 L3 功能:IP、DHCP、ARP

master br0 状态意味着 eno1 把自己收到的所有以太网帧无脑转发给 br0,由 br0 决定怎么处理。eno1 自己的 IP 栈实际上被绕过了——这就是为什么上面 ping -I eno1 不通的原因。

Terminal window
# 验证 eno1 的从属状态
ip -d link show eno1 | grep -E "master|bridge_slave"
# master br0 ← 属于 br0 交换机
# bridge_slave state forwarding ← 处于转发状态

2.2.3 一张图说清楚#

数据包到达 eno1(物理网卡):
┌─────────────────────────────────────────────┐
│ eno1 (L2 端口) │
│ 收到以太网帧,查目标 MAC: │
│ - 目标是 br0 的 MAC (16:47) → 交给 br0 处理 │ ← 宿主机收包
│ - 目标是 VM 的 MAC (52:54) → 转发到 vnet │ ← VM 收包
│ - 目标是广播 → 泛洪到所有端口 │
│ ⚠️ eno1 自己的 IP 地址不被使用! │
└─────────────────────────────────────────────┘

一句话总结:eno1 从”带 IP 的网卡”降级为”无脑转发帧的端口”。IP 功能由 br0 接管。这跟把一台交换机的某个端口配了 IP 不生效是一个道理——IP 应该配在管理口(br0),而不是数据口(eno1)。

2.2.4 为什么 eno1 上还有个残留 IP?#

192.168.43.69 是 NetworkManager 在启动早期阶段残留的。系统启动时 NM 先给 eno1 获取了 DHCP(此时 br0 还没创建),然后 br0 创建并把 eno1 拉入桥接,eno1 的 IP 就变成僵尸了——存在但不通。这在实际使用中无害,但如果强迫症犯了可以清除:

Terminal window
sudo ip addr flush dev eno1 # 清除残留 IP

2.3 vnet 是什么?#

vnet 是 tap 设备的一种,由 libvirt 在启动 VM 时自动创建。你可以把它想象成一根”虚拟网线”:

VM 内部 宿主机
┌──────────┐ ┌──────────┐
│ enp1s0 │◄── vnet ──►│ vnet2 │──► br0
│ (网卡) │ 虚拟网线 │ (端口) │ 交换机
└──────────┘ └──────────┘

libvirt 在启动 VM 时会读取 XML 配置:

Terminal window
sudo virsh dumpxml debian13 | grep -A3 "interface type"
<interface type='bridge'> ← 桥接模式
<mac address='52:54:00:e8:20:e9'/>
<source bridge='br0'/> ← 接到 br0 交换机
<target dev='vnet2'/> ← 宿主机侧端口叫 vnet2
</interface>

对比 arch-vm 的 NAT 网卡:

<interface type='network'> ← NAT 模式
<mac address='52:54:00:f2:15:45'/>
<source network='default' bridge='virbr0'/>
<target dev='vnet0'/>
</interface>

一个 VM 有两段 interface:一段指向 br0(桥接),一段指向 virbr0(NAT)。

2.4 virbr0 和 NAT 网络#

arch-vm (192.168.122.x)
└── vnet0 ──► virbr0 (192.168.122.1)
libvirt dnsmasq
(内置 DHCP+DNS)
NAT 转换: 源 IP 改成宿主机 IP
宿主机路由 → 公网

virbr0 是 libvirt 默认创建的 NAT 桥。它也有自己的 DHCP 服务器(dnsmasq),分配 192.168.122.x 网段地址。VM 发出去的包经过 virbr0 后被 NAT(网络地址转换),源 IP 变成宿主机 IP 再出去。这是 VM 默认的上网方式,不需要额外配置。

关键区别:桥接模式下 VM 直接暴露在局域网中,路由器能看到它;NAT 模式下 VM 躲在宿主机后面,局域网其他设备看不到它。

3. 数据包旅行记#

跟着一个 ping 包,看看它到底经历了什么。

3.1 debian13 (桥接模式) ping 8.8.8.8#

VM 内部:
1. ping 生成 ICMP 包, 目标 8.8.8.8
2. 查路由表: default via 192.168.43.1 dev enp1s0
3. ARP 问"谁有 192.168.43.1?" → 路由器回复 MAC
4. 封装以太网帧: src=52:54:00:e8:20:e9, dst=路由器MAC
5. 写入 enp1s0 (vnet2 的另一端)
宿主机:
6. 帧出现在 vnet2 → br0 转发
7. br0 查 MAC 表: 目标 MAC 在 eno1 方向 → 从 eno1 发出
8. 物理网卡 eno1 发出电信号 → 网线 → 路由器
路由器:
9. 看到目标 8.8.8.8, 查路由表, 从 WAN 口发出
10. ... 互联网路由 ...
回包:
11. 8.8.8.8 回复 → 路由器 → eno1 → br0 → vnet2 → VM enp1s0

整个过程 br0 只做了一件事:根据 MAC 地址转发以太网帧。它不关心 IP 是什么、协议是什么——TCP、UDP、ICMP 对它来说都一样。

3.2 arch-vm (NAT模式) ping 8.8.8.8#

VM 内部:
1. ping 生成 ICMP 包, 目标 8.8.8.8
2. 查路由表: default via 192.168.122.1 (virbr0)
3. 发给 vnet0
宿主机:
4. 帧到达 virbr0
5. virbr0 转发到宿主机协议栈 (因为目标是自己)
6. NAT: 源 IP 192.168.122.x → 宿主机 IP 192.168.43.101
7. 宿主机查路由: default via 192.168.43.1 dev br0
8. br0 → eno1 → 路由器 → 公网
回包:
9. 8.8.8.8 回复到 192.168.43.101
10. 宿主机 NAT 表反向转换 → 还原目标为 192.168.122.x
11. virbr0 → vnet0 → arch-vm

桥接模式:包被转发 1 次(br0),宿主机几乎无感。 NAT 模式:包被处理 2 次(virbr0 + NAT),宿主机参与 IP 层处理。

3.3 一个关键配角:ARP 表#

你可能注意到数据包旅行记里第 3 步出现了”ARP 问’谁有 192.168.43.1?’”。ARP 是地址解析协议(Address Resolution Protocol),简单说就是把 IP 地址翻译成 MAC 地址。

为什么需要 ARP?#

以太网帧必须填写目标的 MAC 地址才能发送。但应用程序只知道 IP(比如 8.8.8.8),不知道 MAC。ARP 就是填补这个缺口的。

我想访问 8.8.8.8
→ 查路由表: 下一跳是 192.168.43.1(网关)
→ 但我不知道 192.168.43.1 的 MAC 是什么!
→ 发 ARP 广播: "谁有 192.168.43.1?报上 MAC 来!"
→ 192.168.43.1 回复: "是我,我的 MAC 是 5e:ee:78:ac:cc:f0"
→ 记下来(缓存到 ARP 表),下次直接用

看一台真实机器的 ARP 表#

Terminal window
ip neigh show

我机器上的实际输出:

192.168.43.232 dev br0 lladdr f8:ac:65:1c:3e:0a DELAY
192.168.43.1 dev br0 lladdr 5e:ee:78:ac:cc:f0 REACHABLE
192.168.122.123 dev virbr0 lladdr 52:54:00:f2:15:45 REACHABLE
192.168.43.69 dev br0 FAILED

每一行就是一个 IP → MAC 的映射关系:

IPMAC设备状态含义
192.168.43.15e:ee:78:ac:cc:f0br0REACHABLE网关,刚通过它通信,确认可达
192.168.122.12352:54:00:f2:15:45virbr0REACHABLEarch-vm 的 NAT 网卡
192.168.43.232f8:ac:65:1c:3e:0abr0DELAY某设备,正在等待确认
192.168.43.6900:00:00:00:00:00br0FAILEDeno1 的僵尸 IP,MAC 全零表示不可达

ARP 状态机#

REACHABLE ←── 确认可达(有通信往来)
↓ 超时
STALE ←── 可能还活着,但太久没用
↓ 发包需要
DELAY ←── 等待确认中 (约5秒)
↓ 收到回复 ↓ 超时无回复
REACHABLE PROBE → FAILED → 删除条目

实战技巧:如果你的 ARP 表里网关是 FAILED 或者 INCOMPLETE,网络必断。用 ip neigh show dev br0 第一时间排查。

ARP 表的内核实现#

ARP 表存在内核内存中,可以同时通过两个途径查看:

Terminal window
ip neigh show # iproute2 方式(推荐)
cat /proc/net/arp # procfs 方式(传统)

/proc/net/arp 输出解析:

IP address HW type Flags HW address Mask Device
192.168.43.1 0x1 0x2 5e:ee:78:ac:cc:f0 * br0

HW type 0x1 = 以太网,Flags 0x2 = 条目已确认。Flags 0x0 = 不可达(如上面的 192.168.43.69)。

3.4 系统启动和运行时到底发生了什么#

了解这些工具之后,我们回头看系统从开机到网络就绪的完整过程——这有助于理解为什么 NM 重启会导致 VM 断网。

系统启动流程 (systemd):
┌─ 内核启动 ─────────────────────────────────────────┐
│ 1. 加载网卡驱动 (eno1 出现) │
│ 2. 内核本身不做任何 IP 配置 │
└────────────────────────────────────────────────────┘
┌─ NetworkManager 启动 (16:56:04) ───────────────────┐
│ 3. NM 读取 /etc/NetworkManager/system-connections/ │
│ 找到配置: br0 (桥接), br0-slave-eno1 (从属) │
│ 4. NM 创建 br0 网桥设备 │
│ 5. NM 将 eno1 绑定为 br0 的从属端口 │
│ → eno1 进入 bridge_slave 状态, 停止处理 IP │
│ 6. NM 在 br0 上启动内部 DHCP 客户端 │
│ → 发送 DHCPDISCOVER 广播 │
│ → 路由器回复 DHCPOFFER: 你可用 192.168.43.101 │
│ → NM 接受, 设置 br0 的 IP 和路由 │
│ 7. NM 在 eno1 上短暂获取了 IP (192.168.43.69) │
│ → 但 eno1 入桥后这个 IP 变成僵尸 (不可达) │
└────────────────────────────────────────────────────┘
┌─ libvirtd 启动 ───────────────────────────────────┐
│ 8. 读取 VM XML 配置 │
│ 9. 创建 virbr0 (NAT 网桥) + 启动 dnsmasq (DHCP) │
│10. 自动启动标记为 autostart 的 VM │
│11. 为每个 VM 创建 vnet* tap 设备 │
│12. 将 vnet* 绑定到 XML 指定的网桥 (br0 或 virbr0) │
│ → NM 看到 vnet 是 libvirt 创建的, 标记为"未托管" │
└────────────────────────────────────────────────────┘
┌─ VM 内部启动 ─────────────────────────────────────┐
│13. debian13: /etc/network/interfaces │
│ → ifupdown 启动 dhclient 守护进程 │
│ → dhclient 在 enp1s0 上发 DHCPDISCOVER │
│ → 广播穿过 vnet2 → br0 → eno1 → 路由器 │
│ → 路由器回复 DHCPOFFER: 192.168.43.19 │
│ → dhclient 接受, 设置 IP │
│ │
│14. arch-vm: 类似, enp1s0 从路由器获取桥接 IP │
│ enp2s0 从 virbr0 的 dnsmasq 获取 192.168.122.x │
└────────────────────────────────────────────────────┘

NM 重启时发生了什么(故障二的根因)#

┌─ NM 重启 (16:58:24) ──────────────────────────────┐
│ 1. NM 进程退出 │
│ 2. NM 管理的 br0 网桥被拆除! │
│ → eno1 脱离 br0, 成为独立网卡 │
│ → vnet1, vnet2 也脱离 br0, 成为孤儿 │
│ 3. 新 NM 进程启动 │
│ 4. 重新创建 br0 (全新设备, 接口编号都变了) │
│ 5. 重新绑定 eno1 → br0 ✅ │
│ 6. NM 不知道 vnet1/vnet2 的存在 ❌ │
│ → vnet 留在原地, 没有 master, 无主状态 │
└────────────────────────────────────────────────────┘
后果: VM 的桥接网卡成为孤岛, DHCP 广播发不到 br0

这就是为什么 ignore-vnet.conf 如此重要——它明确告诉 NM “这些接口不归你管,别碰”,以后 NM 重启也不会影响 libvirt 管理的东西。

4. 工具手册(详解版)#

以下每个命令都附带了真实输出和逐字段解释。

4.1 ip addr —— 查看 IP 地址#

Terminal window
ip addr show br0

真实输出及解读:

19: br0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default qlen 1000
│ └──────────────────────┬─────────────────────┘ └─┬──┘ └──┬───┘ └─┬──┘ └──┬──┘
│ 接口标志位(见下表) MTU 队列策略 链路 操作 发送队列
└── 接口索引号 19(内核分配的序号,重启会变)

接口标志位含义

标志含义
BROADCAST支持广播
MULTICAST支持多播
UP接口已启用(ip link set up
LOWER_UP物理链路已接通(网线插着)
NOARP不响应 ARP(如 tailscale 虚拟接口)
PROMISC混杂模式(接收所有帧,即使目标 MAC 不是自己)

第二行——链路层:

link/ether 16:47:c9:c5:4f:ff brd ff:ff:ff:ff:ff:ff
└──────┬──────┘ └───────┬───────┘
MAC 地址 广播地址 (全F=广播到所有人)

第三行——IPv4:

inet 192.168.43.101/24 brd 192.168.43.255 scope global dynamic noprefixroute br0
│ └──────┬──────┘ └──────┬──────┘ └─┬─┘ └─┬──┘ └───┬───┘ └─────┬─────┘
│ IP+掩码 广播地址 作用域 类型 标志位 接口标签
└── IPv4 地址族

作用域(scope)

含义
global公网可达的 IP 或局域网 IP
link仅本链路有效(如 169.254.x.xfe80::
host仅本机有效(127.0.0.1

IP 类型

含义
dynamicDHCP 获取的,有过期时间
(无 dynamic)静态配置的
noprefixroute不自动添加子网路由

第四行——租约剩余时间:

valid_lft 3387sec preferred_lft 3387sec
└────┬────┘ └──────┬──────┘
有效生命周期 首选生命周期 (过了也可以继续用)

valid_lft 到期后 IP 被释放。forever = 静态 IP,永不过期。

4.2 ip route —— 查看路由表#

Terminal window
ip route show

真实输出逐条解读:

default via 192.168.43.1 dev br0 proto dhcp src 192.168.43.101 metric 425
└──┬──┘ └──────┬──────┘ └─┬──┘ └──┬──┘ └────────┬─────────┘ └──┬──┘
│ 网关 出接口 协议 首选源地址 优先级
└── 默认路由: 0.0.0.0/0, 匹配所有目标
169.254.0.0/24 dev idrac proto kernel scope link src 169.254.0.2 metric 100
└─────┬─────┘ └──┬──┘ └───┬────┘ └────┬────┘ (iDRAC 管理口)
目标网段 直连设备 内核自动生成 源地址
192.168.43.0/24 dev br0 proto kernel scope link src 192.168.43.101 metric 425
↑ 直连路由: 发往 192.168.43.x 的包直接从 br0 出去
192.168.122.0/24 dev virbr0 proto kernel scope link src 192.168.122.1
↑ VM NAT 网段路由

路由匹配规则:最长前缀优先。发往 192.168.43.19 的包匹配第三行 (/24) 而不走默认路由。

metric(跃点数/优先级):数字越小越优先。有多个默认路由时,选 metric 最小的。

Terminal window
# 查一个具体目标走哪条路由
ip route get 8.8.8.8
# 8.8.8.8 via 192.168.43.1 dev br0 src 192.168.43.101

4.3 ip neigh —— 查看 ARP 表#

Terminal window
ip neigh show dev br0

真实输出:

192.168.43.1 dev br0 lladdr 5e:ee:78:ac:cc:f0 REACHABLE
└─────┬─────┘ └───────┬───────┘ └───┬───┘
邻居 IP 邻居 MAC ARP 状态
192.168.43.69 dev br0 FAILED
└─┬──┘
IP 不可达, MAC 全是0, 将被删除

ARP 状态速查(详见 §3.3):

状态含义是否正常
REACHABLE刚确认可达
STALE太久没通信,下次用前需确认✅(正常老化)
DELAY正在等待确认⚠️(应该是瞬态)
PROBE正在发送探测包⚠️
INCOMPLETEARP 请求发出去了但没回复❌ 网络不通
FAILED多次探测无响应,标记为失败❌ 网络不通

常用操作

Terminal window
# 手动添加一条 ARP 记录
sudo ip neigh add 192.168.43.100 lladdr aa:bb:cc:dd:ee:ff dev br0
# 删除某条记录(强制重新 ARP)
sudo ip neigh del 192.168.43.1 dev br0
# 清空整个 ARP 表(慎重)
sudo ip neigh flush all
Terminal window
ip link show eno1
2: eno1: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc mq master br0 state UP ...
└───┬───┘
从属于 br0 网桥
ip link show vnet2
12: vnet2: <BROADCAST,MULTICAST,UP,LOWER_UP> ... master br0 ...
└───┬───┘
绑定在 br0 上

操作

Terminal window
sudo ip link set vnet2 master br0 # 将接口加入网桥
sudo ip link set vnet2 nomaster # 从网桥移除
sudo ip link set eno1 up # 启用接口
sudo ip link set eno1 down # 关闭接口

4.5 nmcli 详解 —— NetworkManager 命令行#

nmcli 是 NM 的命令行操作入口。NM 有明确的”连接 vs 设备”哲学:连接是配置模板,设备是运行时实例

4.5.1 nmcli device —— 查看运行时设备#

Terminal window
nmcli device status

真实输出:

DEVICE TYPE STATE CONNECTION
br0 bridge 已连接 br0
eno1 ethernet 已连接 br0-slave-eno1
virbr0 bridge 连接(外部) virbr0
vnet0 tun 未托管 --
vnet1 tun 未托管 --
vnet2 tun 未托管 --
eno2 ethernet 不可用 --

STATE 含义

STATE含义
已连接NM 管理且已激活
连接(外部)NM 认出这个设备但它不是 NM 激活的(如 virbr0 是 libvirt 激活的)
未托管NM 明确忽略,不碰它
不可用接口存在但没插线 / 没启动
断开NM 管理但当前未激活

vnet* 显示 未托管 是因为我们配了 unmanaged-devices=interface-name:vnet*。如果没配,它会显示 已连接——这意味着 NM 在管它,NM 重启就会把它从桥上拆掉。

Terminal window
nmcli device show br0

真实输出逐段解读:

GENERAL.DEVICE: br0 ← 设备名
GENERAL.TYPE: bridge ← 设备类型
GENERAL.HWADDR: 16:47:C9:C5:4F:FF ← MAC 地址
GENERAL.STATE: 100(已连接) ← 数字+中文状态
GENERAL.CONNECTION: br0 ← 当前激活的连接名
IP4.ADDRESS[1]: 192.168.43.101/24 ← IPv4 地址
IP4.GATEWAY: 192.168.43.1 ← 默认网关
IP4.ROUTE[1]: dst = 192.168.43.0/24, nh = 0.0.0.0, mt = 425
└─────┬──────┘ └─┬─┘ └──┬──┘
目标网段 下一跳 跃点数
IP4.ROUTE[2]: dst = 0.0.0.0/0, nh = 192.168.43.1, mt = 425
└─── 默认路由 ──┘
IP4.DNS[1]: 192.168.43.1 ← DNS 服务器

4.5.2 nmcli connection —— 管理持久化配置#

Terminal window
nmcli con show
NAME UUID TYPE DEVICE
br0 de5f8af7 bridge br0
br0-slave-eno1 a31811be ethernet eno1
virbr0 1016ffb6 bridge virbr0

“连接”是存储在 /etc/NetworkManager/system-connections/ 下的配置文件。UUID 是唯一标识符,DEVICE 是这个连接当前激活到了哪个设备上。

Terminal window
# 查看某个连接的完整配置
nmcli con show br0
# 只看 IPv4 相关的设置
nmcli -f ipv4 con show br0

输出关键字段:

ipv4.method: auto ← DHCP 或 manual(静态)
ipv4.dhcp-timeout: 90 ← 获取 IP 超时秒数
ipv4.dhcp-send-release: 0(false) ← 断开时不发 DHCPRELEASE
ipv4.dhcp-client-id: dhclient ← 客户端标识符
ipv4.addresses: -- ← 静态 IP(method=manual时)
ipv4.gateway: --
ipv4.dns: --

4.5.3 常用操作#

Terminal window
# 修改连接为静态 IP
sudo nmcli con mod br0 ipv4.method manual \
ipv4.addresses 192.168.43.200/24 \
ipv4.gateway 192.168.43.1 \
ipv4.dns 192.168.43.1
# 改回 DHCP
sudo nmcli con mod br0 ipv4.method auto
# 重读配置(不断开)
sudo nmcli con reload
# 重新应用(触发 DHCP 续约,不断开)
sudo nmcli device reapply br0
# 断开重连
sudo nmcli con down br0 && sudo nmcli con up br0
# 删除一个连接配置
sudo nmcli con delete "Wired connection 1"

4.6 dhclient —— 独立 DHCP 客户端#

Terminal window
# 手动获取 IP(-v 详细过程,直接看到四步握手)
sudo dhclient -v enp1s0

真实输出(来自 debian13 VM):

DHCPDISCOVER on enp1s0 to 255.255.255.255 port 67 interval 6
DHCPOFFER of 192.168.43.19 from 192.168.43.1
DHCPREQUEST for 192.168.43.19 on enp1s0 to 255.255.255.255 port 67
DHCPACK of 192.168.43.19 from 192.168.43.1
bound to 192.168.43.19 -- renewal in 1437 seconds.

DHCP 四步握手中每一步的含义:

步骤方向含义
DISCOVERVM → 广播”有人在吗?我要 IP!“
OFFER路由器 → VM”这个 IP 给你,要吗?“
REQUESTVM → 广播”好的我要了!“(广播是为了让其他 DHCP 服务器知道)
ACK路由器 → VM”确认,给你用”

renewal in 1437 seconds:dhclient 是常驻进程,到时间会自动发 DHCPREQUEST 续约(单播给路由器),不需要重走 DISCOVER。

租约文件

Terminal window
cat /var/lib/dhcp/dhclient.enp1s0.leases
lease {
interface "enp1s0";
fixed-address 192.168.43.19;
option subnet-mask 255.255.255.0;
option routers 192.168.43.1;
option dhcp-lease-time 3600; ← 租约时长
option domain-name-servers 192.168.43.1;
renew 4 2026/06/04 19:01:06; ← T1 续约时间
rebind 4 2026/06/04 19:28:06; ← T2 重新绑定时间
expire 4 2026/06/04 19:37:06; ← 租约到期时间
}

T1、T2、expire 的关系

获取 IP ──→ T1(50%租约) ──→ T2(87.5%) ──→ expire(100%)
│ │ │
└─ 单播续约 └─ 广播续约 └─ IP 释放
(发给路由器) (发给所有人)

dhclient 和 NM internal 的对比

dhclientNM internal
运行方式独立守护进程,常驻NM 内部线程
续约可靠性20 年成熟代码,非常稳定偶发续约失败(v1.52.1)
租约文件/var/lib/dhcp/dhclient.*.leasesNM 内存中
进程可见ps aux | grep dhclient 能看到看不到独立进程
日志-v 实时输出四步握手journalctl

4.7 tcpdump —— 终极武器#

当你用上面所有工具都找不到问题时,tcpdump 是最后的王牌——它让你直接看到网线上跑了什么。

Terminal window
# 抓 DHCP 流量
sudo tcpdump -i any port 67 or port 68 -n -v

真实输出片段(就是我用它发现 vnet2 脱离 br0 的那次):

17:45:48.272192 vnet2 B IP 0.0.0.0.68 > 255.255.255.255.67:
└─┬─┘ │ └────┬────┘ └─────┬─────┘
接口 方向 源地址:端口 目标地址:端口
DHCP Discover from 52:54:00:e8:20:e9
Requested-IP: 192.168.214.151 ← VM 还在请求旧网段的 IP!
17:45:48.272192 br0 B IP 0.0.0.0.68 > 255.255.255.255.67:
───
DHCP Discover ... (同样的包出现在 br0 上,说明 vnet2 已在 br0 中)

方向标记

标记含义
B广播包
Out从此接口发出
In从此接口收到
P单播目标为此接口
(无标记)被转发的包

常用过滤条件

Terminal window
# 按接口
sudo tcpdump -i br0 -n
# 按协议端口
sudo tcpdump -i any port 53 -n # DNS
# 按 MAC
sudo tcpdump -i br0 ether host 52:54:00:e8:20:e9
# 按 IP
sudo tcpdump -i br0 host 192.168.43.19
# 组合: 只看发往 Google DNS 的包
sudo tcpdump -i br0 dst 8.8.8.8 -n
# 保存到文件 (用 Wireshark 分析)
sudo tcpdump -i br0 -w /tmp/capture.pcap

5. 故障案例一:宿主机 DHCP 掉线#

详见 KVM虚拟化入门(三)

现象:宿主机 br0 过 2~3 小时后 IP 丢失,VM 正常。

根因

  1. 路由器 DHCP 租约只有 3600 秒
  2. NM 内部 DHCP 客户端对桥接接口的 T1/T2 续约偶发失败
  3. VM 用 ifupdown + dhclient 守护进程,续约正常

工具链路ip addrnmclijournalctltcpdump → 发现 NM internal 不可靠

修复:cron 每 25 分钟 nmcli device reapply + watchdog 每 5 分钟巡检

6. 故障案例二:重启后 VM 断网#

现象:宿主机重启后,两个 VM 的桥接网卡都拿不到 IP。tcpdump 显示 DHCP DISCOVER 到了 vnet 但没到 br0。

排查

Terminal window
# 1. 检查 vnet2 有没有主(归属哪个桥)
ip -d link show vnet2 | grep master
# (空!vnet2 没有 master!)
# 2. 检查 br0 有哪些成员
ls /sys/class/net/br0/brif/
# eno1 ← 只有物理网卡,VM 不见了!

根因:NetworkManager 在启动过程中意外重启了一次(日志明确写着 after a restart)。NM 重启会拆掉旧网桥再重建,但它只知道自己的从属接口 eno1,不知道 libvirt 创建的 vnet,导致 vnet 被”遗弃”。

时间线:
16:56:04 NM 启动,创建 br0,vnet 正常绑定
16:56:45 一切正常
16:58:24 NM 意外重启!旧 br0 拆掉,新 br0 只有 eno1
→ vnet1、vnet2 成为孤儿,脱离所有桥接

修复

Terminal window
# 临时:手动绑回
sudo ip link set vnet1 master br0
sudo ip link set vnet2 master br0
# 永久:配置 NM 忽略 vnet 接口,交给 libvirt 全权管理
echo '[keyfile]
unmanaged-devices=interface-name:vnet*' \
| sudo tee /etc/NetworkManager/conf.d/ignore-vnet.conf
sudo systemctl restart NetworkManager

教训:NM 和 libvirt 各管各的接口,边界不清晰。vnet* 是 libvirt 创建的,NM 就不该碰。用 unmanaged-devices 画一条明确的线,以后 NM 再怎么重启也不会影响 VM。

7. 管理职责划分#

经过两次故障和修复,目前的职责划分如下:

NetworkManager 管理:
├── br0 (网桥本身,DHCP 获取宿主机 IP)
├── eno1 (物理网卡,绑定到 br0)
└── virbr0 (NAT 网桥,观察模式)
libvirt 管理:
├── vnet0 (arch-vm NAT 网卡 → virbr0)
├── vnet1 (arch-vm 桥接网卡 → br0)
└── vnet2 (debian13 桥接网卡 → br0)
NM 明确忽略:
└── interface-name:vnet* ← 写在 /etc/NetworkManager/conf.d/
cron 定时任务:
├── dhcp-renew-br0 (/25min) nmcli device reapply br0
└── dhcp-watchdog (/5min) 检测 br0 IP 丢失则重连

查看职责是否到位#

Terminal window
# NM 是否正确忽略 vnet
nmcli device status | grep vnet
# vnet2 tun 未托管 -- ← "未托管" = NM 不碰
# 桥接成员是否完整
ls /sys/class/net/br0/brif/
# eno1 vnet1 vnet2 ← 都在!
# 租约是否正常
nmcli -f DHCP4 device show br0 # 检查租约

8. 总结#

核心认知#

  1. 网桥 = 虚拟交换机,工作在 L2,只管 MAC 地址转发,不管 IP
  2. vnet = 虚拟网线,一端是 VM 的 enp1s0,另一端是宿主机的 vnet 端口
  3. br0 的 IP 是宿主机自己的,跟 VM 的 IP 完全独立,路由器看到的是不同 MAC
  4. virbr0 = NAT + DHCP,VM 躲在宿主机后面,外面看不到

工具速查#

你想知道…用这个命令
接口有没有 IPip -4 addr show
IP 从哪里来(DHCP/静态)scope global dynamic 还是不带 dynamic
谁在管理这个接口nmcli device status
这个接口插在哪个桥上ip -d link show xxx | grep master
桥上有哪些端口ls /sys/class/net/br0/brif/
DHCP 租约还有多久ip addr show br0 | grep valid_lft
包到底发出去没有sudo tcpdump -i br0 -n

两次故障一句话#

  • 故障一:NM 内部 DHCP 客户端续约失败 → cron reapply 兜底
  • 故障二:NM 重启拆桥忘带 vnet → 配置 NM 忽略 vnet,libvirt 全权管理

理解了这些,以后遇到网络问题就知道先查什么、怎么查,而不是盲目重启。

本文记录于 2026 年 6 月 4 日,环境 Debian 13 + NetworkManager 1.52.1 + libvirt 11.3.0

KVM虚拟化入门(四):从零理解 Linux 桥接网络——拓扑、工具与故障排查
https://www.mintlab.top/posts/tries/kvm-network-101-guide/
作者
Mint
发布于
2026-06-05
许可协议
CC BY-NC-SA 4.0
发表评论

输入用户名和邮箱后自动检查登录状态。登录后用户名和邮箱将被绑定, 只可以修改头像和主页链接。

未登录
昵称
邮箱
填写头像链接与主页链接

头像链接为空默认使用gravatar头像

头像
主页
人机验证
评论列表

以下是可爱的评论们:

暂无评论, 呜呜, 快来评论喵!