在 OpenWrt 上新建一个独立网段并绑定独立 WiFi,用源地址策略路由把整个网段的流量指向另一个网段里的 mihomo TUN 旁路由。客户端零配置,连上这个 SSID 就自动走代理,切回主网 WiFi 就恢复直连。文末附一个挺隐蔽的踩坑:ImmortalWRT 的 DNS 劫持。

旁路由本身怎么搭的,在上一篇里:用 Docker + macvlan 跑 mihomo 旁路由。

目标

家里已有一个 mihomo 容器当旁路由(TUN 模式),主网设备想走代理就把网关/DNS 指过去。但逐台改设备很烦,家人设备也不适合动手脚。理想状态是:

开一个专用 WiFi,连上就全走 mihomo,断开连回主网就直连。客户端什么都不用设。

拓扑长这样:

                ┌──────────────────────────────┐
                │  OpenWrt (ImmortalWRT)       │
   WiFi 主网 ── │  br-lan     10.0.0.1/24      │ ── WAN
   WiFi 代理 ── │  lan_hkt    192.168.88.1/24  │
                └──────────────┬───────────────┘
                               │ 10.0.0.0/24
                     ┌─────────┴─────────┐
                     │  N95 小主机        │
                     │  mihomo (macvlan) │
                     │  10.0.0.6  TUN    │
                     └───────────────────┘

mihomo 跑在 N95 上的 Docker 里,macvlan 拿到 10.0.0.6,是 10 段的地址。新 WiFi 绑的是 192.168.88.0/24。

为什么不能直接把网关设成 10.0.0.6

第一反应是给 88 段的 DHCP 下发网关 10.0.0.6,然后就会发现设备直接断网。原因很基础但容易忽略:网关必须和客户端同网段。客户端要用某个地址当网关,得先在本地二层 ARP 到它,而 10.0.0.6 在另一个广播域里,ARP 请求根本出不了本网段——网关不可达,DNS 也指着同一个地址,于是彻底没网。

两条出路:

  1. 给容器再加一条腿:把 88 网段用 VLAN 透传到 N95,再建一个 macvlan 网络,让 mihomo 同时拥有 192.168.88.6。最干净,但要动交换配置、宿主机和容器三处。
  2. OpenWrt 上做源地址策略路由:客户端网关照旧指 192.168.88.1,由 OpenWrt 把「源地址属于 88 段」的流量转发给 10.0.0.6。只改路由器一处,客户端和 mihomo 零改动。

旁路由的流量本来就要在 N95 的网线上进出各走一遍,方案二多绕的那一下对软路由来说可以忽略。选方案二。

一、建接口,绑 WiFi

/etc/config/network 新建接口:

config interface 'lan_hkt'
	option proto 'static'
	option ipaddr '192.168.88.1'
	option netmask '255.255.255.0'

/etc/config/wireless 加一个 wifi-iface,SSID 随意,关键是 network 绑到新接口:

config wifi-iface 'wifinet_hkt'
	option device 'radio1'
	option mode 'ap'
	option ssid 'MyWiFi-HK'
	option encryption 'sae-mixed'
	option key '你的密码'
	option network 'lan_hkt'

二、策略路由:整个网段指向 mihomo

/etc/config/network 末尾追加。思路是建一张 100 号路由表,规定「源地址是 88 段的包查这张表」:

config route
	option interface 'lan'
	option target '10.0.0.0/24'
	option table '100'

config route
	option interface 'lan_hkt'
	option target '192.168.88.0/24'
	option table '100'

config route
	option interface 'lan'
	option target '0.0.0.0/0'
	option gateway '10.0.0.6'
	option table '100'

config rule
	option src '192.168.88.0/24'
	option lookup '100'
	option priority '100'

前两条直连路由让 88 段能正常到达 10 段(查 DNS、访问 mihomo 面板)和本网段,不被误送进代理。第三条是核心:默认路由指向 10.0.0.6,所有上网流量交给 mihomo。config rule 限定这张表只对 88 段的源地址生效——主网完全不受影响。

三、DHCP:DNS 指向 mihomo,并且关掉 IPv6

/etc/config/dhcp:

config dhcp 'lan_hkt'
	option interface 'lan_hkt'
	option start '100'
	option limit '150'
	option leasetime '12h'
	list dhcp_option '6,10.0.0.6'
	option ra 'disabled'
	option dhcpv6 'disabled'

dhcp_option '6,...' 让客户端 DNS 直指 mihomo,配合它的 fake-ip 模式,域名信息才能进到代理内核里做分流。

IPv6 必须关:家里宽带有公网 IPv6 的话,客户端一旦拿到 v6 地址就会优先走 v6 直连出去,整套代理形同虚设。这个网段只跑 IPv4。顺带确认 lan_hkt 接口上没有 option ip6assign,有就删掉。

四、防火墙:同区放行,关掉 masquerade

把新接口加进 lan 区域(/etc/config/firewall):

config zone
	option name 'lan'
	list network 'lan'
	list network 'lan_hkt'
	...

区域内转发默认放行,88 段的包才能被转发到 10.0.0.6,回程包也才能转发回来。

另外检查 lan 区域没有开 option masq(LuCI 里叫「IP 动态伪装」)。开着的话 OpenWrt 会把转发流量的源地址改写成自己,mihomo 日志里所有设备都变成 10.0.0.1——现在不影响通断,但以后想按源 IP/源网段分流就全废了。这套设计靠纯路由回程,不需要 NAT。

全部改完:

/etc/init.d/network reload
/etc/init.d/dnsmasq restart
/etc/init.d/odhcpd restart
/etc/init.d/firewall reload

踩坑:ImmortalWRT 的 DNSMASQ HIJACK

到这里理论上就完事了,实际连上新 WiFi 后:能上网,但被墙的站全打不开。mihomo 日志里全是这种:

[TCP] dial Other (match Match/) 10.0.0.1:58272 --> 185.45.5.35:443 error: i/o timeout
[UDP] 10.0.0.1:57830 --> 157.240.7.20:443 match Match using Other[DIRECT]

目标全是真实 IP 而不是 198.18.x 的 fake-ip——说明域名解析根本没经过 mihomo,它收到的只有裸 IP,域名规则全匹配不上,落到兜底 DIRECT 直连被墙 IP,超时。

(顺带解释一下日志里的源地址:这两条是在关掉 lan 区域的 masquerade 之前抓的,所以 88 段客户端的源 IP 被路由器改写成了自己的 10.0.0.1。按第四节关掉之后,这里显示的就是 192.168.88.x 的真实客户端 IP 了。)

诡异的是客户端 nslookup facebook.com 明明显示 DNS 服务器是 10.0.0.6,返回的却是 185.45.5.35 这种典型的 GFW 污染 IP。而从 10 段设备(和 mihomo 同二层)直接查 10.0.0.6,返回的是正常的 fake-ip。

差别只有一个:88 段的 DNS 包要经过路由器转发。在路由器上翻 nftables:

nft list ruleset | grep -B2 'dport 53'
chain prerouting {
    type nat hook prerouting priority dstnat + 5; policy accept;
    meta nfproto { ipv4, ipv6 } udp dport 53 counter redirect to :53 comment "DNSMASQ HIJACK"

真凶。这是 ImmortalWRT 的「DNS 重定向」功能:所有经过路由器转发的 53 端口查询统统抢答给本机 dnsmasq。88 段查 10.0.0.6 必经转发,于是被截胡,dnsmasq 拿明文上游解析出污染 IP,而 conntrack 会把回包的源地址改回 10.0.0.6——所以客户端完全看不出被劫持。

关掉:

uci set dhcp.@dnsmasq[0].dns_redirect='0'
uci commit dhcp
/etc/init.d/dnsmasq restart
nft list ruleset | grep HIJACK   # 应无输出

LuCI 位置:网络 → DHCP/DNS → 取消勾选「DNS 重定向」。

不用担心失去它「矫正硬编码 DNS 设备」的作用:凡是被策略路由送进 mihomo 的流量,mihomo TUN 配置里的 dns-hijack: any:53 会在那头截获硬编码的 8.8.8.8 查询并返回 fake-ip,位置比路由器截获更合理。主网设备的 DNS 是发给路由器本机的入站包,不走这条转发劫持规则,关掉零影响。

顺带一提,上一篇里按 MAC 打 tag 的做法之所以不会碰到这个坑,是因为那些设备和 mihomo 在同一个网段,DNS 查询二层直达,压根不经过路由器转发。

验证

# 路由器上
ip rule                        # 应有 from 192.168.88.0/24 lookup 100
ip route show table 100        # 应有三条路由

# 88 段客户端
nslookup facebook.com          # 服务器 10.0.0.6,返回 198.18.x.x
curl ip.sb                     # 显示代理出口 IP

mihomo 日志里此时应该是按域名匹配的记录,而不是裸 IP 落 Match[DIRECT]。

延伸:一个 SSID 一个国家

这套结构天然可扩展。每个出口地区建一个「接口 + WiFi + config rule」三件套,全部指向同一个 mihomo;因为 OpenWrt 只做路由不做 NAT,客户端源 IP 原样保留,mihomo 里用 SRC-IP-CIDR 按源网段选不同的节点组:

rules:
  - SRC-IP-CIDR,192.168.88.0/24,香港节点
  - SRC-IP-CIDR,192.168.89.0/24,美国节点

想换出口,切个 WiFi 就行。