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)
相关推荐
enjoy嚣士1 个月前
Windows10下安装arm64架构的centos
qemu·aarch64 linux·arm64 linux·arm64 centos
x-cmd2 个月前
[x-cmd] QEMU 10.2.0 发布:虚拟机实时更新与性能飞跃的技术深度解读
安全·qemu·虚拟机·x-cmd
yao000372 个月前
基于QEMU+OpenSBI+edk2的riscv启动流程解析
qemu·riscv·uefi·bios·固件·opensbi
三雷科技2 个月前
qemu-img 使用手册(含详细案例)
qemu
河码匠2 个月前
libvirt xml 配置文件说明
qemu·kvm·libvirt
河码匠3 个月前
VXLAN 简介、实现虚拟机跨物理机通信和网络隔离
网络·vxlan
乙酸氧铍3 个月前
【imx6ul 学习笔记】Docker 运行百问网 imx6ul_qemu
linux·docker·arm·qemu·imx6ul
fdtsaid3 个月前
Intel 六位专家对 Simics 助力 Shift-Left 的讨论(2018)
qemu·仿真·simulation·simics·intel simics
shandianchengzi4 个月前
【记录】ARM|Ubuntu 24 快速安装 arm-none-eabi-gdb 及 QEMU 调试实战
linux·arm开发·ubuntu·arm·qemu