最近由于考试周临近,所以博客这边都没怎么更新。不过断断续续研究了几天,总算是摸索出了一个让自己相对满意的透明代理方案,因此就抽空写了篇博客,权当记录。事先说明:这篇博客仅仅描述了一个透明代理方案,并不包含任何代理服务器搭建的内容。方案的大致结构如下图,具体细节和配置我会在后文中详叙。
起因
对我而言,透明代理最重要的利好就是局域网设备接入和 CLI 程序。原先对于 CLI 程序我采用的是 proxychain
,也就是 hook 的方法。但是这种方法没办法针对自己实现请求的 go 程序,而更底层的 graftcp
在 DNS 上游的处理上也存在问题,因此我最后选择了使用透明代理进行解决。我之前使用的教程是新白话文的 TPROXY
配置,它能解决我几乎所有网络方面的痛点。
不过这个方案(主要是 v2ray
)还是有若干问题。首先就是配置的切换非常复杂,需要重启 v2ray
进程才能做到。其次就是没法做到 Fullcone NAT,这是 v2ray
本身机能所限。后来我更换了 clash
,并保留了 v2ray
作为透明代理的前置代理。clash
提供的 RESTful API 确实很好的解决了我关于配置切换的问题,但是我发现仍然无法做到 Fullcone。在后续的调查中我发现这不仅仅是 vmess
协议本身的限制,v2ray
的行为也注定了靠它没法做到 Fullcone。而且仅使用 v2ray
这样复杂的程序用来做 clash
的前置也是我无法接受的,因此我才打算探索新的透明代理方案。
要求
Fullcone NAT 是必须的。要问原因的话,就是 uu 加速器实在是太贵了,以及马造直连真的很卡。其次就是 IPv6 的支持,不过这个比较虚无,因为我家的宽带似乎不能 IPv6,但回校之后其实还是用得上。最后就是性能,由于我的目标是将代理程序部署在旁路由(树莓派)上,因此代理程序的性能要好、占用也不能太大。此外就是要尽可能减少数据包路由的次数,尽量把路由工作放在内核空间(netfilter),降低用户空间切换的开销。
至于为什么不在主路由上部署,原因很简单:学校里的主路由性能差。而且设置了旁路由代理就可以通过主路由设置 DHCP 来控制设备是否启用代理。此外,还有可以部署在 Manjaro 以供便携使用的优势。
后端代理
后端代理采用 clash
。虽然 v2ray
在配置上更加灵活,但是 clash
在运行状态时更加灵活。RESTful API 对我来说是更加重要的,因为借由它就可以使用诸如 yacd
等 WebAPP 快速的在配置之间进行切换。
中端代理
中端代理我使用了一个小巧的工具 ipt2socks。通过这个工具可以从 iptables
接受 TPROXY
流量,并转至 clash
的 Socks 入口。
了解 clash
的朋友可能知道,实际上 clash
本身提供了 TUN 功能用于处理 iptables
来的流量,那为什么还是选择了 ipt2socks
和 TPROXY
呢?的确,TUN 对 iptables
配置的影响不大,而且它的兼容性实际上高于 TPROXY
(部分发行版不自带),最重要的是,它还节省了一次将数据包包装 Socks 协议的过程。
对于这个,我的理由是解耦。不谈 clash
的实现是否稳定,可以确定的是,几乎没有什么代理软件是不支持 Socks 协议的,而支持 TUN 的实际上凤毛麟角。此外,使用 Socks 还意味着支持诸如 MITMProxy 此类使用 Socks 接口的网络应用。而至于性能,在最终的配置下,大多数请求实际上都不会经由这个 Socks 接口。加之 ipt2socks
的实现相当纯粹、轻量化(编译后 100K 不到),因此这一点的性能开销完全是值得的。
ipt2socks
的配置简洁到根本没有配置,所有配置都通过命令行参数来完成。可以使用 systemd
作为守护进程运行,配置如下
这里手工强行加入了配置文件 /etc/ipt2socks/ipt2socks.conf
,如果你怀念命令行参数的简洁,也可以直接修改 ExecStart
。配置文件格式如下
相关配置和编译流程我已经添加至 AUR。Archlinux 用户可以直接使用 yay
之类的程序进行安装。
DNS
我最终的选择是 overture。
说到中国特色社会主义 DNS 解析,大多数人大概第一时间就会想到 chinadns
。的确,chinadns
是一个相当完善可靠的程序,但是 chinadns
也显然不太适合直接作为本地 DNS 服务器 —— 它没有良好的缓存,并且也不支持复杂的路由规则。所以通常的做法是在前面套一个 dnsmasq
做缓存与分流,然后把 chinadns
作为上游。但是 dnsmasq
本身并不支持代理访问,因此你还需要在 iptables
层面对 dnsmasq
和 chinadns
的请求进行分流。这还没完,如果你的后端代理不支持 UDP,你还需要把 DNS 请求的 UDP 转成 TCP 请求(dns2tcp
工具)。所以最后,你得到了《世 界 名 画》chinadns
+dnsmasq
+dns2tcp
。暂且不论来回进出 iptables
的次数已经远远超过《半条命》的作品数,光是这个复杂配置我就觉得有够傻的。
此外另一个可能的选择就是 clash
的內建 DNS。而且 clash
还有 fake-ip 扩展以减少本地 DNS 解析的需要。但是问题有二,一个和之前不选择 TUN 的理由一致;另一个就是其他方案实际上也可以做到接近的效果,而使用 fake-ip 是要以缺少 DNS 缓存和可能得到错误的解析内容为代价的。
所以我找到了 overture
,它支持 IPv6、可以方便的替换 DNS 的 Upstream、支持通过 Socks 代理请求、支持 EDNS、有相对完善的 Dispatcher,可以说基本满足了我所有的要求。而且它还额外支持 RESTful API(虽然目前只能检查 cache),给进一步的配置管理带来了可能。
配置参考官方配置就行,AUR 软件包的默认配置也 OK。就是注意需要将 WhenPrimaryDNSAnswerNoneUse
改成 AlternativeDNS
。
路由分流
集齐了所有碎片,那下一步就该把他们缝合在一起了。缝合用的道具当然就是 iptables
了(IPv6 就是 ipt6ables
,配置几乎完全一致)。
分流的策略很简单,就是 DNS 交给 overture
,私有地址和大陆 IP 直连,剩下的交给 ipt2socks
。不过为了实现大陆 IP 直连,还需要设定相关规则集(因为规则超级多,都用 iptables
的效果还是很恐怖的),因此先介绍 ipset
相关的配置。
ipset
iptables
的 set
模块可以实现按规则集路由,而规则集的添加就是通过 ipset
完成的。在 apnic.net 可以查询到分配给中国大陆的 IP 地址,因此解析下就可以添加到规则集了。脚本如下
运行后就可以得到 IPv4 和 IPv6 适用的规则集了(chnroute
和 chnroute6
)。
iptables
这回是真的开始缝合了。总体的思路还是和新白话文的配置一样,把 OUTPUT
链的包路由至 PREROUTING
链,之后再用 TPROXY
模块进行下一步转发。至于为什么要绕这么一个大圈就和 TPROXY
本身的实现有关了,可以参考 @某昨 的 TProxy 探秘。
因此规则大概可以分为三个部分:策略路由、PREROUTING
链、OUTPUT
链。综合如下:
这里注意,由于要对 clash
和 overture
的流量直连,因此我选择使用 owner
扩展,将用户 clash 的流量全部直连处理。之后将 clash
和 overture
进程运行在用户 clash 即可。此外就是由于两个链的路由规则是公共的(对于 PREROUTING
链也可以用 fwmark
来路由),因此独立出了 TRANS_RULE
用来处理公共部分的路由(主要是标记 fwmark
)。
规则很简单,基本就是不对匹配私有地址和规则集 chnroute 的数据包进行标记。并且使用 CONNMARK
对整个连接的数据包进行标记,减少匹配次数。此外,由于 OUTPUT
链的数据包还会被路由回 PREROUTING
链,导致第二次匹配 TRANS_RULE
,因此遇到有 fwmark
的包就不必匹配了(没有 fwmark
的包也不可能二次匹配)。
然后就是 DNS 流量的拦截。由于我需要对网络中所有 DNS 流量(UDP53)都进行拦截(无论请求哪个地址,这样就不用再手动改 DNS 配置了),因此不可避免的需要一次 DNAT 来将流量转发至 overture
,所以我们还需要创建 nat
表的转发规则。但是由于 nat
表的位置靠后,因此需要在匹配 TRANS_RULE
(位于 mangle
表)之前先 RETURN
所有的 DNS 流量,这样流量才能进入 nat
表的转发规则。
这里还有个坑,就是 owner
扩展不能很好的识别 UDP 流量的发送者。因此还需要对直连的 DNS 服务器单独增加匹配规则(这点我很不满意!但是也没办法……)。不过还好只需要加在 OUTPUT
链,因为局域网设备就不必直连了。
至于效果么……BOOM 忽略那感人的网速
Sum up
最终编写得到了三个脚本:
transparent_proxy.sh
:透明代理规则设置,需要开机运行import_chnroute.sh
:下载并配置 chnroute 规则,至少需要运行一次,并且规则集文件要和transparent_proxy.sh
同目录(当然你也可以修改配置)flush_iptables.sh
:清理所有增加的规则(除了ipset
)
这些代码都可以在我的 GitHub 找到。编写的时候,我大量参考了 ss-tproxy 项目的相关代码,非常感谢这个 repo。
要部署这个配置,除了这三个 shell,你还需要安装 ipt2socks、overture(AUR 都有对应包:ipt2socks、overture)。此外,还需要一个支持 Socks 协议的代理(我用的是 clash,当然其他可以)。按照文中配置完后,修改 transparent_proxy.sh
的开头为你配置的相关内容即可。
缺陷
令人遗憾的是,这份配置还是有不完美之处的。不过好在都不是什么大问题,也可以曲线救国。
- 对于域名形式的代理服务器,必须给代理程序配置 DNS。由于在代理程序启动时需要解析代理服务器的真实 IP,因此需要请求
overture
。这原本没有什么问题,但是为了性能通常会开启AlternativeDNSConcurrent
。而此时overture
会请求clash
以访问备用 DNS,但是clash
还没启动。其实本来也没有问题,但是错就错在overture
在连接不上clash
的时候竟然会崩溃!然后clash
因为解析不到真实 IP 所以也跟着一起崩溃,然后 overture 崩,overture 崩完 clash 崩…… 解决方案有两个,一个是谨慎的调节启动顺序 ——iptables
规则要在clash
解析完毕后请求;另一个就是配置clash
本身的 DNS,让请求不走overture
。前者有点麻烦,而后者实际上是又增加一套和overture
处理不同的 DNS 配置。不过好在普通流量到了clash
都已经完成 DNS 解析,除了直连clash
的 Socks 否则不会用到內建 DNS,所以我选择了后者。本质是overture
的问题,所以如果它修就可以解决。 - 本机直连 DNS。之前说 DNS 配置时提到,必须对本机的直连 DNS 设置直连规则。而这就导致本机无法拦截对直连 DNS 服务器的 DNS 请求。解决方案很简单,就是本机 DNS 别设置成直连 DNS 那几个服务器就行。
overture
不支持 UDP via Socks。这个倒也无所谓,TCP 查询就行,对性能的影响可以忽略。
兄啊,怎么都是 DNS 的问题啊
Reference
- [v1.0] Tun+MITMProxy 初探(https://blog.yesterday17.cn/post/transparent-proxy-with-mitmproxy/)
- zfl9/ss-tproxy(https://github.com/zfl9/ss-tproxy/)