我的透明代理方案 1.0

我的全局透明代理已经运行一年多,简单稳定。近期有些优化的想法,试验前先用本文给目前的方案打个版本号。

以下配置都是简化后的片段,仅供参考。

主路由/OpenWRT

我使用一台红米 AX6S 路由器刷 OpenWRT 后作为主路由,上游是客厅的接宽带的路由器。优点是隔离出了自己的专属内网,怎么折腾都不影响合租室友,即使以后搬家也不需要重新配置。代价仅仅是多了一次 NAT,性能损耗大可忽略不计。

基础配置:

  • 关闭 IPv6,因为不想多写一份配置
  • 关闭 SSH 密码登陆

安装所需依赖:

  • luci-i18n-base-zh-cn:中文语言包
  • 值守式系统更新,方便把需要的包封进新的系统固件
    • luci-app-attendedsysupgrade
    • luci-i18n-attendedsysupgrade-zh-cn
  • ipset:iptables 规则要用
  • 补充一些 iptables 扩展
    • iptables-mod-extra
    • iptables-mod-ipmark
    • iptables-mod-iprange
    • iptables-mod-socket
    • iptables-mod-tproxy
  • 补充 GNU Coreutils 里的工具
    • coreutils-nohup

流量重定向

流量重定向由 Linux TProxy 实现,技术细节可以看「深入理解 Linux TProxy」。

入站

Clash :

tproxy-port: 10000

开机自启用于创建 iptables 规则的脚本:

ipset create bypass_clash hash:net
ipset add bypass_clash 0.0.0.0/8
ipset add bypass_clash 10.0.0.0/8
ipset add bypass_clash 100.64.0.0/10
ipset add bypass_clash 127.0.0.0/8
ipset add bypass_clash 169.254.0.0/16
ipset add bypass_clash 172.16.0.0/12
ipset add bypass_clash 192.0.0.0/24
ipset add bypass_clash 192.0.2.0/24
ipset add bypass_clash 192.88.99.0/24
ipset add bypass_clash 192.168.0.0/16
#ipset add bypass_clash 198.18.0.0/15
ipset add bypass_clash 198.51.100.0/24
ipset add bypass_clash 203.0.113.0/24
ipset add bypass_clash 224.0.0.0/3

ip rule add fwmark 0x233 table 100
ip route add local default dev lo table 100

iptables -t mangle -N clash
# 忽略fake-ip之外的保留地址
iptables -t mangle -A clash -m set --match-set bypass_clash dst -j RETURN
iptables -t mangle -A clash -p udp -s 192.168.80.9 -j RETURN
iptables -t mangle -A clash -p tcp -s 192.168.80.9 -j RETURN
iptables -t mangle -A clash -p tcp -j TPROXY --on-ip 127.0.0.1 --on-port 10000 --tproxy-mark 0x233
iptables -t mangle -A clash -p udp -j TPROXY --on-ip 127.0.0.1 --on-port 10000 --tproxy-mark 0x233
iptables -t mangle -A PREROUTING -p tcp -j clash
iptables -t mangle -A PREROUTING -p udp -j clash

# 分流已连接的请求,优化tproxy性能
iptables -t mangle -N tproxy_divert
iptables -t mangle -A tproxy_divert -j MARK --set-mark 0x233
iptables -t mangle -A tproxy_divert -j ACCEPT
iptables -t mangle -I PREROUTING -p tcp -m socket -j tproxy_divert

# 避免直接发往透明代理端口导致死循环
iptables -A INPUT -p tcp --dport 10000 -m mark ! --mark 0x233 -j REJECT
iptables -A INPUT -p udp --dport 10000 -m mark ! --mark 0x233 -j REJECT

出站

早期也重定向了主路由本机的出站流量,目前不在用了,但为了内容完整性还是写一下。

将出站流量打上入站中同样的标记,使其路由进入本地回环:

iptables -t mangle -N clash_out
# 过滤发向保留地址的
iptables -t mangle -A clash_out -m set --match-set bypass_clash dst -j RETURN
# 给出站流量打标记,之后与入站重定向同理
iptables -t mangle -A clash_out -j MARK --set-mark 0x233

iptables -t mangle -A OUTPUT -j clash_out

接下来还需要避免 Clash 的出站流量被重定向入站,造成死循环。

一种方式是根据用户做区分,需要安装 shadow-useradd 和 iptables owner 扩展(包含在 iptables-mod-extra 包中),然后用单独的用户运行 Clash。假设是 clashuser

iptables -t mangle -I clash_out -m owner --uid-owner clashuser -j RETURN

后来发现 Clash 提供了配置项 routing-mark 标记出站流量,这更方便。假设 Clash 的标记是 666

iptables -t mangle -A OUTPUT -m mark ! --mark 666 -j clash_out

DNS

OpenWRT 配置:

  • 「DNS 转发」中设置 Clash 为 Dnsmasq 的上游
  • 选中「忽略解析文件」,否则会有原先上游的干扰

Clash 关闭 IPv6、设置端口、使用 fake-IP 模式:

dns:
  enable: true
  ipv6: false
  listen: 0.0.0.0:5353
  default-nameserver:
    - 8.8.8.8
    - 223.5.5.5
  enhanced-mode: fake-ip
  fake-ip-range: 198.18.0.1/16
  ...

fake-IP

fake-IP 模式下 Clash 为域名分配一个假 IP(DNS TTL 为 1 避免被客户端缓存)。优点是部分情况下可以节省发向上游 DNS 服务的查询,如果域名规则合理的话也可以避免 DNS 泄漏。

为了让 GEO DNS 分配合理的节点、避免客户端网络环境中的 DNS 污染,代理服务器往往会使用自己网络环境下的的解析结果。所以客户端代理工具会做优化,例如 Clash 在遇到基于 IP 的规则(不带 no-resolve 选项的 IP 、SCRIPT 等类型规则)之前不需要解析域名,命中代理规则的话就直接发给代理服务器。

在非 fake-IP 模式下,即使有上述优化,也必须解析域名来获得一个让客户端发起连接的 IP。

但在 fake-IP 模式下是立即返回一个假 IP,并记录域名和假 IP 的映射关系。客户端以此 IP 为目的地址发起请求,Clash 捕获到该 IP 的请求后根据对应关系获取原始域名,然后进行规则匹配。在没有遇到第一个基于 IP 的规则前,都不需要解析。

Clash 文档中以请求 google.com 为例:

$ curl -v http://google.com
<---- cURL asks your system DNS (Clash) about the IP address of google.com
----> Clash decided 198.18.1.70 should be used as google.com and remembers it
*   Trying 198.18.1.70:80...
<---- cURL connects to 198.18.1.70 tcp/80
----> Clash will accept the connection immediately, and..
* Connected to google.com (198.18.1.70) port 80 (#0)
----> Clash looks up in its memory and found 198.18.1.70 being google.com
----> Clash looks up in the rules and sends the packet via the matching outbound

推荐阅读浅谈在代理环境中的 DNS 解析行为

分流

所有流量都经过 Clash,分流规则决定了用网体验。我不喜欢那种堆了大几万条域名、IP 的规则集。规则匹配时的性能消耗还好说,主要是条目太多没法审计,稳定性完全依赖维护者,可能哪天规则自动更新就打乱了习惯。

我更推荐先用关键字、后缀等粗粒度规则筛出自己用网习惯下的高频域名,比如 foreign。让这些高频域名用上 fake-IP 的优势,剩下的用 GEOIP 兜底。

proxy-groups:
  - name: Proxy
    type: select
    proxies:
      - xxx
    use:
      - xxx

rules:
  - DOMAIN-SUFFIX,googleapis.cn,Proxy
  - DOMAIN-SUFFIX,googleapis.com,Proxy
  ....
  - GEOIP,CN,DIRECT
  - MATCH,Proxy

兜底规则非常依赖 GEOIP 库的准确度,但 Clash 内置 MaxMind GeoLite2 的大陆地区 IP 准确度存疑,所以我替换为综合了 ipip.net 和纯真数据的 Hackl0us/GeoIP2-CN。这个项目每三天更新一次数据,可以写个定时任务替换本地的 Country.mmdb 文件,但这个文件不会热重载,需要重启 Clash。

1% 的不适用场景

代理工具转发流量的基本相同:劫持客户端流量后,分别与两端建立连接,然后在中间做转发。这会破坏对网络要求苛刻的场景,例如端口扫描时 Nmap 发出的 TCP SYN 其实是被代理工具响应,不是实际目标,误导 Nmap 认为所有探测端口都开放。

并且代理工具一般工作在第四层,只处理 TCP/UDP。所以默认基于 ICMP 协议的 traceroute 和 ping 等工具也得换成支持 TCP/UDP 的版本。

目前看来更理想的方式是给数据套一层 L3 VPN 再走代理,只是不知道多一层 NAT 和 UDP-in-TCP 会造成多大的损耗。