跳至主要内容

OpenWrt + Clash Tun + Policy Based Routing 网关代理实践


一、前言

在运行 OpenWrt 的路由器上跑 Clash 网关代理有许多现成的方案,但是因为一些原因,那些方案我都不喜欢。

在 Clash v1.8.0 版本更新之前,我将就用着缝合各家方案的 Shell 脚本来运行 Clash,虽然不是特别稳定,但日子还算勉强过得去。不幸的是,Clash v1.8.0 版本之后开发者去除了 redir-host 模式的远端 DNS 解析,我在并未仔细研究更新日志的情况下例行升级了 Clash Premium Core,从此我的网关代理就没稳定过……终端翻墙的日子持续了好几个月,在这期间我尝试过各种办法,但终究不够稳定,甚至我找回旧版本,想用回原来较稳定的方案都玄学地失败了。

前些日子(1 月中旬的样子)我在例行翻阅 Clash 项目的 Issue 和 Discussion 时,偶然发现一位朋友提出了我从未见过的方案,如下图:

来源位置:Issue#1358

我当时立马就在 OpenWrt 上安装了 VPN Policy Routing 这个组件,但是可能因为心情过于浮躁,不得其门而入,只能遗憾放弃,继续苦哈哈地研究 Shell 脚本启动和 iptables。——实际上还是因为我太菜了,才会被这一点点问题搞得身心俱疲。

前几天我再次尝试这个 VPN Policy Routing 方案时,不知怎么福至心灵,突然就找到了窍门——当然其实逻辑非常简单,各位看官大手子们一定一看到界面就知道该怎么弄了。在“简单”研究了两天之后,我认定这个方案还算稳定,故写出来以作记录,或许能稍微造福到后来人(如果有人看的话嘤嘤嘤)。

二、环境

1. 全新安装的 OpenWrt 21.02.1 x86_64 官方预编译版本
2. Clash Premium Core 2022.01.27 版本(dns: fake-ip)
3. Policy Based Routing 0.9.4-10 版本(pbr 和 luci-app-pbr 包)

其他环境不保证成功,OpenWrt 各种固件差别很大,基于 Linux 让它的环境很容易受到各种预装软件/组件/插件的影响(例如某些固件默认劫持 53 端口的 TCP/UDP 流量到路由器)。

关于第三点为什么是 Policy Based Routing 而不是前文提到的 VPN Policy Routing,那是因为后者已经停止开发,而由同一个开发者开发的前者是后者的继任者,二者的 LuCI 完全一致,功能上基本相同,甚至配置文件可以通用。

它的作用正如它的名字,即按照策略将流量路由到不同的接口,我们只要为 Clash Tun 创建一个网络接口,就可以按照自行设定的策略来将需要的流量路由到 Clash Tun 中——对于我这种完全搞不明白计算机网络和 iptables 的辣鸡来说用处还是很大的。

三、过程

3.1 添加 PBR 开发者的仓库并安装部分组件和依赖

由于 PBR 尚未被添加到 OpenWrt 官方仓库,所以需要添加开发者的个人仓库。(Tips:如果在意这一点完全可以安装官方仓库中的 vpn-policy-routing,二者几乎没有差别。)

添加开发者的仓库可以参照开发者的文档,以下是我自己的用法:
通过 SSH 登录到 OpenWrt,在终端中输入以下代码添加 PBR 仓库:
echo -e -n 'untrusted comment: OpenWrt usign key of Stan Grishin\nRWR//HUXxMwMVnx7fESOKO7x8XoW4/dRidJPjt91hAAU2L59mYvHy0Fa\n' > /etc/opkg/keys/7ffc7517c4cc0c56
echo 'src/gz stangri_repo https://repo.openwrt.melmac.net' >> /etc/opkg/customfeeds.conf
上述仓库托管在 Github,假如你所在的网络环境很差,可以考虑上述开发者文档中提供的jsDelivr 版本。
随后更新 opkg 数据,并安装 pbr 和部分依赖
opkg update
opkg install pbr luci-app-pbr kmod-tun curl

假如你所在的网络环境很差,可以考虑更换为国内的 opkg 仓库源,清华镜像站的更换帮助

可能有人想知道这行命令都安装了什么:

(点开应该可以看到大图)

其中 libcurl、libwolfssl 和 libnghttp2 是 curl 引入的依赖。

安装过程中 pbr 有两行报错,暂且不用管它,先进入下一步。

3.2 启动 Clash Tun 并手动创建接口

将 Clash 二进制文件、配置文件和 mmdb 文件放到 /etc/clash 目录下,并给 Clash 二进制文件添加执行权限。
配置文件暂时不需要完整的,但至少需要写好 dns 和 tun 的部分,我贴一份初始示例:
mixed-port: 1080
mode: rule
log-level: info
dns:
  enable: true
  listen: 0.0.0.0:1053
  enhanced-mode: fake-ip
  nameserver:
    - tls://dot.pub:853
tun:
  enable: true
  stack: gvisor
示例使用 fake-ip 模式,redir-host 由于 dns 行为变更,需要自行研究。
然后在 OpenWrt 终端启动 Clash Tun:
/etc/clash/clash -d /etc/clash
这时候可以看到 Clash 初始化和 utun 的创建,如图所示。先放到一边。

浏览器登陆 OpenWrt LuCI 管理界面,导航到 Network -> Interfaces,然后添加一个接口,名字比如叫 Clash,Protocol 选 Static address,Device 就选 Clash 创建的 utun,创建接口。接口设置,IPv4 address 填 198.18.0.1,netmask 选 255.255.0.0,防火墙区域选择 lan(或者新建区域并设置好转发),保存并应用。

之后导航到 Services -> Policy Based Routing,Enable 之后更改中间两项如图,保存并应用。



第二项的意思是当目标接口不可用时是否继续按策略路由流量,我们选择否,那么在 Clash Tun 掉了(指 Clash 进程掉了、utun 掉了)之后,PBR 会将该策略的目标接口改成 WAN。
第三项要求安装 dnsmasq-full,否则启用则报错,在本文的应用场景基本用不到这个功能,故关闭。

注意,你应该按照自己的需求事先创建好 WAN/LAN,如果此时在 Policies 的下拉框中看不到 WAN/LAN,则需要在 Advanced Configuration 中的 Supported Interfaces 上自行添加接口(此处填写接口应该是大小写敏感的,但 OpenWrt 中经常按全大写显示,需要留意)。

当然,没有 WAN/LAN 的选项也可以选用 IGNORE,该策略会走系统默认的流量走向。

然后删除掉默认的三条示例规则,点击 Start,界面应该如下所示:


我们尝试添加一条策略,将所有流量路由到 Clash Tun。
名字就叫 GoClash,前面几项留空,在 Remote ports,填入 0-65535,Protocol 选 Auto,Chain 选 PREROUTING,Interface 就选我们创建的 Clash 接口:


然后保存并应用,这个时候瞄一眼之前运行 Clash Tun 的终端窗口:


没错,如果你用了我给的 Clash 配置初始示例,不包含节点的,那么你家网炸啦!

这时候在终端窗口按下 Ctrl+C,结束 Clash 进程,这个时候 Clash 创建的 utun 设备会消失,手动创建的 Clash 接口随之下线,按照刚才的设定,接口下线时 PBR 不强制按策略路由——所以刷新一下浏览器,可以看到我们创建的 GoClash 策略的目标接口已经变成了 WAN/LAN/IGNORE,反正不会再是 Clash,这时候网络又恢复正常了。

为了不危害家里的网络,我们需要限制一下要走 Clash Tun 的设备。以只允许当前使用的电脑被路由为例,我们需要在 GoClash 策略上的的 Local addresses / devices 一栏填上当前电脑的 IP,比如:


保存应用之后,再次运行 Clash Tun,可以看到策略的目标接口又变回了 CLASH。

注意注意注意!这时需要点击一次 Restart,否则流量不会被路由到 Clash Tun。

——这是一个 bug,Clash 创建的 utun 设备丢失之后,PBR 为它创建的路由也随之消失,并且在 Clash 接口重新上线时没有添加回来。我已经向开发者提交了 Issue,但不知道会不会/什么时候修好。
——我们自己解决有一个简单的办法,在后续创建 Clash 的 init.d 脚本时,启动 Clash 之后加一行重启 PBR 的命令就好——这个处理得有一点点粗糙,大佬们可以提示一些精致的办法。

3.4 处理 DNS 问题

Restart 之后,你的电脑的外网流量应该被路由到了 Clash Tun 上,要想 Clash Tun 网关正常工作,接下来还要处理 DNS 的问题。
DNS 方案大家可以各显神通,我只提供我的办法,仅使用 OpenWrt 内建的 dnsmasq DNS 以作抛砖引玉

1.首先要确认你其他设备 DNS 已经指向 OpenWrt 路由器,确认你所用的 OpenWrt 固件没有在防火墙用户规则里劫持 53 端口的流量;

2. 其次,无论是从上游自动获取也好,手动设定也好,至少要保证 WAN/LAN 接口上有 DNS,并关闭固件内其他所有的 DNS 方案,保证是 dnsmasq 在监听 53 端口;

3. 然后导航到 Network -> DHCP and DNS -> General Settings,在 DNS Forwardings 填上 Clash 的 DNS,比如按前文配置的 127.0.0.1#1053,用井号代替冒号;

4. 在终端(另开一个终端)使用 nslookup 查询任意域名,如果得到 198.18.0.x 的结果,这时候 DNS 就已经配置成功了,(如果之前使用我的示例配置来测试的)可以将配置换成你自己的配置,改一改 dns 和 tun 部分就能正常按 Clash 规则代理流量了。

使用 dnsmasq 的一点优点,在于它的 DNS 有 fallback(按照我的粗浅理解和实际体验),即当 Clash 进程被杀了、爆炸了,127.0.0.1#1053 获取不到 DNS 结果的时候,dnsmasq 会向在 WAN/LAN 配置的 DNS 查询结果,加上 PBR 会将路由改回 WAN/LAN,这时候家里的网络并不会跟随 Clash 一起爆炸(由于终端设备 DNS 缓存的问题,可能要多刷新几次)。

缺点在于,有时候 dnsmasq 会小概率抽风,跳过 Clash 的 DNS 而向 WAN/LAN 配置的 DNS 查询域名结果,就会没法正常代理。

其他的 DNS 方案,只要能将 DNS 查询路径最终指向 Clash 即可。

——将全部的 DNS 查询指向 Clash 会有一些额外的问题,我们后文再讨论。

 3.5 创建 Clash 的控制脚本、开机启动

同样只是抛砖引玉,我给出一个简单的脚本:
#!/bin/sh /etc/rc.common
# /etc/init.d/clash

USE_PROCD=1
START=99
STOP=15

# CLASH_BIN=/etc/clash/clash
# CONFIG_PATH=/etc/clash
# CONFIG=/etc/clash/config.yaml

LOG=/tmp/clash.log

start_service()
{
	rm $LOG
	touch $LOG
	procd_open_instance
	procd_set_param command /bin/sh -c "/etc/clash/clash -d /etc/clash >> /tmp/clash.log"
	procd_set_param stderr 1
	procd_close_instance
	restart_pbr
}

restart_pbr()
{
	/etc/init.d/pbr restart
}
其中 restart_pbr() 是为了解决前文 3.3 部分末尾提到的 pbr bug,各位大佬如果有更好的方案可以提出来参考借鉴一下😆。
将上述脚本命名为 clash,放置到 /etc/init.d/ 目录下,并给予执行权限,然后在终端窗口运行 /etc/init.d/clash enable,Clash Tun 就配置好开机启动并可以由以下命令控制:
/etc/init.d/clash start
/etc/init.d/clash stop
/etc/init.d/clash restart
然后遇到问题就可以尝试重启 Clash 或重启 PBR,如果还不行——不要找我!我不会。

3.6 终端设备的访问控制(路由和 DNS)、绕过私网地址、端口转发问题

相信大家都会有不需要走网关代理和不想让它走网关代理的设备,比如智能家居、BT 下载机之类的设备,或者不想让来访的客人设备走网关代理,应该怎么办呢?

同样是抛砖引玉,我给出我的办法。

1. 在 LAN 接口将 DHCP 分配的起点改到 130 以上,限制 125 以内自己决定(我这边设备少,为了方便管理设定了 200,55);

2. 将你要走网关代理的设备设定静态 IP 地址,在 192.168.1.2-192.168.1.126 之间;

3. 修改之前在 PBR 上创建的 GoClash 策略,将 Local addresses / devices 一栏改成 192.168.1.0/25 (根据你的子网前缀修改)——为什么是 /25 :/25 囊括了子网前半部分 192.168.1.1-192.168.1.126 的地址,方便进行访问控制,即你手动制定到这个 IP 范围内的设备流量会被路由到 Clash Tun,而未指定的设备、新加入的设备会默认分配到这个范围之外,即默认不走网关代理;

4. 新建一条 PBR 策略,命名为 Local_ips,在 Remote addresses / domains 填上  0.0.0.0/8 10.0.0.0/8 127.0.0.0/8 169.254.0.0/16 172.16.0.0/12 192.168.0.0/16 224.0.0.0/4 240.0.0.0/4 ,用空格隔开,LuCI 界面会标红,但不用管他(是个小 bug),Interface 选 WAN/LAN/IGNORE(我选 IGNORE),并将这条策略上移,保证在 GoClash 策略上面的位置——这条是让私网流量绕过 Clash Tun;

5. 新建一条 PBR 策略,命名为 Ports_Fwd,根据你的需要,将你设定的(子网 /25 之内的)端口转发源设备 IP 填到 Local addresses / devices 一栏、源设备端口填到 Local ports,Interface 选 WAN/LAN/IGNORE,多个项目可以用空格隔开,但最好每个设备新建一条策略,并上移位置,保证在 GoClash 策略之上的位置——Clash Tun 会影响端口转发;

6. 在 Network -> Firewall -> Custom Rules 中粘贴以下 iptables 规则:
# DNS Hijack for Clash-Bypass devices
iptables -t nat -A PREROUTING ! -s 192.168.1.0/25 -p udp --dport 53 -j DNAT --to-destination 119.29.29.29 -m comment --comment "DNS Hijack Rule in Firewall Custom Rules"

——由于子网所有 DNS 请求都指向了 DNS,如果只做路由控制,不走 Clash 网关的设备请求 DNS 之后还是会得到 198.18.0.x 的 Clash Fake-IP 结果,而这个网段按照默认路由一定会走向 Clash 网关,所以需要劫持这些设备的 DNS 请求到一个正常的 DNS 之上,其中 192.168.1.0 需要按自己的子网网段改动,119.29.29.29 也可以改成其他的 DNS。

————————————————————————————————————

然后其实本文的所有步骤就走完了,其他的需求比如更新配置文件啊,更新 mmdb 文件啊,其他的访问控制啊,就需要自己解决了,毕竟 PBR 的策略还是很简洁明了的。

四、其他

这个 PBR 是一位加拿大开发者的作品,大家可以去 github 看看:@stangri

可能有人想知道 PBR 到底干了什么,我给两张截图你们就知道了:

PBR 给要去往 WAN 和 Clash 的流量分别打了标

PBR 给 WAN 和 Clash 分别创建了桌子(x

整个流程有两个比较粗糙的地方:

一是为了应对 utun 下线后上线而导致路由丢失的 PBR bug 而在 Clash 控制脚本中强行重启 PBR。对于这一点,PBR 有热重载机制,只要稍微改改代码就能补上这条 bug,但是我不想(不会)改代码,所以暂时就这么着吧;

二是自己劫持绕过 Clash 网关的设备的 DNS 请求。关于这一点,我也花时间研究过 dnsmqsq 的 tag 分组方法,但是研究了半天觉得在 OpenWrt 上比较难弄,如果写个脚本来处理的话,还不如一行 iptables 优雅呢,所以也就是本人能力所限,这一点也没有更好的办法。

希望这两个地方都能有更好的处理办法……

可能有大佬觉得,折腾半天也就是几行 iptables 的事,但是怎么说呢,对于我等小白来说,有一个 UI 界面比看不见摸不着的 Shell 脚本要令人安心得多~

最后,重申一遍,本文就是个抛砖引玉的玩意儿,希望大家多多批评指点~


参考链接:

https://openwrt.org/

https://github.com/Dreamacro/clash#premium-features

https://github.com/stangri/repo.openwrt.melmac.net

https://docs.openwrt.melmac.net/

https://docs.openwrt.melmac.net/pbr/

评论

  1. 记录一下,今天 dnsmasq 把我炸了……所以我这个 DNS 方案不算靠谱。
    dnsmasq 的 fallback 毕竟不是真 fallback,只是我臆想出来的功能罢了……

    回复删除

发表评论