透明代理这个东西说起来很容易理解,就是设备感知不到有的流量被代理,却有开了代理的效果。这个东西对于学习境外内容的体验提升很大,因为部分应用并不遵循系统代理设置,或设置很麻烦。避免将代理配置文件存放在电脑上,也可以防止电脑上的应用窃取代理信息。
透明代理本身在中文互联网已经是聊烂了的话题,但使用 Nftables 实现的透明代理很少,在主路由上实现透明代理的更少。我的网络拓扑大概是:外部网络 - R2S - 无线 AP - 其他设备。本文将基于这个拓扑,实现在 R2S 上使用 Nftables 实现透明代理。
这个东西作为上篇文章 的一个小节显得有点喧宾夺主了,所以就单独写一篇文章吧。
前置知识
只需要基础的计网知识,以及初步了解 Nftables(起码能看懂规则)。
另外丢掉那张老旧的(you know what) Netfilter 数据包流向图吧,由于 Nftables 的链是可以自由添加、自定义优先级的,现在它应该是这样 :
Netfilter 数据包流向图
思路
要达到的效果是这样的:
DHCP 服务器将默认 DNS 服务器指向本机的 Telescope DNS,由其负责解析 ,直连 DoH
局域网内通信不经过代理工具,包括 DHCP 等特殊服务
其他来自局域网内的 TCP/UDP 流量都经过代理工具,由代理工具分流
不代理路由本身流量,暂时没需求
也就是说流量会经过 Nftables 和代理工具两次分流。对于较为固定、不高于三层的分流规则,交给 Nftables 效率更高;而对于按站点分流之类的规则,则交给代理工具,更灵活。
对于第一条,局域网内机器发送数据包的路径为:
局域网内客户端发出 DNS 请求
Nftables prerouting
对目的为局域网内的请求进行直连,不代理
Telescope DNS 接收到请求(此处设未缓存)
Telescope DNS 向上游发出请求,不带任何 meta mark
Nftables postrouting
进行 NAT
远程 DNS 服务器收到请求,返回结果
Nftables prerouting
对来自局域网外的请求直连
Telescope DNS 收到上游回复
Telescope DNS 将结果返回给客户端
Nftables postrouting
进行 NAT
客户端收到回复
对内网的包:
局域网内客户端发出请求
Nftables prerouting
对目的为局域网内的请求进行直连,不代理
进入 forward
链,转发给目标机器
或/同时被本机处理
对分流决定代理的正常 TCP/UDP 流量:
局域网内客户端发出请求
Nftables prerouting
对于来自局域网、目标为外网的请求,进行 tproxy
代理工具接收并封装请求,并发送到代理服务器,带有 meta mark 2
Nftables postrouting
进行 NAT
代理服务器处理代理请求,返回结果
Nftables prerouting
对于来自外网的请求,不代理(直接发往代理工具)
代理工具接收并解封装请求,将结果返回给客户端
Nftables postrouting
进行 NAT
客户端收到回复
对于决定不分流的正常 TCP/UDP 流量,其实也和上面相似,只不过代理工具直接将请求发往目标服务器,目标服务器也直接把结果返回给代理工具。
实现
类似于 Xray TProxy 透明代理 之类的本机透明代理方案很多,一般也适用于旁路由。核心的规则其实就两条,也就是在 prerouting
的链上加入(tproxy
仅支持 prerouting
):
ip protocol tcp tproxy to 127.0.0.1:12345 meta mark set 1
ip protocol udp tproxy to 127.0.0.1:12345 meta mark set 1
1
2
ip protocol tcp tproxy to 127.0.0.1 : 12345 meta mark set 1
ip protocol udp tproxy to 127.0.0.1 : 12345 meta mark set 1
然而就以上面链接中这篇教程的配置为例,它包含如下三行规则,通过跳过目的为本地 IP 的代理,同时避免了远程服务器发回的流量被重新代理 和局域网内主机间流量被代理 的问题:
ip daddr $RESERVED_IP return
ip daddr 192.168.0.0/16 tcp dport != 53 return
ip daddr 192.168.0.0/16 udp dport != 53 return
1
2
3
ip daddr $RESERVED_IP return
ip daddr 192.168.0.0 / 16 tcp dport != 53 return
ip daddr 192.168.0.0 / 16 udp dport != 53 return
第二个问题很好理解。而这些规则能解决第一个问题,主要原因在于主路由已经做好了 NAT,看起来已经是从远程主机 IP 发往代理客户端主机 IP 的流量了,也就是有了目的 IP 在上述范围内的特征。同样的方法却不能套到主路由上,因为 NAT 一般在 postrouting
阶段去做,而 tproxy
一般在 prerouting
阶段进行 。此时,网络层的目的 IP 仍然是本机的外网 IP,而非 192.168.0.1 这样的局域网 IP。
然而,出于某些原因,我们并不能在 postrouting
阶段等待 NAT 完成后再更改目的 IP ,也就是说这个阶段已经不能把数据包拐到代理上面了。考虑到 tproxy
的作用阶段,实际上在 prerouting
中同时进行 Nftables 阶段的分流和 tproxy
更为合适。相应的,为了识别远程服务器发回流量,我们要添加一条规则,也就是源 IP 不在内网 IP 段中 ,则不代理。
另外不管是上面的教程还是 Linux kernel 文档 ,都指出 tproxy
应搭配自定义 IP route 使用,以让透明代理的包正确地传输到本地。在进行透明代理的同时为数据包附上 meta mark 1,这样就可以被以下两条命令定义的路由规则捕获,然后被传输到本地:
ip rule add fwmark 1 lookup 100 # 对于带标记 1 的包,使用编号为 100 的路由表(100 没有什么特殊含义)
ip route add local 0.0.0.0/0 dev lo table 100 # 对于路由表 100,经过 lo 设备发送到 0.0.0.0
1
2
ip rule add fwmark 1 lookup 100 # 对于带标记 1 的包,使用编号为 100 的路由表(100 没有什么特殊含义)
ip route add local 0.0.0.0/0 dev lo table 100 # 对于路由表 100,经过 lo 设备发送到 0.0.0.0
实验表明去掉这两条规则后,无法正确进行透明代理。由于 tproxy
并不会修改数据包本身的内容,所以我猜测该数据包可能会按照默认策略路由,不会进入本地回环为代理工具所接收。
接下来就该配置代理软件了。我个人建议使用 Xray-core,需要添加入站:
{
"inbounds": [
{
"tag": "all-in",
"port": 12345,
"listen": "127.0.0.1",
"protocol": "dokodemo-door",
"settings": {
"network": "tcp,udp",
"followRedirect": true
},
"sniffing": {
"enabled": true,
"destOverride": [
"http",
"tls"
]
},
"streamSettings": {
"sockopt": {
"tproxy": "tproxy"
}
}
},
]
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
{
"inbounds" : [
{
"tag" : "all-in" ,
"port" : 12345 ,
"listen" : "127.0.0.1" ,
"protocol" : "dokodemo-door" ,
"settings" : {
"network" : "tcp,udp" ,
"followRedirect" : true
},
"sniffing" : {
"enabled" : true ,
"destOverride" : [
"http" ,
"tls"
]
},
"streamSettings" : {
"sockopt" : {
"tproxy" : "tproxy"
}
}
},
]
}
并且建议为每个出站都设定 meta mark 不为 1:
{
"outbounds": [
{
"streamSettings":{
"sockopt": {
"mark": 2
},
},
},
]
}
1
2
3
4
5
6
7
8
9
10
11
{
"outbounds" : [
{
"streamSettings" :{
"sockopt" : {
"mark" : 2
},
},
},
]
}
在添加了上面的规则后,发现连不上新设备了,查找 DHCP 服务器的日志也并未发现端倪。发现由于 DHCP 的 Discover 目标为广播 IP,并未被排除代理,所以被代理工具截留。我使用的 workaround 为添加一条规则,对目标为 udp/67 端口的数据包不代理,DHCP 恢复正常。
宿舍的网络竟然有外界可以直接访问的公网 IP(震惊),所以个人还加了 filter
表,用于防止外界白嫖代理。这个看一下下面的配置文件就明白,我不多讲。
配置
如果你希望使用此配置文件,请根据你路由器的接口分配、网段分配、代理工具等情况进行修改。另外也不要忘了添加 ip
路由规则。
#!/usr/sbin/nft -f
flush ruleset
define RESERVED_IP = {
192.168.0.0/24,
0.0.0.0/8,
}
define WANLINK = lan0
define LANLINK = eth0
table ip nat {
chain postrouting {
type nat hook postrouting priority 100; policy accept;
oifname $WANLINK masquerade
}
}
table ip filter {
chain input {
type filter hook input priority 0; policy accept;
# only accept local/LAN traffic to tproxy
ip saddr != $RESERVED_IP tcp dport 12345 drop
}
}
table ip xray {
chain prerouting {
type filter hook prerouting priority mangle; policy accept;
# source not from LAN (e.g. server reply), don't proxy
ip saddr != $RESERVED_IP return
# dest loopback or local traffic, don't proxy
ip daddr $RESERVED_IP return
# allow dhcp traffic (daddr may be 255.255.255.0)
udp dport 67 return
# tproxy traffic
ip protocol {tcp,udp} tproxy to 127.0.0.1:12345 meta mark set 1
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
#!/usr/sbin/nft -f
flush ruleset
define RESERVED_IP = {
192.168.0.0 / 24 ,
0.0.0.0 / 8 ,
}
define WANLINK = lan0
define LANLINK = eth0
table ip nat {
chain postrouting {
type nat hook postrouting priority 100 ; policy accept ;
oifname $WANLINK masquerade
}
}
table ip filter {
chain input {
type filter hook input priority 0 ; policy accept ;
# only accept local/LAN traffic to tproxy
ip saddr != $RESERVED_IP tcp dport 12345 drop
}
}
table ip xray {
chain prerouting {
type filter hook prerouting priority mangle ; policy accept ;
# source not from LAN (e.g. server reply), don't proxy
ip saddr != $RESERVED_IP return
# dest loopback or local traffic, don't proxy
ip daddr $RESERVED_IP return
# allow dhcp traffic (daddr may be 255.255.255.0)
udp dport 67 return
# tproxy traffic
ip protocol { tcp , udp } tproxy to 127.0.0.1 : 12345 meta mark set 1
}
}
IPv6
本节参考
IPv6 透明代理的实现与上面相似,甚至可以不用建新表,而是修改 xray
表即可。
IPv6 对应的路径设置如下:
ip -6 rule add fwmark 1 table 106
ip -6 route add local ::/0 dev lo table 106
1
2
ip -6 rule add fwmark 1 table 106
ip -6 route add local ::/0 dev lo table 106
这里我直接将修改后的 xray
表列在下面:
table inet xray {
chain prerouting {
type filter hook prerouting priority filter; policy accept;
ip daddr { 127.0.0.0/8, 224.0.0.0/4, 255.255.255.255 } return
ip6 daddr 2408::/16 return
meta l4proto tcp ip daddr 192.168.0.0/16 return
ip daddr 192.168.0.0/16 udp dport != 53 return
ip6 daddr { ::1, fe80::/10 } return
meta l4proto tcp ip6 daddr fd00::/8 return
ip6 daddr fd00::/8 udp dport != 53 return
# tailscale
udp sport 41641 return
udp dport 41641 return
udp dport 3478 return
udp sport 3478 return
meta mark 2 return
meta l4proto { tcp, udp } meta mark set 1 tproxy ip to 127.0.0.1:12345 accept
meta l4proto { tcp, udp } meta mark set 1 tproxy ip6 to [::1]:12345 accept
}
chain divert {
type filter hook prerouting priority mangle; policy accept;
meta l4proto tcp socket transparent 1 meta mark set 1 accept
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
table inet xray {
chain prerouting {
type filter hook prerouting priority filter ; policy accept ;
ip daddr { 127.0.0.0 /8, 224.0.0.0/ 4 , 255.255.255.255 } return
ip6 daddr 2408 ::/ 16 return
meta l4proto tcp ip daddr 192.168.0.0 / 16 return
ip daddr 192.168.0.0 / 16 udp dport != 53 return
ip6 daddr { :: 1 , fe80:: / 10 } return
meta l4proto tcp ip6 daddr fd00:: / 8 return
ip6 daddr fd00:: / 8 udp dport != 53 return
# tailscale
udp sport 41641 return
udp dport 41641 return
udp dport 3478 return
udp sport 3478 return
meta mark 2 return
meta l4proto { tcp , udp } meta mark set 1 tproxy ip to 127.0.0.1 : 12345 accept
meta l4proto { tcp , udp } meta mark set 1 tproxy ip6 to [ :: 1 ]: 12345 accept
}
chain divert {
type filter hook prerouting priority mangle ; policy accept ;
meta l4proto tcp socket transparent 1 meta mark set 1 accept
}
}
如果你的代理工具只监听了 IPv4 地址,那么需要同时监听 IPv6 [::1]:12345。
优化
Nftables 数据包追踪
在编写 Nftables 配置的时候,经常可能有疑问,这个数据包到哪里去了。可以使用 nft monitor trace
监视特定数据包的流向和规则判定过程,详细使用方法见 。
米家设备无法连网
如果开启透明代理的情况下,发现米家设备无法联网(或者无法绑定到 App),而关闭透明代理又恢复,那么可能是因为米家设备需要连接域名 mijia cloud
(是的你没看错,中间一个空格),代理工具嗅探到了这个域名并尝试解析以分流,但其无法正常处理 ,于是走默认。可以通过查看代理工具的日志来验证这个情况,Xray-core 和 Clash 都可能会有这个问题。
解决方法是跳过这个域名的嗅探分流,如 Xray-core 加上这一段:
{
"inbounds": [
{
"sniffing": {
"domainsExcluded": [
"mijia cloud"
]
}
}
]
}
1
2
3
4
5
6
7
8
9
10
11
{
"inbounds" : [
{
"sniffing" : {
"domainsExcluded" : [
"mijia cloud"
]
}
}
]
}
干出这个事的小米工程师真该拉出去祭天。
米哈游游戏登录失败
如国服原神“连接超时”,国服星穹铁道错误“1001_1”等。即使设置了正确的分流规则和 DNS 解析,也可能无法登录。抓包发现有许多和米哈游的 TCP 连接被 reset 掉,暂时不知道根本原因。
个人的解决办法是使用 Nftables 进行国内 IP 分流,具体可见 这篇文章 。实测使用该措施后,由于不需要回到用户态过代理软件,日常上网也会快一点。