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 一样:
- 管理员创建
IPPoolCRD,定义 CIDR 范围(Neon 用10.100.0.0/16) - 每个 VM 创建时,从池中分配一个唯一的 overlay IP
- 分配记录写入
Allocations,保证不冲突 - 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.goL65-79calcIPs()函数从 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.goL92-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.goL122-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.goL157-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.goL221-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 却成功),通常是因为:
- FORWARD 链默认 DROP (Docker 环境)→ 需要 Step 4c 的
FORWARD -i br-def -j ACCEPT规则- IP 不在同一子网 → 确保 BRIDGE_IP 和 VM_IP 在同一个 /30 内
- 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.goL71-79:创建 bridge + VXLANvxlan-controller/cmd/main.goL83-101:循环 30 秒更新 FDBvxlan-controller/cmd/main.goL150-191:createVxlanInterface()函数vxlan-controller/cmd/main.goL193-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)