环境: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) │ ││ └───────────┘ │└─────────────────────────────────────────────────────────────────────┘对照表
| 组件 | 类型 | MAC | IP | 说明 |
|---|---|---|---|---|
eno1 | 物理网卡 | 44:a8:42:0f:12:fe | 无 IP | 真的插网线的那块 |
br0 | 虚拟交换机 | 16:47:c9:c5:4f:ff | 192.168.43.101 | 宿主机自己的”网卡” |
vnet1 | 虚拟网线 | fe:54:00:d0:c8:a3 | — | arch-vm → br0 |
vnet2 | 虚拟网线 | fe:54:00:e8:20:e9 | — | debian13 → br0 |
virbr0 | NAT 交换机 | 52:54:00:e7:d2:b1 | 192.168.122.1 | libvirt 默认 NAT |
vnet0 | 虚拟网线 | fe:54:00:f2:15:45 | — | arch-vm → virbr0 |
enp1s0 | VM 内网卡 | 52:54:00:e8:20:e9 | 192.168.43.19 | debian13 内部视角 |
等一下——你可能会发现 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?
先看看我机器上的真实状态:
# 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 不通的原因。
# 验证 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 就变成僵尸了——存在但不通。这在实际使用中无害,但如果强迫症犯了可以清除:
sudo ip addr flush dev eno1 # 清除残留 IP2.3 vnet 是什么?
vnet 是 tap 设备的一种,由 libvirt 在启动 VM 时自动创建。你可以把它想象成一根”虚拟网线”:
VM 内部 宿主机┌──────────┐ ┌──────────┐│ enp1s0 │◄── vnet ──►│ vnet2 │──► br0│ (网卡) │ 虚拟网线 │ (端口) │ 交换机└──────────┘ └──────────┘libvirt 在启动 VM 时会读取 XML 配置:
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 表
ip neigh show我机器上的实际输出:
192.168.43.232 dev br0 lladdr f8:ac:65:1c:3e:0a DELAY192.168.43.1 dev br0 lladdr 5e:ee:78:ac:cc:f0 REACHABLE192.168.122.123 dev virbr0 lladdr 52:54:00:f2:15:45 REACHABLE192.168.43.69 dev br0 FAILED每一行就是一个 IP → MAC 的映射关系:
| IP | MAC | 设备 | 状态 | 含义 |
|---|---|---|---|---|
192.168.43.1 | 5e:ee:78:ac:cc:f0 | br0 | REACHABLE | 网关,刚通过它通信,确认可达 |
192.168.122.123 | 52:54:00:f2:15:45 | virbr0 | REACHABLE | arch-vm 的 NAT 网卡 |
192.168.43.232 | f8:ac:65:1c:3e:0a | br0 | DELAY | 某设备,正在等待确认 |
192.168.43.69 | 00:00:00:00:00:00 | br0 | FAILED | eno1 的僵尸 IP,MAC 全零表示不可达 |
ARP 状态机
REACHABLE ←── 确认可达(有通信往来) ↓ 超时 STALE ←── 可能还活着,但太久没用 ↓ 发包需要 DELAY ←── 等待确认中 (约5秒) ↓ 收到回复 ↓ 超时无回复 REACHABLE PROBE → FAILED → 删除条目实战技巧:如果你的 ARP 表里网关是
FAILED或者INCOMPLETE,网络必断。用ip neigh show dev br0第一时间排查。
ARP 表的内核实现
ARP 表存在内核内存中,可以同时通过两个途径查看:
ip neigh show # iproute2 方式(推荐)cat /proc/net/arp # procfs 方式(传统)/proc/net/arp 输出解析:
IP address HW type Flags HW address Mask Device192.168.43.1 0x1 0x2 5e:ee:78:ac:cc:f0 * br0HW 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 地址
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.x、fe80::) |
host | 仅本机有效(127.0.0.1) |
IP 类型:
| 值 | 含义 |
|---|---|
dynamic | DHCP 获取的,有过期时间 |
| (无 dynamic) | 静态配置的 |
noprefixroute | 不自动添加子网路由 |
第四行——租约剩余时间:
valid_lft 3387sec preferred_lft 3387sec └────┬────┘ └──────┬──────┘ 有效生命周期 首选生命周期 (过了也可以继续用)
valid_lft到期后 IP 被释放。forever= 静态 IP,永不过期。
4.2 ip route —— 查看路由表
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 最小的。
# 查一个具体目标走哪条路由ip route get 8.8.8.8# 8.8.8.8 via 192.168.43.1 dev br0 src 192.168.43.1014.3 ip neigh —— 查看 ARP 表
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 | 正在发送探测包 | ⚠️ |
INCOMPLETE | ARP 请求发出去了但没回复 | ❌ 网络不通 |
FAILED | 多次探测无响应,标记为失败 | ❌ 网络不通 |
常用操作:
# 手动添加一条 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 all4.4 ip link —— 查看和管理链路层
ip link show eno12: eno1: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc mq master br0 state UP ... └───┬───┘ 从属于 br0 网桥
ip link show vnet212: vnet2: <BROADCAST,MULTICAST,UP,LOWER_UP> ... master br0 ... └───┬───┘ 绑定在 br0 上操作:
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 —— 查看运行时设备
nmcli device status真实输出:
DEVICE TYPE STATE CONNECTIONbr0 bridge 已连接 br0eno1 ethernet 已连接 br0-slave-eno1virbr0 bridge 连接(外部) virbr0vnet0 tun 未托管 --vnet1 tun 未托管 --vnet2 tun 未托管 --eno2 ethernet 不可用 --STATE 含义:
| STATE | 含义 |
|---|---|
已连接 | NM 管理且已激活 |
连接(外部) | NM 认出这个设备但它不是 NM 激活的(如 virbr0 是 libvirt 激活的) |
未托管 | NM 明确忽略,不碰它 |
不可用 | 接口存在但没插线 / 没启动 |
断开 | NM 管理但当前未激活 |
vnet*显示未托管是因为我们配了unmanaged-devices=interface-name:vnet*。如果没配,它会显示已连接——这意味着 NM 在管它,NM 重启就会把它从桥上拆掉。
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 —— 管理持久化配置
nmcli con showNAME UUID TYPE DEVICEbr0 de5f8af7 bridge br0br0-slave-eno1 a31811be ethernet eno1virbr0 1016ffb6 bridge virbr0“连接”是存储在 /etc/NetworkManager/system-connections/ 下的配置文件。UUID 是唯一标识符,DEVICE 是这个连接当前激活到了哪个设备上。
# 查看某个连接的完整配置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) ← 断开时不发 DHCPRELEASEipv4.dhcp-client-id: dhclient ← 客户端标识符ipv4.addresses: -- ← 静态 IP(method=manual时)ipv4.gateway: --ipv4.dns: --4.5.3 常用操作
# 修改连接为静态 IPsudo 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
# 改回 DHCPsudo 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 客户端
# 手动获取 IP(-v 详细过程,直接看到四步握手)sudo dhclient -v enp1s0真实输出(来自 debian13 VM):
DHCPDISCOVER on enp1s0 to 255.255.255.255 port 67 interval 6DHCPOFFER of 192.168.43.19 from 192.168.43.1DHCPREQUEST for 192.168.43.19 on enp1s0 to 255.255.255.255 port 67DHCPACK of 192.168.43.19 from 192.168.43.1bound to 192.168.43.19 -- renewal in 1437 seconds.DHCP 四步握手中每一步的含义:
| 步骤 | 方向 | 含义 |
|---|---|---|
DISCOVER | VM → 广播 | ”有人在吗?我要 IP!“ |
OFFER | 路由器 → VM | ”这个 IP 给你,要吗?“ |
REQUEST | VM → 广播 | ”好的我要了!“(广播是为了让其他 DHCP 服务器知道) |
ACK | 路由器 → VM | ”确认,给你用” |
renewal in 1437 seconds:dhclient 是常驻进程,到时间会自动发DHCPREQUEST续约(单播给路由器),不需要重走 DISCOVER。
租约文件:
cat /var/lib/dhcp/dhclient.enp1s0.leaseslease { 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 的对比:
| dhclient | NM internal | |
|---|---|---|
| 运行方式 | 独立守护进程,常驻 | NM 内部线程 |
| 续约可靠性 | 20 年成熟代码,非常稳定 | 偶发续约失败(v1.52.1) |
| 租约文件 | /var/lib/dhcp/dhclient.*.leases | NM 内存中 |
| 进程可见 | ps aux | grep dhclient 能看到 | 看不到独立进程 |
| 日志 | -v 实时输出四步握手 | journalctl |
4.7 tcpdump —— 终极武器
当你用上面所有工具都找不到问题时,tcpdump 是最后的王牌——它让你直接看到网线上跑了什么。
# 抓 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 | 单播目标为此接口 |
| (无标记) | 被转发的包 |
常用过滤条件:
# 按接口sudo tcpdump -i br0 -n
# 按协议端口sudo tcpdump -i any port 53 -n # DNS
# 按 MACsudo tcpdump -i br0 ether host 52:54:00:e8:20:e9
# 按 IPsudo 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.pcap5. 故障案例一:宿主机 DHCP 掉线
详见 KVM虚拟化入门(三)
现象:宿主机 br0 过 2~3 小时后 IP 丢失,VM 正常。
根因:
- 路由器 DHCP 租约只有 3600 秒
- NM 内部 DHCP 客户端对桥接接口的 T1/T2 续约偶发失败
- VM 用
ifupdown+dhclient守护进程,续约正常
工具链路:ip addr → nmcli → journalctl → tcpdump → 发现 NM internal 不可靠
修复:cron 每 25 分钟 nmcli device reapply + watchdog 每 5 分钟巡检
6. 故障案例二:重启后 VM 断网
现象:宿主机重启后,两个 VM 的桥接网卡都拿不到 IP。tcpdump 显示 DHCP DISCOVER 到了 vnet 但没到 br0。
排查:
# 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 成为孤儿,脱离所有桥接修复:
# 临时:手动绑回sudo ip link set vnet1 master br0sudo 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 丢失则重连查看职责是否到位
# NM 是否正确忽略 vnetnmcli device status | grep vnet# vnet2 tun 未托管 -- ← "未托管" = NM 不碰
# 桥接成员是否完整ls /sys/class/net/br0/brif/# eno1 vnet1 vnet2 ← 都在!
# 租约是否正常nmcli -f DHCP4 device show br0 # 检查租约8. 总结
核心认知
- 网桥 = 虚拟交换机,工作在 L2,只管 MAC 地址转发,不管 IP
- vnet = 虚拟网线,一端是 VM 的 enp1s0,另一端是宿主机的 vnet 端口
- br0 的 IP 是宿主机自己的,跟 VM 的 IP 完全独立,路由器看到的是不同 MAC
- virbr0 = NAT + DHCP,VM 躲在宿主机后面,外面看不到
工具速查
| 你想知道… | 用这个命令 |
|---|---|
| 接口有没有 IP | ip -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
以下是可爱的评论们:

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