[All In One] HomeLab 虚拟化 iKuai + Mihomo

8 mins to read

本文思路基本遵循 iKuai与Openwrt的有机结合:传统旁路由方案的完美替代,主要差异点如下:

  • 虚拟化平台: unRaid 替换为 Proxmox VE (PVE)
  • 透明网关: OpenWRT 替换为 Debian + Mihomo
  • DNS 服务: paopaodns 替换为 SmartDNS + AdGuard Home 组合

网络拓扑

为方便描述,下文统一将处理出站流量的虚拟机称为透明网关 (gateway)

架构核心思想

本方案采用“主路由 + 透明网关”的模式。iKuai 作为主路由,以其出色的稳定性、多线负载和流控能力负责整体网络的 DHCP、NAT 和流量调度。透明网关则是一个专职处理特定流量(如国际流量)的“处理器”。iKuai 通过策略路由(PBR)将特定流量“扔”给透明网关处理,处理完毕后再由透明网关“扔”回给 iKuai,由 iKuai 统一出口。这种架构实现了功能解耦,稳定性和灵活性兼得。

  • iKuai (主路由):

    • WAN1: 用于 PPPoE 拨号或从光猫 DHCP 连接到外网。
      • 物理网卡:eth3 (硬件直通)
    • LAN: 地址为 192.168.100.1,桥接所有局域网内的网卡,构成统一的二层网络。
      • eth1, eth2: 硬件直通,连接 AP 或其他有线设备。
      • vmbr0: PVE 虚拟网桥,专用于 PVE 的 Web 管理界面访问,桥接到 LAN 使其可从内网访问。
      • vmbr2: PVE 虚拟网桥,作为通用网桥,接入其他虚拟机或容器。
    • WAN2: 逻辑接口,作为 iKuai 与透明网关之间的数据通道。
      • IP 地址: 192.168.200.2
      • 网关指向: 192.168.200.1 (透明网关的 LAN 地址)
      • 网卡:vmbr1 (PVE 虚拟网桥)
  • gateway (透明网关):

    • LAN: 接收来自 iKuai (WAN2) 的流量。
      • IP 地址: 192.168.200.1
      • 网卡:vmbr1 (连接 iKuai WAN2)
    • WAN: 将处理后的流量发回 iKuai 的 LAN。
      • IP 地址: 192.168.100.2
      • 网关指向: 192.168.100.1 (iKuai 的 LAN 地址)
      • 网卡:vmbr2 (连接 iKuai LAN)

网络拓扑图

PVE 网络设置

pve-net

iKuai 安装与初始化

虚拟机配置

在 PVE 中为 iKuai 创建虚拟机时,以下是一些关键配置建议:

  • 机型: q35。提供更现代的硬件特性,是使用 PCIe Passthrough (硬件直通) 的推荐选项。
  • BIOS: 默认 (SeaBIOS)。对 iKuai 兼容性良好。
  • SCSI 控制器: VirtIO SCSI single。使用半虚拟化驱动 VirtIO 可最大化磁盘 I/O 性能。

安装步骤

  1. 从 iKuai 官网下载 32 位 ISO 镜像并上传到 PVE。

  2. 新建虚拟机,可以暂时不添加网络设备,稍后统一配置。

  3. 硬件直通 (PCI Passthrough): 进入 iKuai 虚拟机的“硬件”菜单,将物理网卡(除 PVE 管理口外)直通给虚拟机。

    什么是硬件直通 (PCI Passthrough)?

    它允许虚拟机绕过 Hypervisor (PVE) 直接控制物理硬件设备。对于网卡而言,这意味着虚拟机可以获得接近物理机的网络性能和延迟,并能直接利用网卡的硬件特性,是追求极致性能的软路由方案首选。

  4. 添加虚拟网卡: 添加 vmbr0, vmbr1, vmbr2 这三个 PVE 虚拟网桥作为虚拟网卡。

  5. 启动虚拟机,iKuai 系统将自动安装。完成后关闭虚拟机,在“选项”中调整引导顺序至硬盘,并移除 ISO 镜像。

  6. 重启虚拟机,通过 PVE 控制台初始化 iKuai。根据 PVE 硬件信息中显示的 MAC 地址,将网卡与 iKuai 的接口(如 eth0, eth1)一一对应绑定,并设置 LAN 口 IP。

系统内设置

使用 192.168.100.1 访问 iKuai Web 管理界面,完成配置。

  • WAN1: 配置为 PPPoE 拨号或 DHCP,作为主外网出口。
  • WAN2: 配置为静态 IP,绑定 vmbr1 对应的虚拟网卡。
    • IP 地址:192.168.200.2
    • 网关:192.168.200.1
    • 关键配置: 将此线路设置为 默认线路。此举的目的是让所有流量在默认情况下都经过透明网关,再通过后续的分流规则将国内流量“豁免”。
    • 开启 掉线切换,并设置线路检测,确保透明网关异常时网络可用性。
  • LAN1:
    • 在“高级设置”中,启用 链路桥接,将所有内网接口(物理和虚拟)桥接在一起,创建一个统一的广播域,确保所有设备都在同一局域网内。

wan2

配置分流

  1. 进入 流控分流 > 多线负载,添加自定义运营商。将 china_ip_list 的 IP 段复制进来。由于 iKuai UI 存在输入长度限制,需要将 IP 列表分批添加。
  2. 添加负载均衡规则,将目标地址为国内 IP 的流量,强制走 WAN1 出口,实现国内流量直连。

分流

添加端口分流规则防止回环

什么是路由回环 (Routing Loop)?

在本架构中,一个数据包的路径可能是:客户端 -> iKuai -> 透明网关 -> iKuai -> 互联网。当数据包从透明网关返回 iKuai 时,如果 iKuai 依然按照默认路由(即 WAN2)处理,就会把这个包再次发给透明网关,形成 iKuai -> 网关 -> iKuai 的死循环,导致网络中断。

为防止回环,需添加一条规则:将源地址为透明网关 IP (192.168.100.2) 的所有流量,强制从 WAN1 出口发出。

防止回环

透明网关

推荐使用 LXC 容器而非完整虚拟机,因为它更轻量,资源开销小,网络性能接近宿主机。我选择 PVE 官方 CT 模板的 debian 12 作为 mihomo 和 dns 服务的运行时。

新建 LXC 容器

LXC 配置文件 (/etc/pve/lxc/111.conf) 示例:

arch: amd64
cores: 2
features: nesting=1
hostname: gateway
memory: 2048
net0: name=wan0,bridge=vmbr2,gw=192.168.100.1,hwaddr=BC:24:11:B8:D5:64,ip=192.168.100.2/24,type=veth
net1: name=lan0,bridge=vmbr1,hwaddr=BC:24:11:AF:9E:03,ip=192.168.200.1/24,type=veth
ostype: debian
rootfs: local:111/vm-111-disk-0.raw,size=8G
swap: 2048
unprivileged: 1
nameserver: 223.5.5.5

网络配置

编辑 LXC 容器内的 /etc/network/interfaces,为 lan0(接收 iKuai 流量)和 wan0(发回 iKuai)配置静态 IP。

auto lo
iface lo inet loopback

auto lan0
iface lan0 inet static
        address 192.168.200.1/24
        gateway 192.168.200.2

auto wan0
iface wan0 inet static
        address 192.168.100.2/24

Mihomo

安装与配置

  1. 从 Github Releases 下载二进制文件并放置到 /usr/local/bin/
  2. 创建配置目录 /etc/mihomo
  3. 编写核心配置文件 /etc/mihomo/config.yaml。与本方案相关的关键配置如下:
# ... (其他配置)
tproxy-port: 7894 # TProxy 监听端口,需与 nftables 脚本一致
routing-mark: 6666 # Mihomo 自身发出流量的标记,用于在 nftables 中豁免,防止代理自身

dns:
  enable: true
  listen: 0.0.0.0:1053 # DNS 服务监听端口
# ... (其他配置)

服务化 (Systemd)

创建 /etc/systemd/system/mihomo.service 文件。

Systemd Capabilities (权能) 说明

CapabilityBoundingSetAmbientCapabilities 是 Linux 的安全特性,用于授予进程精确的权限,而非给予完整的 root 权限。

  • CAP_NET_ADMIN: 允许程序修改网络接口、路由表、防火墙规则等。TProxy 设置需要此权限。
  • CAP_NET_RAW: 允许程序使用 RAW 和 PACKET 套接字。
  • CAP_NET_BIND_SERVICE: 允许程序绑定到 1024 以下的特权端口。

这种配置方式比直接使用 User=root 更安全。

[Unit]
Description=Mihomo Daemon, a Clash API compatible core.
After=network.target

[Service]
Type=simple
LimitNPROC=500
LimitNOFILE=1000000
CapabilityBoundingSet=CAP_NET_ADMIN CAP_NET_RAW CAP_NET_BIND_SERVICE
AmbientCapabilities=CAP_NET_ADMIN CAP_NET_RAW CAP_NET_BIND_SERVICE
Restart=always
ExecStartPre=/usr/bin/sleep 1s
ExecStart=/usr/local/bin/mihomo -d /etc/mihomo
ExecReload=/bin/kill -HUP $MAINPID

[Install]
WantedBy=multi-user.target

TProxy 透明代理

什么是 TProxy?

TProxy (Transparent Proxy) 是 Linux 内核的一项功能,它允许一个应用程序(如 Mihomo)在不修改数据包目的 IP 地址和端口的情况下,接收并处理本应发往其他地址的数据包。相比传统的 REDIRECT (DNAT) 模式,TProxy 能让服务端程序看到真实的客户端源 IP 地址,且对 UDP 的支持更完善,性能也更优。

步骤一:开启 IP 转发

tee /etc/sysctl.conf <<-'EOF'
net.ipv4.ip_forward = 1
net.ipv6.conf.all.forwarding = 1
EOF

步骤二:配置防火墙与策略路由

启动脚本: /etc/mihomo/ip-route-poststart.sh

#!/bin/sh
# 1. 在策略路由表 100 中,添加一条默认路由,所有流量从 lan0 发出
ip route add local default dev lan0 table 100
# 2. 添加一条规则:为防火墙标记(fwmark)为 1 的数据包,应用策略路由表 100
ip rule add fwmark 1 lookup 100
# 3. 加载 nftables 防火墙规则集
nft -f /etc/mihomo/nftrules.nft

停止脚本: /etc/mihomo/ip-route-stop.sh

#!/bin/sh
# 按相反顺序清理规则
nft delete table ip clash
ip route del local default dev lan0 table 100
ip rule del fwmark 1 lookup 100

nftables 规则集: /etc/mihomo/nftrules.nft

#!/usr/sbin/nft -f

## 只处理指定网卡的流量,要和ip规则中的接口操持一致
define interface = lan0

## clash的透明代理端口
define tproxy_port = 7894

## clash打的标记(routing-mark)
define clash_mark = 6666

## 常规流量标记,ip rule中加的标记,要和ip规则中保持一致,对应 "ip rule add fwmark 1 lookup 100" 中的 "1"
define default_mark = 1

## 本机运行了服务并且需要在公网上访问的tcp端口(本机开放在公网上的端口),仅本地局域网访问的服务端口可不用在此变量中,以半角逗号分隔
define local_tcp_port = {
    22,      # ssh,按需设置
    8000-9999  # http
}

## 要绕过的局域网内tcp流量经由本机访问的目标端口,也就是允许局域网内其他主机主动设置DNS服务器为其他服务器,而非旁路由
define lan_2_dport_tcp = {
    53, 5353, 5354     # dns查询
}

## 要绕过的局域网内udp流量经由本机访问的目标端口,也就是允许局域网内其他主机主动设置DNS服务器为其他服务器,而非旁路由;另外也允许局域网内其他主机访问远程的NTP
服务器
define lan_2_dport_udp = {
    53, 5353, 5354,    # dns查询
    123    # ntp端口
}

## 保留ip地址
define private_address = {
    127.0.0.0/8,
    100.64.0.0/10,
    169.254.0.0/16,
    224.0.0.0/4,
    240.0.0.0/4,
    10.0.0.0/8,
    172.16.0.0/12,
    192.168.0.0/16
}

table ip clash {
    ## 保留ipv4集合
    set private_address_set {
        type ipv4_addr
        flags interval
        elements = $private_address
    }

    ## prerouting链
    chain prerouting {
        type filter hook prerouting priority filter; policy accept;
        ip protocol { tcp, udp } socket transparent 1 meta mark set $default_mark accept # 绕过已经建立的连接
        meta mark $default_mark goto clash_tproxy                                        # 已经打上default_mark标记的属于本机流量转过来的,直接进入透明代理
        fib daddr type { local, broadcast, anycast, multicast } accept                   # 绕过本地、单播、组播、多播地址
        tcp dport $lan_2_dport_tcp accept                                                # 绕过经由本机到目标端口的tcp流量
        udp dport $lan_2_dport_udp accept                                                # 绕过经由本地到目标端口的udp流量
        ip daddr @private_address_set accept                                             # 绕过目标地址为保留ip的地址
        goto clash_tproxy                                                                # 其他流量透明代理到clash
    }

    ## 透明代理
    chain clash_tproxy {
        ip protocol { tcp, udp } tproxy to :$tproxy_port meta mark set $default_mark
    }

    ## output链
    chain output {
        type route hook output priority filter; policy accept;
        oifname != $interface accept                                   # 绕过本机内部通信的流量(接口lo)
        meta mark $clash_mark accept                                   # 绕过本机clash发出的流量
        fib daddr type { local, broadcast, anycast, multicast } accept # 绕过本地、单播、组播、多播地址
        udp dport { 53, 5353, 123 } accept                             # 绕过本机dns查询、NTP流量
        tcp sport $local_tcp_port accept                               # 绕过本地运行了服务的tcp端口,如果并不需要从公网访问这些端口,可以注释掉本行
        ip daddr @private_address_set accept                           # 绕过目标地址为保留ip的地址
        ip protocol { tcp, udp } meta mark set $default_mark           # 其他流量重路由到prerouting
    }
}

步骤三:集成到 systemd 服务

/etc/systemd/system/mihomo.service 加入声明周期脚本,令 systemd 维护 mihomo 的时候自动执行:

ExecStartPost=/etc/mihomo/ip-route-poststart.sh
ExecStop=/etc/mihomo/ip-route-stop.sh

DNS

DNS 查询链

本方案构建了一个“责任链模式”的 DNS 解析流程:客户端 -> AdGuard Home -> SmartDNS -> 最终上游 DNS。每个环节只做自己的专职工作:AdGuard Home 负责过滤广告和黑名单,SmartDNS 负责对国内外域名进行智能、快速的解析。这种分层设计使得结构清晰,易于排错和维护。

  1. 安装 SmartDNS: apt install smartdns。配置好国内外两组上游 DNS 服务器。为获得最佳性能,禁用测速 (speed-check-mode none) 并开启缓存持久化。
  2. 安装 AdGuard Home: 在 AdGuard Home 的上游 DNS 设置中,仅填入 SmartDNS 的监听地址(如 127.0.0.1:5353)。关闭 AdGuard Home 的查询缓存,避免与 SmartDNS 重复缓存。
  3. 整合: 在 iKuai 的 DHCP 服务 -> LAN 口设置 中,将主 DNS 服务器地址修改为透明网关的地址:192.168.200.1。这样所有内网设备获取到的 DNS 服务器就是 AdGuard Home。

dns

自动更新配置

为了实现低维护运行,各类配置应尽可能自动化更新。

  • iKuai 大陆 IP 分流: 手动更新即可,IP 列表变动不频繁。
  • Mihomo 订阅配置: 推荐部署私有 sub-store 用于聚合。在透明网关中设置 cron 定时任务,定期 wget 新的配置文件并 systemctl restart mihomo
  • SmartDNS 国内域名列表: 定时任务从 Olixn/china_list_for_smartdns 等项目下载更新列表并重启 SmartDNS。
  • AdGuard Home 过滤规则: 直接在 AdGuard Home 的 Web UI 中订阅优秀的过滤列表即可自动更新。
  • AdGuard Home 过滤规则自定义规则同步: TODO。

参考资料