探索:适用 IPv6、Fullcone NAT 的旁路由透明代理方案

最近由于考试周临近,所以博客这边都没怎么更新。不过断断续续研究了几天,总算是摸索出了一个让自己相对满意的透明代理方案,因此就抽空写了篇博客,权当记录。事先说明:这篇博客仅仅描述了一个透明代理方案,并包含任何代理服务器搭建的内容。方案的大致结构如下图,具体细节和配置我会在后文中详叙。

起因

对我而言,透明代理最重要的利好就是局域网设备接入和 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 作为守护进程运行,配置如下

[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 直连,还需要设定相关规则集(因为规则超级多,都用 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 忽略那感人的网速

Sum up

最终编写得到了三个脚本:

  • transparent_proxy.sh:透明代理规则设置,需要开机运行
  • import_chnroute.sh:下载并配置 chnroute 规则,至少需要运行一次,并且规则集文件要和 transparent_proxy.sh 同目录(当然你也可以修改配置)
  • flush_iptables.sh:清理所有增加的规则(除了 ipset

这些代码都可以在我的 GitHub 找到。编写的时候,我大量参考了 ss-tproxy 项目的相关代码,非常感谢这个 repo。

要部署这个配置,除了这三个 shell,你还需要安装 ipt2socksoverture(AUR 都有对应包:ipt2socksoverture)。此外,还需要一个支持 Socks 协议的代理(我用的是 clash,当然其他可以)。按照文中配置完后,修改 transparent_proxy.sh 的开头为你配置的相关内容即可。

缺陷

令人遗憾的是,这份配置还是有不完美之处的。不过好在都不是什么大问题,也可以曲线救国。

  1. 对于域名形式的代理服务器,必须给代理程序配置 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 的问题,所以如果它修就可以解决。
  2. 本机直连 DNS。之前说 DNS 配置时提到,必须对本机的直连 DNS 设置直连规则。而这就导致本机无法拦截对直连 DNS 服务器的 DNS 请求。解决方案很简单,就是本机 DNS 别设置成直连 DNS 那几个服务器就行。
  3. overture 不支持 UDP via Socks。这个倒也无所谓,TCP 查询就行,对性能的影响可以忽略。

兄啊,怎么都是 DNS 的问题啊

Reference

  1. [v1.0] Tun+MITMProxy 初探(https://blog.yesterday17.cn/post/transparent-proxy-with-mitmproxy/
  2. zfl9/ss-tproxy(https://github.com/zfl9/ss-tproxy/

评论 在此处输入想要评论的文本。

Copied title and URL