Armbian + R2S 番外篇:透明代理

透明代理这个东西说起来很容易理解,就是设备感知不到有的流量被代理,却有开了代理的效果。这个东西对于学习境外内容的体验提升很大,因为部分应用并不遵循系统代理设置,或设置很麻烦。避免将代理配置文件存放在电脑上,也可以防止电脑上的应用窃取代理信息。

透明代理本身在中文互联网已经是聊烂了的话题,但使用 Nftables 实现的透明代理很少,在主路由上实现透明代理的更少。我的网络拓扑大概是:外部网络 - R2S - 无线 AP - 其他设备。本文将基于这个拓扑,实现在 R2S 上使用 Nftables 实现透明代理。

这个东西作为上篇文章的一个小节显得有点喧宾夺主了,所以就单独写一篇文章吧。

前置知识

只需要基础的计网知识,以及初步了解 Nftables(起码能看懂规则)。

另外丢掉那张老旧的(you know what) Netfilter 数据包流向图吧,由于 Nftables 的链是可以自由添加、自定义优先级的,现在它应该是这样1

Netfilter 数据包流向图
Netfilter 数据包流向图

思路

要达到的效果是这样的:

  • DHCP 服务器将默认 DNS 服务器指向本机的 Telescope DNS,由其负责解析2,直连 DoH
  • 局域网内通信不经过代理工具,包括 DHCP 等特殊服务
  • 其他来自局域网内的 TCP/UDP 流量都经过代理工具,由代理工具分流
  • 不代理路由本身流量,暂时没需求

也就是说流量会经过 Nftables 和代理工具两次分流。对于较为固定、不高于三层的分流规则,交给 Nftables 效率更高;而对于按站点分流之类的规则,则交给代理工具,更灵活。

对于第一条,局域网内机器发送数据包的路径为:

  1. 局域网内客户端发出 DNS 请求
  2. Nftables prerouting 对目的为局域网内的请求进行直连,不代理
  3. Telescope DNS 接收到请求(此处设未缓存)
  4. Telescope DNS 向上游发出请求,不带任何 meta mark
  5. Nftables postrouting 进行 NAT
  6. 远程 DNS 服务器收到请求,返回结果
  7. Nftables prerouting 对来自局域网外的请求直连
  8. Telescope DNS 收到上游回复
  9. Telescope DNS 将结果返回给客户端
  10. Nftables postrouting 进行 NAT
  11. 客户端收到回复

对内网的包:

  1. 局域网内客户端发出请求
  2. Nftables prerouting 对目的为局域网内的请求进行直连,不代理
  3. 进入 forward 链,转发给目标机器 或/同时被本机处理

对分流决定代理的正常 TCP/UDP 流量:

  1. 局域网内客户端发出请求
  2. Nftables prerouting 对于来自局域网、目标为外网的请求,进行 tproxy
  3. 代理工具接收并封装请求,并发送到代理服务器,带有 meta mark 2
  4. Nftables postrouting 进行 NAT
  5. 代理服务器处理代理请求,返回结果
  6. Nftables prerouting 对于来自外网的请求,不代理(直接发往代理工具)
  7. 代理工具接收并解封装请求,将结果返回给客户端
  8. Nftables postrouting 进行 NAT
  9. 客户端收到回复

对于决定不分流的正常 TCP/UDP 流量,其实也和上面相似,只不过代理工具直接将请求发往目标服务器,目标服务器也直接把结果返回给代理工具。

实现

类似于 Xray TProxy 透明代理 之类的本机透明代理方案很多,一般也适用于旁路由。核心的规则其实就两条,也就是在 prerouting 的链上加入(tproxy 仅支持 prerouting):

perl
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 的代理,同时避免了远程服务器发回的流量被重新代理局域网内主机间流量被代理的问题:

perl
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 阶段进行3。此时,网络层的目的 IP 仍然是本机的外网 IP,而非 192.168.0.1 这样的局域网 IP。

然而,出于某些原因,我们并不能在 postrouting 阶段等待 NAT 完成后再更改目的 IP4,也就是说这个阶段已经不能把数据包拐到代理上面了。考虑到 tproxy 的作用阶段,实际上在 prerouting 中同时进行 Nftables 阶段的分流和 tproxy 更为合适。相应的,为了识别远程服务器发回流量,我们要添加一条规则,也就是源 IP 不在内网 IP 段中,则不代理。

 

另外不管是上面的教程还是 Linux kernel 文档3,都指出 tproxy 应搭配自定义 IP route 使用,以让透明代理的包正确地传输到本地。在进行透明代理的同时为数据包附上 meta mark 1,这样就可以被以下两条命令定义的路由规则捕获,然后被传输到本地:

bash
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,需要添加入站:

json
 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:

json
 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 路由规则。

perl
 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
41
#!/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 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
    }
}

优化

Nftables 数据包追踪

在编写 Nftables 配置的时候,经常可能有疑问,这个数据包到哪里去了。可以使用 nft monitor trace 监视特定数据包的流向和规则判定过程,详细使用方法见 5

米家设备无法连网

如果开启透明代理的情况下,发现米家设备无法联网(或者无法绑定到 App),而关闭透明代理又恢复,那么可能是因为米家设备需要连接域名 mijia cloud(是的你没看错,中间一个空格),代理工具嗅探到了这个域名并尝试解析以分流,但其无法正常处理6,于是走默认。可以通过查看代理工具的日志来验证这个情况,Xray-core 和 Clash 都可能会有这个问题。

解决方法是跳过这个域名的嗅探分流,如 Xray-core 加上这一段:

json
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
{
  "inbounds": [
    {
      "sniffing": {
        "domainsExcluded": [
          "mijia cloud"
        ]
      }
    }
  ]
}

干出这个事的小米工程师真该拉出去祭天。


  1. https://thermalcircle.de/doku.php?id=blog:linux:nftables_packet_flow_netfilter_hooks_detail 另外这也是一篇极佳的 Nftables 入门文章,推荐阅读。 ↩︎

  2. 或许有人担心这会影响 Xray-core 的域名分流,但其域名分流是依靠 sniffing 头中的 SNI 实现的,即使 Xray-core 得不到解析前的域名,也不会受到影响。 ↩︎

  3. https://www.kernel.org/doc/Documentation/networking/tproxy.txt ↩︎ ↩︎

  4. https://serverfault.com/a/125913 ↩︎

  5. https://wiki.nftables.org/wiki-nftables/index.php/Ruleset_debug/tracing ↩︎

  6. https://github.com/XTLS/Xray-core/issues/293 ↩︎