Skip to content

Cilium

2 posts with the tag “Cilium”

在 K8s/K3s 中为特定 Pod 分配独立物理 IP (Multus + Macvlan + VLAN)

在构建 HomeLab 或企业内网时,我们经常会将服务部署在 K8s/K3s 集群中。默认情况下,集群内的出站流量会经过 CNI(如 Flannel 或 Cilium)的 SNAT 转换,源 IP 会变成宿主机的 Node IP。

但在某些场景下(例如部署 qBittorrent 等 P2P 下载工具、需要被独立监控的服务,或者需要绕过网关透明代理的流量),我们需要让 Pod 拥有一个真实的局域网独立 IP,并且可能还需要将其划分到**特定的 VLAN(如 VLAN 10)**中。

本文将介绍如何使用 Multus CNI 配合 Macvlan,为特定的 Pod “插上一根直通物理局域网的网线”,并配置 OPNsense 网关实现流量直连。

本方案完美兼容硬核 HomeLab 常用的 Cilium CNI + kube-vip 环境。各组件分工明确,互不冲突:

  • Cilium (主 CNI & Service LB):负责集群内 eBPF 流量管理与 Service LB IP 分配,为 Pod 提供基础的 eth0 网卡。
  • kube-vip:负责控制平面 API Server 的 VIP 漂移与高可用。
  • Multus + Macvlan (旁路直通):作为一个旁路插件,专门为指定的 Pod 塞入第二块物理网卡 net1

⚠️ 核心避坑:IP 地址池必须严格隔离! 必须确保 Macvlan 使用的静态 IP(本文以 10.0.10.101 为例),与 Cilium LB IPAM 池、kube-vip 的地址、以及 OPNsense 的 DHCP 池完全错开,避免产生 IP 冲突。


Multus 作为一个“元 CNI”,允许 Pod 挂载多块网卡。如果你的集群尚未安装,可以通过官方提供的 DaemonSet 一键安装(Thick 模式兼容性最好):

Terminal window
kubectl apply -f https://raw.githubusercontent.com/k8snetworkplumbingwg/multus-cni/master/deployments/multus-daemonset-thick.yml
kubectl set resources daemonset kube-multus-ds -n kube-system -c kube-multus --limits=memory=512Mi --requests=memory=100Mi

等待所有 kube-system 命名空间下的 multus Pod 处于 Running 状态即可。

步骤二:创建网络附件定义 (NetworkAttachmentDefinition)

Section titled “步骤二:创建网络附件定义 (NetworkAttachmentDefinition)”

我们需要创建一个 CRD 资源,告诉 K8s 如何划分这个直通的局域网。

创建一个名为 macvlan-network.yaml 的文件。注意:该资源必须与目标 Pod 处于同一个 Namespace。

💡 关于 VLAN 10 的特殊说明:

  • 情况 A(推荐 PVE 用户): 如果你在 PVE 虚拟机的硬件设置中,为该网卡配置了 VLAN Tag: 10,那么 K3s 系统内部的物理网卡(如 eth0ens18)已经是解包后的 VLAN 10 网络了。下方配置中的 master 直接填 eth0 即可。
  • 情况 B(Trunk 模式): 如果 PVE 传递的是 Trunk 口,你需要在 K3s 宿主机系统层面(如 Netplan 或 NetworkManager)先创建一个 VLAN 子接口 eth0.10,然后下方的 master 必须填入 eth0.10
macvlan-network.yaml
apiVersion: "k8s.cni.cncf.io/v1"
kind: NetworkAttachmentDefinition
metadata:
name: macvlan-direct
namespace: default # 必须替换为你的应用所在的 namespace
spec:
# ⚠️ 核心提醒:下方 config 内的字符串会被解析为纯 JSON,绝对不能包含 '#' 注释。
# 所有的关键备注已经为你提取到此处,请根据这些提示修改下方 JSON 对应的值:
#
# 1. master: 根据上方 VLAN 说明,替换为 eth0 或 eth0.10
# 2. subnet: 10.0.10.0/24 (VLAN 10 的真实网段)
# 3. rangeStart: 10.0.10.101 (为 Pod 分配的静态独立 IP)
# 4. rangeEnd: 10.0.10.101 (与 rangeStart 保持一致)
# 5. gateway: 10.0.10.1 (VLAN 10 的网关)
# 6. routes: 必须添加 0.0.0.0/0 的默认路由,否则无法访问外网 (Tracker 会报错)
config: |
{
"cniVersion": "0.3.1",
"type": "macvlan",
"master": "eth0",
"mode": "bridge",
"ipam": {
"type": "host-local",
"subnet": "10.0.10.0/24",
"rangeStart": "10.0.10.101",
"rangeEnd": "10.0.10.101",
"gateway": "10.0.10.1",
"routes": [
{ "dst": "0.0.0.0/0", "gw": "10.0.10.1" }
]
}
}

应用此配置:kubectl apply -f macvlan-network.yaml

步骤三:修改应用的 Deployment 配置

Section titled “步骤三:修改应用的 Deployment 配置”

在应用的 template.metadata.annotations 中声明使用刚刚创建的网络。同时,为了避免 DNS 流量黑洞,我们需要让该 Pod 绕过集群内部的 CoreDNS,直接使用公网 DNS。

以部署 qBittorrent 为例:

deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: qbittorrent
namespace: default
spec:
replicas: 1
selector:
matchLabels:
app: qbittorrent
template:
metadata:
labels:
app: qbittorrent
annotations:
# 绑定前面创建的 Macvlan 网络
k8s.v1.cni.cncf.io/networks: macvlan-direct
spec:
# 跳过 K8s CoreDNS,直接使用公网 DNS 解析 Tracker
dnsPolicy: "None"
dnsConfig:
nameservers:
- 223.5.5.5
- 114.114.114.114
# 必须固定节点,防止漂移导致物理网卡名称不匹配
nodeSelector:
kubernetes.io/hostname: k3s-worker-02
containers:
- name: qbittorrent
image: lscr.io/linuxserver/qbittorrent:latest
# ... 其他配置保持不变

重新部署后,进入 Pod 内部执行 ip a,你会发现它拥有了两张网卡:eth0 (集群内部 IP) 和 net1 (直连 VLAN 10 的 10.0.10.101)。

关键一步: 必须在应用的内部设置(如 qBittorrent 的高级设置 -> 网络接口)中,强制将监听网络接口绑定为 net1,确保流量不走 Cilium 的默认路由,而是真正从 Macvlan 发出。

步骤四:在 OPNsense 中设置 NAT 豁免 (Do not NAT)

Section titled “步骤四:在 OPNsense 中设置 NAT 豁免 (Do not NAT)”

当 qB 绑定 net1 向外发送数据时,流量会到达 OPNsense 网关。由于 OPNsense 默认会对出站流量进行 NAT 伪装,我们需要对这个特定的 IP 进行“豁免”,让最外层的设备(如 OpenClash)能看到它的真实源 IP。

  1. 进入设置菜单: 登录 OPNsense,导航至 防火墙 (Firewall) -> NAT -> 出站 (Outbound)
  2. 修改模式: 将出站 NAT 模式修改为 混合出站 NAT 规则生成 (Hybrid outbound NAT rule generation),点击保存。
  3. 添加豁免规则: 点击右上角的 + 号添加一条新规则。
  4. 关键配置项:
    • 不进行 NAT (Do not NAT): 必须勾选页面最上方这个复选框。
    • 接口 (Interface): 选择流量离开 OPNsense 去往上层网络(如光猫或旁路由)的接口,通常是 WAN
    • TCP/IP 版本: IPv4
    • 协议 (Protocol): any
    • 源地址 (Source address): 下拉选择 单主机或网络 (Single host or Network),并输入分配给 Pod 的独立 IP 10.0.10.101,掩码选择 /32
    • 转换/目标 (Translation / target): 保持默认(勾选了不进行 NAT 后,此项会失效)。
  5. 调整规则优先级: 保存规则后,回到列表页。将这条新创建的豁免规则拖动到所有规则的最顶部(确保它在常规的子网 NAT 规则之前生效),然后点击 应用更改 (Apply Changes)

步骤五:配置透明代理直连 (OpenClash)

Section titled “步骤五:配置透明代理直连 (OpenClash)”

完成 NAT 豁免后,外部网络(包括旁路代理)就能直接识别到 10.0.10.101 这个源 IP 了。

进入你的代理软件(如 OpenClash),在 访问控制 (Access Control) -> 不走代理的局域网设备 (Not Proxy LAN IPs) 中,将 10.0.10.101 加入列表。至此,你的 P2P 下载流量将完美绕过集群的网络虚拟化层和全局科学代理。

步骤六:访问方式变更与联动服务更新 (*arr)

Section titled “步骤六:访问方式变更与联动服务更新 (*arr)”

当你为 Pod 加上了默认物理路由并绑定了网卡后,它已经彻底变成了一台插在局域网上的“实体机”。这会带来两个必须处理的变更:

  1. 集群 Ingress / 域名访问失效: 由于流量现在是从集群的 eth0 进,却从直连的 net1 出,这会导致严重的“异步路由”(Asymmetric Routing),使得原有的 Nginx/Traefik Ingress 域名访问或 NodePort 访问不断转圈超时。
    • 解决方案: 放弃域名和 NodePort 转发,以后请直接在浏览器输入分配的独立 IP 与端口(如 http://10.0.10.101:8080)来访问后台 WebUI。
  2. 更新 Radarr / Sonarr 下载客户端: 如果你使用了 Radarr、Sonarr 或 Prowlarr 等自动化追剧工具,由于 qBittorrent 的访问入口已经改变,你必须前往这些工具的 设置 -> 下载客户端 (Download Clients) 中,将 qBittorrent 的主机地址更新为新的独立 IP(10.0.10.101)。如果不更新,这些联动组件将无法把种子推送到下载器中。

如果在配置过程中遇到 Pod 无法获取独立 IP 的情况,通常是由以下三个核心问题导致的:

如果你的 K3s 节点是运行在 Proxmox VE (PVE) 等虚拟化平台上的虚拟机,请务必注意以下两点限制:

  • 混杂模式与 MAC 地址过滤: 进入 PVE 的虚拟机设置,找到 硬件 (Hardware) -> 网络设备 (Network Device),双击打开编辑,关闭“防火墙 (Firewall)”选项。如果开启该选项,PVE 会拦截 Macvlan 自动生成的非原生 MAC 地址,导致 Pod 无法联网。
  • VLAN 隔离问题: 如果你在 Macvlan 层面使用的是子接口(如 eth0.10),请确保 PVE 分配给该虚拟机的网桥(Bridge)开启了 VLAN 感知 (VLAN aware),否则宿主机将会直接丢弃带有 VLAN Tag 的数据包。

2. Cilium 独占模式导致 Multus 失效

Section titled “2. Cilium 独占模式导致 Multus 失效”

如果你发现配置完全正确,但 Pod 依然只分配了 Cilium 的默认内部 IP,请登录宿主机检查 /etc/cni/net.d/ 目录。如果你看到 00-multus.conf 被系统自动重命名为了 00-multus.conf.cilium_bak,这说明 Cilium 开启了“独占模式”,强行禁用了 Multus。

解决办法: 关闭 Cilium 的独占模式并重启相关组件抢回控制权:

Terminal window
# 1. 修改 ConfigMap,关闭独占模式
kubectl patch configmap cilium-config -n kube-system --type merge -p '{"data":{"cni-exclusive":"false"}}'
# 2. 重启 Cilium 代理以应用新配置
kubectl rollout restart daemonset cilium -n kube-system
# 3. 等待 Cilium 启动后,重启 Multus 使其重新生成 00-multus.conf
kubectl rollout restart daemonset kube-multus-ds -n kube-system

3. K3s 节点缺失官方 CNI 基础插件

Section titled “3. K3s 节点缺失官方 CNI 基础插件”

K3s 是一个极致精简的轻量级发行版,特别是配合 Cilium 使用时,宿主机的 /opt/cni/bin 目录下可能并没有预装 macvlan 等基础官方插件。此时 Multus 虽能正常工作,但在为 Pod 创建网络沙盒时会抛出如下报错: failed to find plugin "macvlan" in path [/opt/cni/bin]

解决办法: SSH 登录到运行该 Pod 的 K3s 宿主机节点,手动下载并解压官方的标准 CNI 插件包:

Terminal window
# 确保目录存在
sudo mkdir -p /opt/cni/bin
# 下载官方 CNI 插件包 (此处以稳定的 v1.4.1 为例)
wget https://github.com/containernetworking/plugins/releases/download/v1.4.1/cni-plugins-linux-amd64-v1.4.1.tgz
# 将压缩包解压到 /opt/cni/bin 目录下
sudo tar -C /opt/cni/bin -xzf cni-plugins-linux-amd64-v1.4.1.tgz
# 重建失败的 Pod 即可正常挂载

K3s + Cilium 踩坑实录:Longhorn 插件无限重启?揪出 eBPF 路由脑裂的幕后黑手

大家好,我是赛博包工头。欢迎来到赛博工地。

在咱们上一期对 K3s 故障节点进行了“核弹级强拆”并重新加入集群后,本以为可以顺利盖起 Longhorn 存储大坝了。结果,Longhorn 的核心存储特派员 longhorn-csi-plugin 却一直在无限崩溃重启(CrashLoopBackOff)。

表面上看是存储插件坏了,但经过一顿抽丝剥茧,最后发现:这根本不是存储的锅,而是一起典型的网络大动脉断裂案!

今天这篇排错实录,我们就来复盘一下如何解决这种“查号台还活着,但去查号台的路被炸断了”的底层网络脑裂问题。


💥 案发现场:伪装成存储故障的网络崩塌

Section titled “💥 案发现场:伪装成存储故障的网络崩塌”

当时查看 longhorn-csi-plugin 的崩溃遗言(Logs),发现了极其关键的一行报错:

E0419 00:36:26.346354 1 main.go:167] "Error connecting to CSI driver" err="context deadline exceeded"
time="2026-04-19T00:36:30.790662906Z" level=warning msg="Failed to initialize Longhorn API client Get \"http://longhorn-backend:9500/v1\": dial tcp: lookup longhorn-backend on 10.61.0.10:53: read udp 10.60.2.244:53943->10.61.0.10:53: i/o timeout. Retrying"

日志翻译: Longhorn 插件启动时,需要找集群内部的“114查号台”(CoreDNS,IP 为 10.61.0.10:53)去查询总控中心的地址。结果请求发出去后,如同石沉大海,直接报了 i/o timeout(连接超时)。因为拿不到总控地址,插件干脆拒绝启动,最后被 Kubelet 当做死进程给毙了。


🕵️‍♂️ 抽丝剥茧:派出侦察兵探路

Section titled “🕵️‍♂️ 抽丝剥茧:派出侦察兵探路”

为了验证是不是真的连不上 DNS,我跑到后台看了一眼 CoreDNS 的状态,发现它分明是 1/1 Running,活得好好的,安安稳稳地跑在 k3s-master-02 节点上。

接着,我拉起了一个带 nslookup 工具的 busybox 侦察兵进行极限测试:

Terminal window
kubectl run -i --tty --rm debug-net --image=busybox --restart=Never -- nslookup kubernetes.default

返回的结果非常绝望:

;; connection timed out; no servers could be reached

结论:查号台活得好好的,但去找查号台的路,全断了。


🧠 底层逻辑:大动脉为什么会断?

Section titled “🧠 底层逻辑:大动脉为什么会断?”

这要归咎于咱们这套集群的高级架构。

在安装 K3s 时,我们加上了 --disable-kube-proxy 参数。这意味着 Kubernetes 传统的基于 iptables 的内部流量转发机制被废弃了。全集群的 Service IP(包括内部 DNS 地址 10.61.0.10),全靠 Cilium 的 eBPF 机制在 Linux 内核底层进行拦截和高效转发。

但是!咱们刚才对 k3s-master-03 执行了“核弹级强拆”,导致整个集群的 Cilium 路由映射表(BPF Maps)出现了严重的数据断层和脑裂。

Cilium 彻底迷失了方向,不知道该怎么把 UDP 53 端口的请求跨节点发给 k3s-master-02 上的 CoreDNS,于是网络包在内核层直接被无情丢弃。底层网络一断,上层依赖内部域名的 Longhorn 存储插件自然跟着全军覆没。


🛠️ 抢修方案:网络管线“大换血”

Section titled “🛠️ 抢修方案:网络管线“大换血””

既然病根在底层的路由表缓存,咱们不需要重装,只需要强制集群对网络管线进行一次滚动重启(Rollout Restart),让 eBPF 重新绘制整张全网路由表即可。

请依次执行这排雷三把斧

第一斧:强制重建 Cilium 网络映射表

Section titled “第一斧:强制重建 Cilium 网络映射表”

让所有节点的 Cilium 特派员重新扫描集群,重写内核里的路由规则:

Terminal window
kubectl rollout restart ds cilium -n kube-system

(敲完后喝口水等个半分钟,让 3 个节点的 Cilium Pod 挨个重启完成)

第二斧:顺手重启查号台 (CoreDNS)

Section titled “第二斧:顺手重启查号台 (CoreDNS)”

为了防止 CoreDNS 本身残留什么僵尸缓存,给它也来一记还我漂漂拳:

Terminal window
kubectl rollout restart deploy coredns -n kube-system

第三斧:再次派出侦察兵验证验收

Section titled “第三斧:再次派出侦察兵验证验收”

等上面的网络和 DNS Pod 都处于 Running 状态后,重新敲一遍之前的测试命令:

Terminal window
kubectl run -i --tty --rm debug-net --image=busybox --restart=Never -- nslookup kubernetes.default

💡 预期结果: 只要这次没有报 timed out,而是瞬间返回了类似 Server: 10.61.0.10Address: 10.61.0.10:53 的解析结果(即便带了 NXDOMAIN 的小 Bug 也不影响),就说明全集群的大动脉彻底打通了!

大动脉一通,之前卡在 CrashLoopBackOfflonghorn-csi-plugin 就会瞬间找回组织,自动停止报错并挂载成功。

包工头施工笔记: 排查 K8s 故障时,如果上层应用报“连接超时”,不要急着重装应用。拔出网络排错“侦察兵”测一下底层连通性,往往能事半功倍。“遇到 eBPF 脑裂,一键 Rollout 重启网络插件”,这是赛博工地里必须掌握的保命神技!