最近由于考试周临近,所以博客这边都没怎么更新,这回逮到机会赶紧更一篇。我其实一直有个需求,就是想在学校也能无缝接入家里的网络,访问NAS之类的设备,因此我一直想设置一个透明代理。于是最近断断续续研究了几天,总算是摸索出了一个让自己相对满意的透明代理方案,因此就抽空写了篇博客,权当记录。事先说明:这篇博客仅仅描述了一个透明代理方案,并不包含任何代理服务器搭建的内容。方案的大致结构如下图,具体细节和配置我会在后文中详叙。
起因
对我而言,透明代理最重要的利好就是局域网设备接入和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是必须的。其次就是IPv6的支持,不过这个比较虚无,因为想要给局域网设备设置v6网关是一件很复杂的事情。最后就是性能,由于我的目标是将代理程序部署在旁路由(树莓派)上,因此代理程序的性能要好、占用也不能太大。此外就是要尽可能减少数据包路由的次数,尽量把路由工作放在内核空间(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
作为守护进程运行,配置如下
[Unit]
Description=utility for converting iptables(redirect/tproxy) to socks5
After=network.target
[Service]
User=nobody
EnvironmentFile=/etc/ipt2socks/ipt2socks.conf
CapabilityBoundingSet=CAP_NET_ADMIN CAP_NET_BIND_SERVICE
AmbientCapabilities=CAP_NET_ADMIN CAP_NET_BIND_SERVICE
NoNewPrivileges=true
ExecStart=/usr/bin/ipt2socks -s $server_addr -p $server_port -l $listen_port -j $thread_nums $extra_args
Restart=on-failure
RestartSec=5
[Install]
WantedBy=multi-user.target
这里手工强行加入了配置文件/etc/ipt2socks/ipt2socks.conf
,如果你怀念命令行参数的简洁,也可以直接修改ExecStart
。配置文件格式如下
# ipt2socks configure file
#
# detailed helps could be found at: https://github.com/zfl9/ipt2socks
# Socks5 server ip
server_addr=127.0.0.1
# Socks5 server port
server_port=1080
# Listen port number
listen_port=60080
# Number of the worker threads
thread_nums=1
# Extra arguments
extra_args=
相关配置和编译流程我已经添加至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段直连,还需要设定相关规则集(因为规则可能超级多,以大陆IP段为例,都用iptables
的效果还是很恐怖的),因此先介绍ipset
相关的配置。
ipset
iptables
的set
模块可以实现按规则集路由,而规则集的添加就是通过ipset
完成的。在apnic.net可以查询到分配给中国大陆的IP地址,因此解析下就可以添加到规则集了。脚本如下
# 下载并解析 route
wget --no-check-certificate -O- 'http://ftp.apnic.net/apnic/stats/apnic/delegated-apnic-latest' | grep CN > tmp_ips
cat tmp_ips | grep ipv4 | awk -F\| '{ printf("%s/%d\n", $4, 32-log($5)/log(2)) }' > chnroute.set
cat tmp_ips | grep ipv6 | awk -F\| '{ printf("%s/%d\n", $4, 32-log($5)/log(2)) }' > chnroute6.set
rm -rf tmp_ips
# 导入 ipset 表
sudo ipset -X chnroute &>/dev/null
sudo ipset -X chnroute6 &>/dev/null
sudo ipset create chnroute hash:net family inet
sudo ipset create chnroute6 hash:net family inet6
cat chnroute.set | sudo xargs -I ip ipset add chnroute ip
cat chnroute6.set | sudo xargs -I ip ipset add chnroute6 ip
运行后就可以得到IPv4和IPv6适用的规则集了(chnroute
和chnroute6
)。
iptables
这回是真的开始缝合了。总体的思路还是和新白话文的配置一样,把OUTPUT
链的包路由至PREROUTING
链,之后再用TPROXY
模块进行下一步转发。至于为什么要绕这么一个大圈就和TPROXY
本身的实现有关了,可以参考 @某昨 的TProxy探秘。
因此规则大概可以分为三个部分:策略路由、PREROUTING
链、OUTPUT
链。综合如下:
# fwmark 匹配的包进入本地环回
ip -4 rule add fwmark $lo_fwmark table 100
ip -4 route add local default dev lo table 100
########## PREROUTING 链配置 ##########
iptables -t mangle -N TRANS_PREROUTING
iptables -t mangle -A TRANS_PREROUTING -i lo -m mark ! --mark $lo_fwmark -j RETURN
# 规则路由
iptables -t mangle -A TRANS_PREROUTING -p tcp -m addrtype ! --src-type LOCAL ! --dst-type LOCAL -j TRANS_RULE
iptables -t mangle -A TRANS_PREROUTING -p udp -m addrtype ! --src-type LOCAL ! --dst-type LOCAL -j TRANS_RULE
# TPROXY 路由
iptables -t mangle -A TRANS_PREROUTING -p tcp -m mark --mark $lo_fwmark -j TPROXY --on-port $tproxy_port --on-ip $loopback_addr --tproxy-mark $tproxy_mark
iptables -t mangle -A TRANS_PREROUTING -p udp -m mark --mark $lo_fwmark -j TPROXY --on-port $tproxy_port --on-ip $loopback_addr --tproxy-mark $tproxy_mark
# 应用规则
iptables -t mangle -A PREROUTING -j TRANS_PREROUTING
########## OUTPUT 链配置 ##########
iptables -t mangle -N TRANS_OUTPUT
# 直连 @clash
iptables -t mangle -A TRANS_OUTPUT -j RETURN -m owner --uid-owner $direct_user
iptables -t mangle -A TRANS_OUTPUT -j RETURN -m mark --mark 0xff # (兼容配置) 直连 SO_MARK 为 0xff 的流量
# 规则路由
iptables -t mangle -A TRANS_OUTPUT -p tcp -m addrtype --src-type LOCAL ! --dst-type LOCAL -j TRANS_RULE
iptables -t mangle -A TRANS_OUTPUT -p udp -m addrtype --src-type LOCAL ! --dst-type LOCAL -j TRANS_RULE
# 应用规则
iptables -t mangle -A OUTPUT -j TRANS_OUTPUT
这里注意,由于要对clash
和overture
的流量直连,因此我选择使用owner
扩展,将用户clash的流量全部直连处理。之后将clash
和overture
进程运行在用户clash即可。此外就是由于两个链的路由规则是公共的(对于PREROUTING
链也可以用fwmark
来路由),因此独立出了TRANS_RULE
用来处理公共部分的路由(主要是标记fwmark
)。
########## 代理规则配置 ##########
iptables -t mangle -N TRANS_RULE
iptables -t mangle -A TRANS_RULE -j CONNMARK --restore-mark
iptables -t mangle -A TRANS_RULE -m mark --mark $lo_fwmark -j RETURN # 避免回环
# 私有地址
for addr in "${privaddr_array[@]}"; do
iptables -t mangle -A TRANS_RULE -d $addr -j RETURN
done
# ipset 路由
iptables -t mangle -A TRANS_RULE -m set --match-set $chnroute_name dst -j RETURN
# TCP/UDP 重路由 PREROUTING
iptables -t mangle -A TRANS_RULE -p tcp --syn -j MARK --set-mark $lo_fwmark
iptables -t mangle -A TRANS_RULE -p udp -m conntrack --ctstate NEW -j MARK --set-mark $lo_fwmark
iptables -t mangle -A TRANS_RULE -j CONNMARK --save-mark
规则很简单,基本就是不对匹配私有地址和规则集chnroute的数据包进行标记。并且使用CONNMARK
对整个连接的数据包进行标记,减少匹配次数。此外,由于OUTPUT
链的数据包还会被路由回PREROUTING
链,导致第二次匹配TRANS_RULE
,因此遇到有fwmark
的包就不必匹配了(没有fwmark
的包也不可能二次匹配)。
然后就是DNS流量的拦截。由于我需要对网络中所有DNS流量(UDP53)都进行拦截(无论请求哪个地址,这样就不用再手动改DNS配置了),因此不可避免的需要一次DNAT来将流量转发至overture
,所以我们还需要创建nat
表的转发规则。但是由于nat
表的位置靠后,因此需要在匹配TRANS_RULE
(位于mangle
表)之前先RETURN
所有的DNS流量,这样流量才能进入nat
表的转发规则。
# 局域网 DNS 路由
iptables -t mangle -A TRANS_PREROUTING -p udp -m addrtype ! --src-type LOCAL -m udp --dport 53 -j RETURN
iptables -t nat -A TRANS_PREROUTING -p udp -m addrtype ! --src-type LOCAL -m udp --dport 53 -j REDIRECT --to-ports $dns_port
# 这之后是 PREROUTING 链的 TRANS_RULE
# ...
# 本地 DNS 路由
iptables -t mangle -A TRANS_OUTPUT -p udp -m udp --dport 53 -j RETURN
for addr in "${dns_direct_array[@]}"; do
iptables -t nat -A TRANS_OUTPUT -d $addr -p udp -m udp --dport 53 -j RETURN
done
iptables -t nat -A TRANS_OUTPUT -p udp -m udp --dport 53 -j DNAT --to-destination $local_dns
# 这之后是 OUTPUT 链的 TRANS_RULE
这里还有个坑,就是owner
扩展不能很好的识别UDP流量的发送者。因此还需要对直连的DNS服务器单独增加匹配规则(这点我很不满意!但是也没办法……)。不过还好只需要加在OUTPUT
链,因为局域网设备就不必直连了。
至于效果么……BOOM忽略那感人的网速,其实刚刚试了下可以到40Mbps左右但是懒得更新图了(逃)
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/)