最近手握ubnt ER-X,把计网拿出来活用了一波,实现了局域网内的自动VPN代理,智能分流需要走代理的流量。本质上是在路由器上通过OpenVPN连接到目标网络,但是只是这样做会使得所有流量都走VPN,而这显然是无法满足丰富多样的用户需求的。
在ovpn文件里添加
route-nopull
可以指定OpenVPN不更新路由器的路由表,这样允许我们自行决定流量走向。
随后五一期间尝试了的三种方案,来实现VPN流量智能分流的功能。
太长不看版:
准备工作一 OpenVPN的搭建和连接
ubnt路由器自带OpenVPN客户端。OpenVPN服务器申请、搭建和连接详情这里不讲。
准备工作二 DNS转发设置
DNS是一个明文协议,而国内可访问的DNS服务器在查询部分国外域名时由于山高路远,结果难免会受到污染。当DNS结果被污染时,就会频繁出现HTTPS证书错误以及明明可以ping通却无法访问网页以及其它各种各样的问题。因此,要正常实现丝滑的智能分流上网,需要首先对DNS进行一个额外配置。配置所需要达到的目的是:
- 国内域名请求,通过国内流量,走国内DNS,以充分利用国内CDN。
- 容易被污染的域名请求,通过VPN流量,走国外DNS,以避免山高路远导致的结果污染。
而上述目的可以通过ubnt路由器自带的dnsmasq和静态路由表实现。
首先,我们在ubnt的EdgeOS的网页管理的Routing页面中添加一条静态路由(Add Static Route)将google DNS 8.8.8.8固定路由到VPN的接口,我这里是vtun0
当然也可以顺手添加其它常见的国际DNS,比如9.9.9.9、1.1.1.1,为了描述方便,后面我们统一使用8.8.8.8。随后,ubnt路由器自带了dnsmasq功能,我们需要通过配置dnsmasq来指定哪些域名需要通过google DNS 8.8.8.8来完成解析。dnsmasq的配置格式为xxx.conf,其具体语法形如
server=/google.com/8.8.8.8
server=/twitter.com/9.9.9.9
表示的是google.com域名下的DNS请求转发至8.8.8.8,而twitter.com域名下的DNS请求转发至9.9.9.9。不在配置列表里的域名,则由路由器默认的DNS服务器直接解析。
dnsmasq配置更加详细的语法可以参照这里。
有同学可能会问:能不能直接让所有DNS解析都走VPN而不配置dnsmasq呐?当然是可以的,但是体验上可能会有一些问题。主要是CDN往往是依赖于DNS工作的,如果走VPN去解析DNS,很容易解析出离VPN近但是离你飘洋过海的CDN服务器,这就使得你有一种网速变慢的感觉。
在ubnt路由器里,dnsmasq的配置文件位于目录
/etc/dnsmasq.d/
你可以按需要自己生成一套配置文件,当然也可以借用别人已经生成好的配置文件,举个例子,这个项目提供了gfwlist到dnsmasq配置的生成脚本。
把配置文件同步到路由器上后,记得重启dnsmasq服务
sudo -i
systemctl restart dnsmasq
并保证局域网内的上网客户端的DNS服务器都是路由器的IP(可通过路由器DHCP选项设置)。
方案一 PAC自动代理
PAC (Proxy Auto-Configuration) 是用于实现局域网内自动代理的常见途径,它本质上是一个包含
FindProxyForURL(url, host)
函数的js文件,用来告诉客户端哪些URL需要走代理服务器,而哪些URL则可以直接连接。PAC文件可以自己配置,也可以根据需要找一些前人配置好的,举个例子,比如这里提供了从gfwlist生成PAC的工具。
优点:
- 非常经典成熟的技术路线
- 搭建相比于其它方案最简单
缺点:
- 需要额外配置一个SOCKS代理服务器
- 每个上网设备需要额外开启代理选项,并设置PAC文件路径
基于PAC的自动代理首先需要搭建一个SOCKS代理服务器。简单起见,我们可以在OpenVPN服务器上直接通过dante-server实现一个SOCKS4/SOCKS5服务器。如在ubuntu下可以通过
sudo apt install dante-server
来安装,并修改配置文件
/etc/danted.conf
使得dante-server接受来自VPN的连接请求,并将请求代理由服务器的网络接口进行转发。配置文件的具体修改可以参照比如这些攻略。
随后将PAC文件里的代理服务器指向局域网内dante-server所在的主机,再将PAC文件host到局域网内可以访问的HTTP服务器上。客户端启用代理时,只需提供PAC文件的地址即可实现自动代理。下面以Windows 11为例:
值得一提的是,WPAD (Web Proxy Auto-Discovery Protocol) 协议允许客户端在局域网中自动发现PAC配置,这样客户端可以只开启代理功能,而无需手动填写PAC文件路径。例如,在Windows 11下,如果配置好WPAD,我们只需要打开
在这里简单过一下WPAD的配置流程。WPAD一共定义了两种自动发现途径,一种是通过DHCP,另一种是通过DNS,而这两种都可以基于ubnt路由器比较方便的实现。
DHCP自动发现。可在路由器网页端Config Tree配置。找到service=>dhcp-server=>shared-network-name=>LAN=>subnet下你的子网,然后编辑wpad-url为你需要启用自动发现的PAC文件地址,然后记得保存。DHCP自动发现对PAC文件名没有额外要求。
DNS自动发现。基于DNS的WPAD会在局域网内主机名为wpad的服务器的HTTP服务上下载wpad.dat文件作为自动代理配置,即自动请求http://wpad/wpad.dat,这是个定死的规范。这里的wpad.dat和proxy.pac实际上是同一个文件,只是DNS这里对文件命名有硬性要求。要实现DNS自动发现,我们只需要保证局域网内的主机能够顺利下载到http://wpad/wpad.dat即可。首先,在Config Tree里为局域网添加一个任意的domain-name,例如我这里是ccss。至于为什么要设置domain-name,因为我不设置domain-name的解析尝试好像都失败了……
随后,在Config Tree的DNS设置里,将wpad.ccss固定解析到存放wpad.dat文件的IP地址
是不是看着很眼熟?因为这里依然是在配置前面准备工作二里接触过的dnsmasq,只不过这里用的语法形如
address=/wpad.ccss/192.168.1.1
意思是将wpad.ccss的DNS请求固定解析到指定的IP地址192.168.3.1,而不进行转发。随后,你应该能够在局域网内通过http://wpad/wpad.dat下载到你的自动代理配置文件。
最后讲讲WPAD的缺点。除了每个上网设备都需要额外开启代理开关外,WPAD还容易导致一系列的安全问题。所以在iOS 15中就开始对其做出了诸多限制,比如PAC地址必须要是https(神经病,我一个IP哪里给你HTTPS)之类的。这正是为什么我不再推荐基于PAC去实现智能的分流。
方案二 静态路由表
路由器静态路由表的可以无需对局域网内上网客户端进行额外设置,直接引流指定IP段到VPN接口,适用于你事先知道哪些IP段需要走VPN的情况。
静态路由表可以通过在OpenVPN的ovpn配置文件里添加形如
route-nopull
route 1.0.0.0 255.255.248.0 vpn_gateway
route 1.0.16.0 255.255.240.0 vpn_gateway
route 1.0.64.0 255.255.192.0 vpn_gateway
route 1.0.128.0 255.255.128.0 vpn_gateway
的配置,通过IP和子网掩码指定哪些流量流量需要走VPN。
在实际应用中有一种需求是通过公开的IP分配表
将大陆以外的IP段设置为需要走VPN。全部的路由整合后大概有几千条,可以在网上找到,也可以自己生产(如果有需要,我可以提供公开的IP分配表到ovpn的route配置的脚本参考哈)
优点:
- 相比于PAC,上网设备无需额外配置
缺点:
- 静态路由表需要非常精准的区分哪些流量走VPN,并且可能需要持续手动更新路由表
- 如果是大陆外流量走VPN这种粗暴的设置,像是Steam这种下载也会因为服务器位置问题而全部走VPN流量
方案三 结合DNS/ipset的智能路由
结合DNS的智能路由实际上基于了这样的假设:在准备工作二中需要走指定DNS解析的域名,也需要通过VPN去访问它们的IP;反过来一个需要由VPN访问的IP,必然是在准备工作二中由指定DNS解析得到的地址。
优点:
- 相比于PAC,上网设备无需额外配置
- 相比于静态路由,可以精确定制无需走VPN的流量
结合DNS的智能路由流程如下:
- 局域网客户端访问一个域名,并指定路由器DNS解析IP。
- 路由器的dnsmasq服务比对请求的域名,判断哪些域名需要经由VPN转发到指定DNS 8.8.8.8
- 对于2中指定DNS返回的IP,把它们记录到路由器ipset中
- 对于ipset中的IP,路由器统一路由到VPN
1和2我们已经在准备工作二里配置好了,而3也是dnsmasq服务支持的功能,可以通过配置文件实现。例如
server=/google.com/8.8.8.8
ipset=/google.com/gfwlist
表示google.com域名的DNS请求转发至DNS服务器8.8.8.8,同时将DNS结果添加到名为gfwlist的ipset中。
在ubnt路由器中,我们可以通过ssh事先创建一个名为gfwlist的ipset
sudo -i #后续操作均需要sudo
ipset create gfwlist hash:ip netmask 24
这里再创建ipset的时候耍了一个小聪明,即通过netmask 24合并了相近网段的IP,因此当123.123.123.1~255这255个IP添加到ipset中时,只会保留123.123.123.0这一个前缀的子网,这样可以有效避免ipset在长期运行后增长过快的问题。创建成功并且正确配置了dnsmasq后,在局域网内ping过指定域名后,你应该可以通过
ipset list gfwlist
看到里面的IP开始增长。
随后,要实现第4步基于ipset的路由转发,我们需要先借助于iptables,把目标IP位于ipset中的所有包打上标记。
iptables -t mangle -N gfwlist_mark1
iptables -t mangle -A gfwlist_mark1 -m set --match-set gfwlist dst -j MARK --set-mark 1
iptables -t mangle -I OUTPUT -j gfwlist_mark1 # 对路由器本身生效
iptables -t mangle -I PREROUTING -j gfwlist_mark1 # 对路由器转发生效
这里我们创建了一个名为gfwlist_mark1的规则,这个规则将匹配gfwlist这一ipset的包统一打上标记1。然后我们把这个规则附加到OUTPUT和PREROUTING这两个阶段之前。
然后是基于ip rule,我们添加一个名为gfwtable的路由表,其编号为100,并把前面打上标记1的包统一添加到这个名为gfwtable的表中
echo "100 gfwtable" | tee -a /etc/iproute2/rt_tables
ip rule add fwmark 1 table gfwtable
最后,通过ip route添加路由规则,将名为gfwtable的路由表中的包,统一由VPN接口转发出去,我的接口是vtun0,你可以通过ifconfig找到你的VPN接口
ip route add default dev vtun0 table gfwtable
上面的命令,每次重启路由器或在路由器端重连OpenVPN都需要执行一次,为了方便,我们可以把下面的配置追加到ovpn配置文件里,来实现连接OpenVPN时自动执行
script-security 2
route-up "/sbin/ip route add default dev vtun0 table gfwtable"
上面的/sbin/ip是ip命令的实际地址,可以通过在sudo下运行which ip看到,不同路由器可能不一样。