QEMU网络配置1

Linux 上为 QEMU 虚拟机搭建网络。

本篇用ip配置,模拟网络行为,下一篇用qemu配置。


长求总

总结1:vxlan总结

  • vxlan负责将每个vm组成2层网络,可用交换机通信,例如VM:10.255.1.1可以与VM:10.255.1.2通信。

  • vxlan可以看做一个网线(收集2层流量包装到走3层UDP,然后发到对端给2层使用)把分散在不同物理节点上的 bridge 合并成一个跨节点的大虚拟交换机,让所有 VM 像在同一个局域网里一样通信。

  • vxlan价值:构建一个跨节点的L2网络,让 VM 可以带着自己的 IP 和 MAC 在节点间自由移动。

  • vxlan为什么要连bridge?因为网线要插到交换机上和其他mac进行通信,vm已经插在bridge上了,vxlan也要接入。

  • vxlan在NODE_A_IP配置时,在哪配置对端IP?通过bridge fdb append 00:00:00:00:00:00 dev neon-vxlan0 dst ${NODE_B_IP}来配置,告诉 VXLAN "不认识的 MAC 都发给 NODE_B_IP"

    Node A (北京) Node B (上海)
    ┌─────────────────┐ ┌─────────────────┐
    │ VM: 10.255.1.1 │ │ VM: 10.255.1.2 │
    │ ↓ eth frame │ │ ↑ eth frame │
    │ neon-br0 │ │ neon-br0 │
    │ ↓ │ │ ↑ │
    │ neon-vxlan0 │ │ neon-vxlan0 │
    │ ↓ 封装成 UDP │ │ ↑ 解封装 │
    │ eth0: 10.0.1.1 │ ── UDP:4789 ──► │ eth0: 10.0.1.2 │
    └─────────────────┘ 物理网络 └─────────────────┘

    外层 IP: 10.0.1.1 → 10.0.1.2 (Node 物理 IP, L3)
    内层帧: VM MAC → VM MAC, 10.255.1.1 → 10.255.1.2 (overlay L2)

总结2:vm内如何和外部通信

  • vm是部署在pod中的,pod中会配置tap-def虚拟网卡,然后启动qemu绑定这个虚拟网卡。

  • pod中还会配置bridge(2层交换机)然后把tap插到交换机上br-def,这样vm就可以和pod通信了。

  • vm想要和外部通信的话,pod上会用iptables处理vm中发来的包,出站是会把vm的内部ip换成pod的ip。入站的时候DNAT发给vm,这样vm就可以和外面进行通信了,外面看到的是pod的ip,看不到vm内部ip。

    复制代码
    ┌─────────────────────────────────────────────────────────────────────────┐                                                                                           23:22:15 [81/873]
    │                          VM 内部 (Guest OS)                             │
    │                                                                         │
    │   App (如 Postgres)                                                     │
    │       ↑↓                                                                │
    │   eth0 (virtio-net, IP: 10.222.0.2/30, MAC: 52:54:00:12:34:56)          │
    │       ↑↓                                                                │
    │   路由表: default via 10.222.0.1 (网关 = bridge IP)                       │
    └───────╂─────────────────────────────────────────────────────────────────┘
            ║
            ║  QEMU 进程内部:eth0 ←→ tap-def 一一映射
            ║  VM 写入 eth0 的帧 → QEMU 写入 tap-def 的文件描述符
            ║  tap-def 收到的帧 → QEMU 注入 VM 的 eth0
            ║
    ┌───────╂─────────────────────────────────────────────────────────────────┐
    │       ║                Pod / 宿主 网络栈                                  │
    │       ▼                                                                 │
    │  ┌──────────┐                                                           │
    │  │ tap-def  │  (TAP 设备,无 IP,纯 L2 端口)                               │
    │  └────┬─────┘                                                           │
    │       │ master br-def(插在 bridge 上,像一根网线)                         │
    │       ▼                                                                 │
    │  ┌─────────────────────────────────┐                                    │
    │  │ br-def (Linux Bridge 虚拟交换机) │                                    │
    │  │ IP: 10.222.0.1/30              │                                     │
    │  │                                 │                                    │
    │  │  端口:                          │                                    │
    │  │   - tap-def (连 VM)             │                                    │
    │  │   - br-def 自身 (连宿主协议栈)   │                                    │
    │  └────────────┬────────────────────┘                                    │
    │               │                                                         │
    │               │ 宿主路由表判断下一跳                                      │
    │               ▼                                                         │
    │  ┌─────────────────────────────────────────────────┐                    │
    │  │              iptables                            │                    │
    │  │                                                  │                    │
    │  │  [入站 - PREROUTING]                             │                    │
    │  │   外部:5432 → DNAT → 10.222.0.2:5432           │                    │
    │  │                                                  │                    │
    │  │  [转发 - FORWARD]                                │                    │
    │  │   ACCEPT -i br-def                               │                    │
    │  │   ACCEPT -o br-def                               │                    │
    │  │                                                  │                    │
    │  │  [出站 - POSTROUTING]                            │                    │
    │  │   MASQUERADE: src 10.222.0.2 → src Pod_IP      │                    │
    │  └────────────────────┬────────────────────────────┘                    │
    │                       ▼                                                 │
    │                 ┌──────────┐                                            │
    │                 │ eth0/eth1│  Pod 的物理网卡 (Pod_IP)                    │
    │                 └────┬─────┘                                            │
    └──────────────────────╂──────────────────────────────────────────────────┘
                           ║
                        互联网 / K8s 集群网络

前置

1. 网络命名空间 (Network Namespace)

Linux 的 Network Namespace 是网络隔离的基本单元。每个命名空间拥有独立的:

  • 网卡 (interfaces)
  • 路由表 (routes)
  • iptables 规则
  • ARP 缓存

类比:想象你的电脑有多个"独立网络世界",每个世界看不到其他世界的网卡。

复制代码
宿主机 (默认命名空间)              VM 命名空间
┌──────────────────────┐      ┌──────────────────────┐
│ eth0: 10.0.0.100     │      │ eth0: 169.254.0.2    │
│ lo:   127.0.0.1      │      │ lo:   127.0.0.1      │
│ br-def: 169.254.0.1  │      │ (通过 DHCP 获取 IP)   │
└──────────────────────┘      └──────────────────────┘
     各有各的路由表、iptables             完全隔离

在 Kubernetes 中,每个 Pod 就是一个 Network Namespace

bash 复制代码
# 查看当前所有网络命名空间
ip netns list

# 创建一个新的网络命名空间
ip netns add test-ns

# 在命名空间内执行命令
ip netns exec test-ns ip addr

# 删除
ip netns del test-ns

2. TAP 设备

TAP 设备是一种虚拟网卡 ,工作在 L2(数据链路层),可以收发以太网帧。

类比:TAP 就像一根"虚拟网线",一端插在你的 Linux 系统上,另一端插在 QEMU 虚拟机里。

复制代码
Linux 系统                    QEMU 虚拟机
┌──────────┐                 ┌──────────┐
│          │  ← TAP 设备 →   │          │
│ tap-def  │═══════════════  │ eth0     │
│          │   虚拟网线       │          │
└──────────┘                 └──────────┘

TAP vs TUN 的区别:

  • TAP :传输以太网帧(L2),有 MAC 地址 → QEMU 用这个
  • TUN:传输 IP 包(L3),无 MAC 地址 → VPN 常用
bash 复制代码
# 创建 TAP 设备
ip tuntap add mode tap name tap0

# 查看
ip link show tap0

# 删除
ip link del tap0

3. Linux Bridge(网桥)

Bridge 是一个虚拟交换机 ,工作在 L2,连接多个网络设备并在它们之间转发以太网帧。

类比:想象你买了一个 4 口交换机,把几根网线插进去,所有设备就能互相通信了。Linux Bridge 就是软件版的交换机。

复制代码
                    ┌─────────────────────┐
                    │    br-def (Bridge)  │
                    │    = 虚拟交换机       │
                    │                     │
端口1: tap-def ─────┤                      │
端口2: tap-xxx ─────┤     自动学习 MAC      │
端口3: ...    ──-───┤     转发以太网帧       │
                    └─────────────────────┘
bash 复制代码
# 创建 bridge
ip link add name br0 type bridge

# 把 tap0 加入 bridge(相当于"插网线")
ip link set tap0 master br0

# 启动
ip link set br0 up
ip link set tap0 up

# 查看 bridge 上有哪些端口
bridge link show

4. iptables / NAT / MASQUERADE

iptables 是 Linux 的防火墙 + 网络地址转换 (NAT) 工具。

三种关键操作

操作 含义 类比
MASQUERADE 出站时把源 IP 改成宿主 IP 你家所有设备通过路由器上网,外面只看到路由器的 IP
DNAT 入站时把目标 IP/端口改成 VM 的 IP/端口 路由器的端口转发功能
SNAT 出站时把源 IP 改成固定 IP 和 MASQUERADE 类似,但 IP 固定
复制代码
外部 → Pod IP:5432 → DNAT → VM_IP:5432  (入站)
VM_IP:any → MASQUERADE → Pod IP:any → 外部  (出站)
bash 复制代码
# 查看 NAT 表的所有规则
iptables -t nat -L -n -v

# 添加 MASQUERADE (所有从主网卡出去的流量,源地址伪装)
iptables -t nat -A POSTROUTING -o eth0 -j MASQUERADE

# 添加 DNAT (外部访问 5432 端口时,转发到 VM)
iptables -t nat -A PREROUTING -i eth0 -p tcp --dport 5432 -j DNAT --to 10.222.0.2:5432

# 允许 IP 转发
echo 1 > /proc/sys/net/ipv4/ip_forward

5. dnsmasq(轻量 DHCP 服务器)

dnsmasq 是一个小巧的 DNS/DHCP 服务器。Neon 用它给 VM 自动分配 IP 地址。

为什么需要:QEMU 里的 VM 启动时不知道自己的 IP,需要通过 DHCP 获取。dnsmasq 监听在 bridge 上,响应 VM 的 DHCP 请求。

复制代码
VM 启动 → eth0 发 DHCP Discover
   → TAP 设备收到
   → Bridge 转发给 dnsmasq
   → dnsmasq 回复 DHCP Offer (IP=10.222.0.2, GW=10.222.0.1, DNS=...)
   → VM 配置好网络
bash 复制代码
# 最简模式:只做 DHCP,不做 DNS
dnsmasq --port=0 \
    --no-resolv \
    --bind-interfaces \
    --interface=br-def \
    --dhcp-range=10.222.0.2,static,255.255.255.252 \
    --dhcp-host=52:54:00:12:34:56,10.222.0.2,infinite \
    --dhcp-option=option:router,10.222.0.1 \
    --dhcp-option=option:dns-server,119.29.29.29

6. VXLAN(虚拟可扩展局域网)

VXLAN 是一种 L2-over-L3 的隧道协议:把完整的 L2 以太网帧封装在 L3 的 UDP 包里,通过物理网络传输。

关键理解 :VXLAN 不是"运行在 L3 上的 L3 协议",而是借助 L3(Node IP + UDP)来传输 L2 帧。VM 看到的是一个虚拟的 L2 交换网络。

复制代码
封装结构(一个 VXLAN 数据包的内部):
┌─────────────────────────────────────────────────────────┐
│ 外层以太网头   │ 外层 IP 头       │ UDP 头     │ VXLAN 头 │  ← 物理网络 (L3)
│ (Node MAC)    │ (Node IP)       │ (port 4789)│ (VNI=100)│
├─────────────────────────────────────────────────────────┤
│ 内层以太网头   │ 内层 IP 头       │ TCP/数据               │  ← 虚拟 L2 网络
│ (VM MAC)      │ (VM overlay IP) │                        │     VM 看到的世界
└─────────────────────────────────────────────────────────┘

类比:想象你在北京和上海各有一个办公室,想让两个办公室的电脑像在同一个局域网一样通信。VXLAN 就像在互联网上架了一条"虚拟网线"------外面走的是快递(IP 包),里面装的是局域网帧。

复制代码
Node A (北京)                         Node B (上海)
┌─────────────────┐                  ┌─────────────────┐
│ VM: 10.255.1.1  │                  │ VM: 10.255.1.2  │
│   ↓ eth frame   │                  │   ↑ eth frame   │
│ neon-br0        │                  │ neon-br0        │
│   ↓             │                  │   ↑             │
│ neon-vxlan0     │                  │ neon-vxlan0     │
│   ↓ 封装成 UDP  │                  │   ↑ 解封装      │
│ eth0: 10.0.1.1  │ ── UDP:4789 ──► │ eth0: 10.0.1.2  │
└─────────────────┘    物理网络       └─────────────────┘

外层 IP: 10.0.1.1 → 10.0.1.2 (Node 物理 IP, L3)
内层帧:  VM MAC → VM MAC,  10.255.1.1 → 10.255.1.2 (overlay L2)

VXLAN 的本质功能 :把分散在不同物理节点上的 bridge 合并成一个跨节点的大虚拟交换机,让所有 VM 像在同一个局域网里一样通信。

复制代码
Node A                      Node B                      Node C
┌────────────┐             ┌────────────┐             ┌────────────┐
│ VM-1  VM-2 │             │ VM-3  VM-4 │             │ VM-5       │
│  │     │   │             │  │     │   │             │  │         │
│ neon-br0   │             │ neon-br0   │             │ neon-br0   │
│     │      │             │     │      │             │     │      │
│ neon-vxlan0│             │ neon-vxlan0│             │ neon-vxlan0│
└─────┼──────┘             └─────┼──────┘             └─────┼──────┘
      │                          │                          │
══════╧══════════════════════════╧══════════════════════════╧═══════
                    物理网络 (L3, UDP:4789)
              三个 neon-br0 通过 VXLAN 合并成一个大交换机

热迁移时 :VM 从 Node A 搬到 Node B,因为还在同一个"虚拟交换机"上,IP 和 MAC 都不用变

Overlay IP 会不会冲突?

会冲突,如果不管理的话。所以 Neon 有专门的 IP 池管理机制(IPAM) ,源码在 neonvm/apis/neonvm/v1/ippool_types.go

go 复制代码
type IPPoolSpec struct {
    // CIDR 范围,例如 "10.255.0.0/16"(可容纳 65534 个 VM)
    Range       string
    // 已分配的 IP → 对应的 Pod(防止重复分配)
    Allocations map[string]IPAllocation
}

工作流程和你家路由器的 DHCP 一样:

  1. 管理员创建 IPPool CRD,定义 CIDR 范围(Neon 用 10.100.0.0/16
  2. 每个 VM 创建时,从池中分配一个唯一的 overlay IP
  3. 分配记录写入 Allocations,保证不冲突
  4. VM 销毁时释放 IP

注:早期 Neon 用自己写的 IPAM,后来(v0.8.0)换成了标准的 Whereabouts CNI,原理一样。

VXLAN 的关键参数:

  • VNI (VXLAN Network Identifier) :类似 VLAN ID,标识哪个虚拟网络。Neon 用 100
  • VTEP (VXLAN Tunnel Endpoint):隧道端点,就是物理机的 IP
  • UDP Port :默认 4789
bash 复制代码
# 创建 VXLAN 接口
ip link add neon-vxlan0 type vxlan \
    id 100 \
    local 9.134.130.23 \
    dstport 4789 \
    learning

# 加入 bridge
ip link set neon-vxlan0 master neon-br0
ip link set neon-vxlan0 up

7. FDB(转发数据库)

FDB 是 bridge 的 MAC 地址转发表,记录"哪个 MAC 地址在哪个端口后面"。

对于 VXLAN,FDB 还记录"哪个 MAC 地址在哪个远程 VTEP 后面"。

bash 复制代码
# 查看 FDB 表
bridge fdb show dev neon-vxlan0

# 添加 broadcast FDB entry(让 VXLAN 知道把 broadcast 帧发给哪些远程节点)
bridge fdb append 00:00:00:00:00:00 dev neon-vxlan0 dst 21.6.160.243

第一部分:Pod 内网络(单机)

这一部分模拟 Neon 的 neonvm-runner 如何在 一个 Pod 内部 为 QEMU VM 搭建网络。

对应代码neonvm-runner/cmd/net.go 中的 defaultNetwork() 函数

目标

复制代码
外部世界
    │
    ▼
Pod eth0 (宿主机 IP)
    │
    │ iptables MASQUERADE + DNAT
    ▼
br-def (bridge, IP=10.222.0.1/30)
    │
    │ (bridge 转发)
    ▼
tap-def (TAP device)
    │
    │ (虚拟网线)
    ▼
QEMU VM eth0 (10.222.0.2/30, via DHCP)

Step 0:环境准备

bash 复制代码
# 需要 root 权限
sudo -i

# 安装必要工具
# Ubuntu/Debian:
apt-get update && apt-get install -y \
    iproute2 iptables dnsmasq bridge-utils qemu-system-x86

# CentOS/RHEL/TencentOS:
yum install -y iproute iptables dnsmasq bridge-utils qemu-kvm

# 开启内核 IP 转发(重要!否则跨网段通信不工作)
echo 1 > /proc/sys/net/ipv4/ip_forward
# 持久化:
sysctl -w net.ipv4.ip_forward=1

Step 1:IP 地址规划

Neon 使用一个 /30 的最小子网 (只有 4 个 IP,2 个可用),和 Neon 的 calcIPs() 函数完全一致。

Neon 代码中的计算逻辑 (net.go L65-79):

go 复制代码
func calcIPs(cidr string) (net.IP, net.IP, net.IPMask, error) {
    _, ipv4Net, err := net.ParseCIDR(cidr)  // 得到网络地址
    ip0 := ipv4Net.IP.To4()                 // 网络地址
    ip1 := ip0 + 1  // bridge IP (Pod 侧网关)
    ip2 := ip0 + 2  // VM IP
    return ip1, ip2, mask, nil
}

Neon 硬编码的 CIDR 是 "169.254.254.252/30"net.go L24),实际计算出:

  • 网络地址: 169.254.254.252, Bridge IP: 169.254.254.253, VM IP: 169.254.254.254

我们教程使用一个 /30 私有子网,用法完全一样:

bash 复制代码
# ============================================
# 教程使用的 IP 规划(/30 子网,和 Neon 一样)
# ============================================
# CIDR: 10.222.0.0/30
# 网络地址:  10.222.0.0  (不可用)
# IP1:       10.222.0.1  → Bridge IP(VM 的网关)------ 对应 Neon 的 169.254.254.253
# IP2:       10.222.0.2  → VM IP                ------ 对应 Neon 的 169.254.254.254
# 广播地址:  10.222.0.3  (不可用)

BRIDGE_IP="10.222.0.1"     # Bridge 端 IP(Neon 的 ipPod)
VM_IP="10.222.0.2"         # VM 端 IP(Neon 的 ipVm)
NETMASK="255.255.255.252"     # /30 掩码
VM_MAC="52:54:00:12:34:56"    # VM 的 MAC 地址(Neon 用 mac.GenerateRandMAC())

Neon 代码对照net.go L65-79 calcIPs() 函数从 CIDR 计算出两个 IP

为什么 /30? 每个 VM 只需要 2 个 IP(bridge 和 VM 各一个)。/30 刚好 4 个 IP,去掉网络和广播地址,剩 2 个可用,最节省 IP。

Step 2:创建 Bridge

bash 复制代码
# 创建 Linux bridge(虚拟交换机)
ip link add name br-def type bridge

# 给 bridge 分配 IP 地址
# 这个 IP 就是 VM 的默认网关
ip addr add ${BRIDGE_IP}/30 dev br-def

# 启动 bridge
ip link set br-def up

# 重要:禁用 bridge 接口的反向路径过滤XZ
# 否则从 VM 到 bridge 自身的包可能被丢弃
sysctl -w net.ipv4.conf.br-def.rp_filter=0

# 验证
ip addr show br-def
# 应该看到:
#   br-def: <BROADCAST,MULTICAST,UP,...> ...
#   inet 10.222.0.1/30 scope global br-def

Neon 代码对照net.go L92-119 创建 bridge 并分配 IP

Step 3:创建 TAP 设备

bash 复制代码
# 确保 /dev/net/tun 存在(QEMU 需要它)
ls -la /dev/net/tun
# 如果不存在:
mkdir -p /dev/net
mknod /dev/net/tun c 10 200
chmod 666 /dev/net/tun

# 创建 TAP 设备(multiqueue 模式)
ip tuntap add mode tap name tap-def multi_queue

# 把 TAP 加入 bridge(相当于"插网线"到交换机)
ip link set tap-def master br-def

# 启动 TAP
ip link set tap-def up

# 验证
bridge link show
# 应该看到:
#   tap-def: <BROADCAST,MULTICAST,UP,LOWER_UP> ... master br-def

Neon 代码对照net.go L122-154 创建 TAP 并加入 bridge

现在的网络拓扑:

复制代码
br-def (10.222.0.1/30) ─── tap-def (无 IP,纯 L2 转发)
   │
   └── 以后 QEMU 会把 tap-def 作为虚拟网卡使用

Step 4:配置 iptables NAT

bash 复制代码
# ============================================
# 4a. MASQUERADE:VM 出站流量伪装成宿主 IP
# ============================================
# 把主网卡名称替换为你的实际网卡名(如 eth0, eth1, ens33, enp0s3)
HOST_IFACE=$(ip route | grep default | awk '{print $5}' | head -1)
echo "自动检测主网卡: ${HOST_IFACE}"

iptables -t nat -A POSTROUTING -o ${HOST_IFACE} -j MASQUERADE

# 解释:
# -t nat          → 操作 NAT 表
# -A POSTROUTING  → 在包离开本机前执行
# -o eth0         → 只匹配从主网卡出去的包
# -j MASQUERADE   → 把源 IP 改成主网卡的 IP

# ============================================
# 4b. DNAT:外部访问 5432 端口时转发到 VM
# ============================================
iptables -t nat -A PREROUTING \
    -i ${HOST_IFACE} -p tcp --dport 5432 \
    -j DNAT --to ${VM_IP}:5432

# ============================================
# 4c. FORWARD 规则(重要!)
# ============================================
# 在有 Docker/containerd 的环境中,FORWARD 链默认策略是 DROP。
# 必须显式允许 bridge 相关的流量转发,否则 VM 无法和外界通信。
# 在 Neon 的 K8s 环境中,这由 CNI 插件自动处理。
iptables -I FORWARD -i br-def -j ACCEPT
iptables -I FORWARD -o br-def -j ACCEPT

# ============================================
# 4d. 本地流量转发(localhost 也能访问 VM)
# ============================================

# 第 1 条:本机访问 5432 时,目标地址改成 VM
# 效果:psql -h localhost -p 5432 → 目标从 127.0.0.1:5432 改成 10.222.0.2:5432
iptables -t nat -A OUTPUT \
    -m addrtype --src-type LOCAL --dst-type LOCAL \
    -p tcp --dport 5432 \
    -j DNAT --to-destination ${VM_IP}:5432

# 第 2 条:允许这个转发后的包通过
# DNAT 改完目标地址后,包变成了 127.0.0.1 → 10.222.0.2,这种"从 lo 到外部 IP"的包可能被某些安全规则拦截,所以显式放行。
iptables -A OUTPUT \
    -s 127.0.0.1 -d ${VM_IP} \
    -p tcp --dport 5432 \
    -j ACCEPT

# 第 3 条:回包时源地址伪装
# 包到达 VM 时,源地址是 127.0.0.1。VM 回包给 127.0.0.1 会发给自己的 loopback,回不到宿主机。
# MASQUERADE 把源地址从 127.0.0.1 改成宿主机在 bridge 上的 IP(10.222.0.1),VM 回包就能正确回到宿主机。
iptables -t nat -A POSTROUTING \
    -m addrtype --src-type LOCAL --dst-type UNICAST \
    -j MASQUERADE

# ============================================
# 验证规则
# ============================================
echo "=== NAT 表 ==="
iptables -t nat -L -n -v --line-numbers
echo ""
echo "=== Filter 表 (FORWARD) ==="
iptables -L FORWARD -n -v --line-numbers | head -10

Neon 代码对照net.go L157-208 设置 MASQUERADE 和 DNAT 规则

Step 5:启动 DHCP 服务器 (dnsmasq)

IP 写死在 Pod 侧(宿主侧),但 VM 内部的 Guest OS 并不知道这件事,dnsmasq 不是在"分配"IP,而是在"通知"VM 它应该用的 IP。

go 复制代码
// net.go L221-244
  dnsMaskCmd := []string{
      "--port=0",                    // 不做 DNS
      "--dhcp-range=%s,static,...",  // static! 不是动态分配
      "--dhcp-host=%s,%s,infinite",  // MAC → IP 一对一绑定,永不过期
      //           ↑    ↑
      //         VM MAC  169.254.254.254 (写死的)
      "--dhcp-option=option:router,%s",      // 网关 169.254.254.253 (写死的)
      "--dhcp-option=option:dns-server,%s",  // DNS
  }

命令:

bash 复制代码
# 启动 dnsmasq,只做 DHCP(不做 DNS)
dnsmasq \
    --port=0 \
    --no-resolv \
    --bind-interfaces \
    --dhcp-authoritative \
    --interface=br-def \
    --dhcp-range=${VM_IP},static,${NETMASK} \
    --dhcp-host=${VM_MAC},${VM_IP},infinite \
    --dhcp-option=option:router,${BRIDGE_IP} \
    --dhcp-option=option:dns-server,8.8.8.8 \
    --dhcp-option=option:domain-search,default.svc.cluster.local

# 各参数解释:
# --port=0                不提供 DNS 服务
# --no-resolv             不读取 /etc/resolv.conf
# --bind-interfaces       只监听指定接口
# --dhcp-authoritative    作为权威 DHCP 服务器
# --interface=br-def      监听 bridge 接口
# --dhcp-range=...,static 只分配静态 IP
# --dhcp-host=MAC,IP,infinite  把这个 MAC 绑定到这个 IP,永不过期
# --dhcp-option=router    告诉 VM 默认网关是 bridge IP
# --dhcp-option=dns       告诉 VM DNS 服务器

# 验证 dnsmasq 运行
ps aux | grep dnsmasq
# 或者查看日志
cat /var/log/syslog | grep dnsmasq  # Ubuntu
journalctl -u dnsmasq               # systemd

Neon 代码对照net.go L221-244 构造 dnsmasq 命令行

Step 6:验证单机网络(无 QEMU)

在启动 QEMU 之前,我们可以用 ip netns 模拟 VM 端测试网络连通性:

bash 复制代码
# ============================================
# 用 network namespace 模拟 VM 测试
# ============================================

# 创建一个 veth pair(虚拟网线对)
ip link add veth-vm type veth peer name veth-br

# 把 veth-br 端加入 bridge(模拟 TAP)
ip link set veth-br master br-def
ip link set veth-br up

# 创建 namespace 模拟 VM
ip netns add fake-vm

# 把 veth-vm 移入 namespace
ip link set veth-vm netns fake-vm

# 在 namespace 内配置 IP(注意:使用和 bridge 相同的 /30 子网!)
ip netns exec fake-vm ip addr add ${VM_IP}/30 dev veth-vm
ip netns exec fake-vm ip link set veth-vm up
ip netns exec fake-vm ip link set lo up
ip netns exec fake-vm ip route add default via ${BRIDGE_IP}

# ============================================
# 测试连通性
# ============================================

# 测试:从 VM namespace ping bridge(网关)
ip netns exec fake-vm ping -c 3 ${BRIDGE_IP}
# 应该成功 ✓

# 测试:从 VM namespace ping 外网
ip netns exec fake-vm ping -c 3 119.29.29.29
# 如果 IP 转发、MASQUERADE、FORWARD 规则都配置正确,应该成功 ✓
# 注意:某些公网 IP 可能不响应 ICMP,可以换用你能 ping 通的地址

# 测试:从宿主 ping VM
ping -c 3 ${VM_IP}
# 应该成功 ✓

# 清理测试环境
ip netns del fake-vm
ip link del veth-br 2>/dev/null

vm是怎么找到dhcp的?

复制代码
● VM 根本不需要"找到" DHCP 服务器,因为 DHCP 协议本身就是基于广播的。

  完整过程如下:

  时间线                Pod 侧                              VM 内部 (Guest OS)
  ─────────────────────────────────────────────────────────────────────────────

  1. QEMU 启动前      neonvm-runner 创建:
                       br-def (169.254.254.253/30)
                           │
                       tap-def ── 加入 br-def
                           │
                       dnsmasq 监听在 br-def 上
                       (绑定 MAC→169.254.254.254)

  2. QEMU 启动        QEMU 用 tap-def 作为网卡后端
                       -netdev tap,ifname=tap-def
                       -device virtio-net-pci,mac=XX

                                                          Linux kernel 启动
                                                          识别到 eth0 (virtio-net)

  3. vminit 执行                                          ip link set up dev eth0
                                                          # 网卡 UP 了,但没有 IP

  4. Guest OS 的                                          udhcpc -i eth0  (或 dhclient)
     DHCP 客户端启动                                       │
                                                          ▼
  5. DHCP Discover    ◄─────────────────────────── 广播: "谁能给我 IP?"
     (L2 广播帧)                                          src MAC: VM的MAC
                                                          dst MAC: ff:ff:ff:ff:ff:ff
                      这个广播帧怎么走的:                    dst IP:  255.255.255.255
                                                          src IP:  0.0.0.0
                      VM eth0
                        ↓ (QEMU 内部转发)
                      tap-def
                        ↓ (tap 是 bridge 的端口)
                      br-def ← bridge 收到广播帧
                        ↓       把它转发给所有端口
                      dnsmasq ← 因为 dnsmasq 绑定在
                                br-def 上,所以收到了

  6. DHCP Offer       dnsmasq 回复: ──────────────────► 收到!配置网络:
                      "IP=169.254.254.254"                    ip=169.254.254.254
                      "GW=169.254.254.253"                     gw=169.254.254.253
                      "DNS=xxx"                            dns=xxx
                      "租约=infinite"

  7. DHCP Request     ◄─────────────────────────── "我要用 169.254.254.254"

  8. DHCP ACK         "确认,169.254.254.254 是你的" ────►  网络就绪 ✓

  关键点:DHCP 的 Discover 是 L2 广播帧(目标 MAC = ff:ff:ff:ff:ff:ff),不需要知道 DHCP 服务器在哪。bridge 会把广播帧转发给所有端口,dnsmasq 监听在 bridge 上自然就收到了。

  这和你家里的情况一模一样:你的笔记本连上 WiFi 后不知道路由器 IP 是多少,但它发一个广播"谁能给我 IP",路由器的 DHCP 服务自然收到并回复。

  整个发现过程只依赖 L2 广播,不需要任何预知的 IP 地址。这也是为什么 dnsmasq 必须绑定在 br-def 这个 bridge 接口上(--interface=br-def)------它和 tap-def(VM 的虚拟网线)在同一个 bridge
  上,广播帧自然互通。

常见问题:如果 VM→Bridge 的 ping 失败(Host→VM 却成功),通常是因为:

  1. FORWARD 链默认 DROP (Docker 环境)→ 需要 Step 4c 的 FORWARD -i br-def -j ACCEPT 规则
  2. IP 不在同一子网 → 确保 BRIDGE_IP 和 VM_IP 在同一个 /30 内
  3. bridge-nf-call-iptables=1 → 如果还不行,尝试 sysctl -w net.bridge.bridge-nf-call-iptables=0

Step 7:清理第一部分实验

bash 复制代码
# 如果需要清理,执行以下命令:
# (如果要继续第三部分的 QEMU 实验,先不要清理)

# 停止 dnsmasq
killall dnsmasq

# 删除 iptables 规则
iptables -t nat -F  # 清空 NAT 表
iptables -F         # 清空 Filter 表

# 删除网络设备
ip link del tap-def
ip link del br-def

第二部分:Overlay 网络(跨机)

这一部分模拟 Neon 的 neonvm-vxlan-controller 如何建立跨节点的 VXLAN overlay 网络。

对应代码neonvm-vxlan-controller/cmd/main.go

先搞清楚:K8s 节点之间本来就互通,为什么还需要 VXLAN?

K8s 节点之间确实是互通的,但它们是 L3 互通(IP 路由层) ,不是 L2 互通(以太网交换层)。这个区别在热迁移时是致命的。

没有 VXLAN 会发生什么:

复制代码
迁移前:
  客户端 TCP 连接 → Pod A IP (10.0.1.5:5432) → DNAT → VM (169.254.254.254:5432)
                    ↑ 客户端记住的是这个 IP

迁移后:VM 搬到了 Node B,运行在 Pod B 里
  Pod A (10.0.1.5) 已经不存在了
  Pod B (10.0.2.8) 是新的 IP
  客户端还在往 10.0.1.5 发包 → 找不到了 → TCP 连接断开 ✗

每个 Pod 的 IP 是绑定在特定节点上的,VM 搬家后老 Pod 消失、新 Pod 的 IP 不同,客户端的 TCP 连接就断了。

有 VXLAN 的方案:给 VM 一个不绑定在任何节点上的 "浮动 IP"

复制代码
迁移前:
  客户端 TCP 连接 → Overlay IP (172.20.0.100:5432) → Node A 上的 VM
                    ↑ 这个 IP 在 VXLAN 虚拟交换机上
                      不属于任何节点,跟着 VM 走

迁移后:VM 搬到了 Node B
  VM 发送 Gratuitous ARP: "172.20.0.100 现在在 Node B 了!"
  VXLAN 的 FDB 表更新 → 流量自动转向 Node B

  客户端 TCP 连接 → Overlay IP (172.20.0.100:5432) → Node B 上的 VM
                    ↑ 同一个 IP、同一个 MAC
                      TCP 连接不断 ✓

VXLAN 的核心价值:构建一个跨节点的 L2 网络,让 VM 可以带着自己的 IP 和 MAC 在节点间自由移动。

这和你在办公室拔掉网线插到隔壁工位一样------你的笔记本 IP 和 MAC 都没变,局域网交换机自动学到了你的新位置。VXLAN 就是把这个能力扩展到了跨物理机的场景。

L3 互通(普通 K8s 网络) L2 互通(VXLAN overlay)
寻址方式 IP 地址 + 路由 MAC 地址 + 交换
IP 归属 绑定在特定 Pod/节点上 跟着 VM 走,不绑节点
VM 搬家后 老 IP 消失,新 IP 出现 同一个 IP,同一个 MAC
TCP 连接 断开 不断

目标

复制代码
Node A (9.134.130.23)                   Node B (21.6.160.243)
┌──────────────────┐                    ┌──────────────────┐
│ VM-A: 10.0.100.5 │                    │ VM-B: 10.0.100.6 │
│       ↕          │                    │       ↕          │
│   neon-br0       │                    │   neon-br0       │
│       ↕          │                    │       ↕          │
│  neon-vxlan0     │                    │  neon-vxlan0     │
│  (VNI=100)       │                    │  (VNI=100)       │
│       ↕          │                    │       ↕          │
│  eth0:9.134.130.23  ◄──UDP:4789──►  eth0:21.6.160.243  │
└──────────────────┘                    └──────────────────┘

环境要求

  • 两台 Linux 机器(或两个虚拟机),网络互通
  • 如果只有一台机器,可以用两个 network namespace 模拟(见下方"单机模拟")

方案 A:两台真实机器

在 Node A 上执行:
bash 复制代码
# ============================================
# Node A: 9.134.130.23(改成你的实际 IP)
# ============================================
NODE_A_IP="9.134.130.23"
NODE_B_IP="21.6.160.243"

# Step 0: 禁止 bridge 流量经过 iptables(否则会被 FORWARD DROP 拦截)
sysctl -w net.bridge.bridge-nf-call-iptables=0
sysctl -w net.bridge.bridge-nf-call-ip6tables=0

# Step 1: 创建 bridge
ip link add name neon-br0 type bridge
ip link set neon-br0 up

# Step 2: 创建 VXLAN 接口
ip link add neon-vxlan0 type vxlan \
    id 100 \
    local ${NODE_A_IP} \
    dstport 4789 \
    learning

# Step 3: 把 VXLAN 加入 bridge
ip link set neon-vxlan0 master neon-br0
ip link set neon-vxlan0 up

# Step 4: 添加 FDB 条目(告诉 VXLAN 把 broadcast 帧发给 Node B)
# >>> 告诉 VXLAN "不认识的 MAC 都发给 21.6.160.243" <<<
bridge fdb append 00:00:00:00:00:00 dev neon-vxlan0 dst ${NODE_B_IP}

# Step 5: 创建 TAP 设备模拟 VM 连接
ip tuntap add mode tap name tap-vm-a
ip link set tap-vm-a master neon-br0
ip link set tap-vm-a up

# Step 6: 用 namespace 模拟 VM
ip netns add vm-a
ip link add veth-a type veth peer name veth-a-br
ip link set veth-a-br master neon-br0
ip link set veth-a-br up
ip link set veth-a netns vm-a
ip netns exec vm-a ip addr add 10.100.0.1/24 dev veth-a
ip netns exec vm-a ip link set veth-a up
ip netns exec vm-a ip link set lo up

echo "Node A 配置完成!VM-A IP: 10.100.0.1"
在 Node B 上执行:
bash 复制代码
# ============================================
# Node B: 21.6.160.243(改成你的实际 IP)
# ============================================
NODE_A_IP="9.134.130.23"
NODE_B_IP="21.6.160.243"

# Step 0: 禁止 bridge 流量经过 iptables(否则会被 FORWARD DROP 拦截)
sysctl -w net.bridge.bridge-nf-call-iptables=0
sysctl -w net.bridge.bridge-nf-call-ip6tables=0

# Step 1-4: 同样的步骤
ip link add name neon-br0 type bridge
ip link set neon-br0 up

ip link add neon-vxlan0 type vxlan \
    id 100 \
    local ${NODE_B_IP} \
    dstport 4789 \
    learning

ip link set neon-vxlan0 master neon-br0
ip link set neon-vxlan0 up

bridge fdb append 00:00:00:00:00:00 dev neon-vxlan0 dst ${NODE_A_IP}

# Step 5-6: 模拟 VM
ip netns add vm-b
ip link add veth-b type veth peer name veth-b-br
ip link set veth-b-br master neon-br0
ip link set veth-b-br up
ip link set veth-b netns vm-b
ip netns exec vm-b ip addr add 10.100.0.2/24 dev veth-b
ip netns exec vm-b ip link set veth-b up
ip netns exec vm-b ip link set lo up

echo "Node B 配置完成!VM-B IP: 10.100.0.2"
测试跨节点连通性:
bash 复制代码
# 在 Node A 上:
ip netns exec vm-a ping -c 3 10.100.0.2
# 应该能 ping 通 Node B 上的 VM ✓

# 在 Node B 上:
ip netns exec vm-b ping -c 3 10.100.0.1
# 应该能 ping 通 Node A 上的 VM ✓

# 查看 FDB 表,可以看到自动学习到的 MAC 地址
bridge fdb show dev neon-vxlan0

路径

复制代码
Node A (9.134.130.23)                          Node B (21.6.160.243)

vm-a 命名空间                                   vm-b 命名空间
┌─────────────┐                                ┌─────────────┐
│ ping 10.100.0.2                              │ veth-b      │
│ veth-a ─────┤                                │ 10.100.0.2  │
│ 10.100.0.1  │                                └──────┬──────┘
└──────┬──────┘                                       │
  ①   │ veth pair                                ⑧   │ veth pair
       │                                              │
┌──────┴──────────────────────┐          ┌────────────┴─────────────┐
│  veth-a-br                  │          │  veth-b-br               │
│      │                      │          │      │                   │
│  neon-br0 (bridge/交换机)    │          │  neon-br0 (bridge/交换机) │
│      │                      │          │      │                   │
│  neon-vxlan0                │          │  neon-vxlan0             │
└──────┬──────────────────────┘          └──────┬──────────────────┘
  ④   │ 封装成 UDP                           ⑥  │ 解封装
       │                                        │
    eth1:9.134.130.23 ───UDP:4789──→ eth1:21.6.160.243
                      ⑤ 互联网传输

方案 B:单机模拟(用 Network Namespace)

如果只有一台机器,可以用两个 namespace 模拟两个"节点":

bash 复制代码
# ============================================
# 在一台机器上模拟两个节点的 VXLAN 网络
# ============================================

# 创建两个 namespace 模拟两个节点
ip netns add node-a
ip netns add node-b

# 用 veth pair 连接两个"节点"(模拟物理网线)
ip link add veth-ab type veth peer name veth-ba
ip link set veth-ab netns node-a
ip link set veth-ba netns node-b

# 给"节点"分配底层 IP
ip netns exec node-a ip addr add 192.168.99.1/24 dev veth-ab
ip netns exec node-a ip link set veth-ab up
ip netns exec node-a ip link set lo up

ip netns exec node-b ip addr add 192.168.99.2/24 dev veth-ba
ip netns exec node-b ip link set veth-ba up
ip netns exec node-b ip link set lo up

# 验证底层连通性
ip netns exec node-a ping -c 2 192.168.99.2
# ✓ 两个"节点"互通

# ============================================
# 在 node-a 中搭建 VXLAN
# ============================================
ip netns exec node-a bash -c '
    ip link add neon-br0 type bridge
    ip link set neon-br0 up

    ip link add neon-vxlan0 type vxlan \
        id 100 \
        local 192.168.99.1 \
        dstport 4789 \
        learning

    ip link set neon-vxlan0 master neon-br0
    ip link set neon-vxlan0 up

    bridge fdb append 00:00:00:00:00:00 dev neon-vxlan0 dst 192.168.99.2

    # 创建模拟 VM 的接口
    ip link add veth-vm-a type veth peer name veth-vm-a-br
    ip link set veth-vm-a-br master neon-br0
    ip link set veth-vm-a-br up
    ip addr add 10.0.100.5/24 dev veth-vm-a
    ip link set veth-vm-a up
'

# ============================================
# 在 node-b 中搭建 VXLAN
# ============================================
ip netns exec node-b bash -c '
    ip link add neon-br0 type bridge
    ip link set neon-br0 up

    ip link add neon-vxlan0 type vxlan \
        id 100 \
        local 192.168.99.2 \
        dstport 4789 \
        learning

    ip link set neon-vxlan0 master neon-br0
    ip link set neon-vxlan0 up

    bridge fdb append 00:00:00:00:00:00 dev neon-vxlan0 dst 192.168.99.1

    # 创建模拟 VM 的接口
    ip link add veth-vm-b type veth peer name veth-vm-b-br
    ip link set veth-vm-b-br master neon-br0
    ip link set veth-vm-b-br up
    ip addr add 10.0.100.6/24 dev veth-vm-b
    ip link set veth-vm-b up
'

# ============================================
# 测试跨 "节点" 的 VXLAN 连通性
# ============================================
ip netns exec node-a ping -c 3 10.0.100.6
# 应该成功!数据包: vm-a → neon-br0 → neon-vxlan0 → UDP封装 → node-b → neon-vxlan0 → neon-br0 → vm-b ✓

ip netns exec node-b ping -c 3 10.0.100.5
# 同样成功 ✓

echo "=== VXLAN overlay 网络搭建成功! ==="

# 查看 FDB 学习到的 MAC 地址
ip netns exec node-a bridge fdb show dev neon-vxlan0
ip netns exec node-b bridge fdb show dev neon-vxlan0

# ============================================
# 清理
# ============================================
# ip netns del node-a
# ip netns del node-b

Neon 代码对照

  • vxlan-controller/cmd/main.go L71-79:创建 bridge + VXLAN
  • vxlan-controller/cmd/main.go L83-101:循环 30 秒更新 FDB
  • vxlan-controller/cmd/main.go L150-191:createVxlanInterface() 函数
  • vxlan-controller/cmd/main.go L193-227:updateFDB() 函数

理解 FDB 的工作原理

bash 复制代码
# 查看 FDB 表
bridge fdb show dev neon-vxlan0

# 你会看到类似输出:
# 00:00:00:00:00:00 dst 192.168.99.2 self permanent
#   → 这是 broadcast entry,意思是"不知道 MAC 在哪时,发给所有远程节点"
#
# 52:54:00:xx:xx:xx dst 192.168.99.2 self
#   → 这是学习到的 entry,意思是"这个 MAC 地址在 192.168.99.2 那个节点上"

# FDB 工作流程:
# 1. VM-A 发送 ARP 请求 "谁是 10.0.100.6?"
# 2. neon-br0 收到,转发给 neon-vxlan0
# 3. neon-vxlan0 查 FDB,发现是 broadcast → 发给所有远程 VTEP
# 4. Node B 收到 → neon-vxlan0 解封装 → neon-br0 → VM-B
# 5. VM-B 回复 → neon-vxlan0 同时学习到 VM-A 的 MAC 对应 Node A
# 6. 此后 VM-A 的流量直接定向发送给 Node A(不再 broadcast)
相关推荐
牛奶咖啡1315 天前
KVM虚拟化与企业应用实践——给远端主机创建虚拟机
云原生·qemu·kvm·给远端主机创建虚拟机·创建uefi模式的虚拟机·安装openeulersp2·vnc与虚拟机环境搭建
牛奶咖啡1316 天前
KVM虚拟化与企业应用实践——通过网络介质配合ks自动应答文件实现自动安装KVM虚拟机
云原生·qemu·kvm·系统网络引导与ks自动应答环境·远程资源+ks文件安装虚拟机·通过网络介质引导自动安装虚拟机·qemu的总线类型详解
冰山一脚201317 天前
kvm驱动学习笔记
qemu
茶乡浪子17 天前
基于IPv4网络分布式网关动态VXLAN配置示例
网络·数据中心·vxlan·华为vxlan·华为数据中心网络·bgp evpn·数据中心网络工程师
ggaofeng20 天前
如何通过uboot加载硬盘
linux·qemu·uboot
ScilogyHunter20 天前
QEMU完全指南
linux·qemu
茶乡浪子23 天前
同子网基于IPv4网络静态VXLAN配置示例(下)
运维·网络·数据中心·vxlan·evpn·华为vxlan·华为数据中心网络
冰山一脚20131 个月前
qemu的cpu加速器分析笔记
qemu
longji2 个月前
win11 使用 QEMU11 模拟器跑龙芯系统(debian13,openKylin,openEuler,uos,Loongnix)
qemu·龙芯模拟器
冰山一脚20132 个月前
qemu的板级初始化笔记(以i4ffx为例)
qemu