昨天今天花了一天的时间,终于把网络环境配好了。当然了,中间还有一些杂七杂八的事情暂且不提(但是真的很舒服啊,比如下面这个,我好了)——

嘛,总之是配置完了,现在一切就都正常多了,整体的架构也很符合直觉。当然了,还有一些比较 dirty 的实现需要后期修正,那就是之后的事了(逃
当然,这些这是后话了。话说这一切,都要从……
目标?
其实原来目标很简单。本机能够实现流畅访问互联网。
然后后来就开始慢慢加戏了。你说本机流畅了,那 create_ap
的热点呢?这个需求是解决了,但如果我要深度去广告怎么办?这又涉及到 MITM 和脚本的问题。
于是,最终的目标就成型了。我们的目标必须满足下列要求:
- 能使本机流畅访问互联网
- 能使连接了本机通过 create_ap 共享的网络的设备流畅访问互联网
- 能够根据简单的域名规则屏蔽广告
- 能够根据复杂的脚本规则屏蔽广告
方案 -2.0
最开始我使用的方案是旧白话文里的普通配置,这个配置最大的优点就是简单。我们这里不妨摸一份过来(有修改):
# TCPiptables -t nat -N RUA # 新建一个名为 RUA 的链iptables -t nat -A RUA -d 192.168.0.0/16 -j RETURN # 直连 192.168.0.0/16iptables -t nat -A RUA -p tcp -m mark --mark 0xff -j RETURN # 直连 SO_MARK 为 0xff 的流量(0xff 是 16 进制数,数值上等同与上面配置的 255),此规则目的是避免代理本机(网关)流量出现回环问题iptables -t nat -A RUA -p tcp -j REDIRECT --to-ports 12345 # 其余流量转发到 12345 端口(即 RUA)iptables -t nat -A PREROUTING -p tcp -j RUA # 局域网其他设备iptables -t nat -A OUTPUT -p tcp -j RUA # 本机
# UDPip rule add fwmark 1 table 100ip route add local 0.0.0.0/0 dev lo table 100iptables -t mangle -N RUA_MASKiptables -t mangle -A RUA_MASK -d 192.168.0.0/16 -j RETURNiptables -t mangle -A RUA_MASK -p udp -j TPROXY --on-port 12345 --tproxy-mark 1iptables -t mangle -A PREROUTING -p udp -j RUA_MASK
这种方案的本质是 NAT
。我们这里只讨论 TCP 部分:对于局域网内的流量,我们希望能够直接连接,因此我们让 192.168.0.0/16
的流量 RETURN
;对于任意的 TCP 流量,我们希望它能够默认被送至 dokodemo
,但又不希望从 dokodemo
出来的流量再被送回 dokodemo
,于是我们给它打上 fwmark
;对于剩下的流量,我们就把它送到 dokodemo
对应的端口,也就是文中对应的 12345。
最后,我们需要让这条链(也可以说是我们定义的规则组)运作。我们需要把它加到两条链里:nat
表的 PREROUTING
链和 OUTPUT
链,前者是转发报文处理过程中的一环,而后者则是本机发出报文的必经之路。
经过这样的处理之后,无论是对于本机,又或是外部设备,访问互联网都是透明的了。那这种方案有什么问题呢?
其实问题是相对主观的。这个版本的配置我也用了一段时间,但最后还是换成了下一个版本的,其中最关键的因素就是速度。虽然没有经过 benchmark
,但从个人的体感速度上来说是不如下一个版本的。另外,NAT 不支持 IPv6(原文)。
方案 -1.0
这个版本的方案就是新白话文中的 TProxy 了,这也是上一个方案中处理 UDP 的做法,就一并放到这里讲好了。
TProxy 的详细原理在我后来写的这篇文章中有详细解释,感兴趣的不妨去看看(
TProxy 的方案非常优秀,整个过程中没有任何问题,这也是我使用时间最长的方案,将近一年了吧,但最后我还是放弃了这个方案主要的原因其实并不是它的过错,而是 JSON
的配置格式极大地限制了整个文档的修改与扩充。于是,在第114513次尝试扩充之后,我放弃了。
方案 0.0
这时候我遇到了 clash。yaml
的格式很对我胃口,类 Surge 的配置方式也很顺眼(谁让我几个月前刚买了一年订阅呢)。正好,有新的 PR 支持了 Tun
,虽然并不是太稳定,但也能用了不是,于是我就准备尝试一下。
这个过程持续了大约两个月的时间,使用的脚本是 AUR
clash-tun
的个人修改版本,期间给 PR
解决了一个 [bug](https://github.com/Dreamacro/clash/pull/393#discussion_r379273286)
。但是这个方案有一个最大的问题:热点共享失效了。
现在回过头来看其实原因很简单,是动了 PREROUTING 的奶酪,但这个问题着实困扰了我长大一个月的时间,期间各种补课,总算理解了整个过程。
#!/bin/bash
TUN_USER="root"TUN_DEVICE="utun"PROXY_FWMARK="0x162"TUN_ROUTE_TABLE="0x162"TUN_ADDRESS="198.18.0.1/16"CREATE_AP_ADDR="192.168.114.0/24"
/srv/clash/scripts/clean.sh
# IPv4 环回地址ipset create localnetwork hash:netipset add localnetwork 127.0.0.0/8ipset add localnetwork "$CREATE_AP_ADDR"
# IPv6 环回地址ipset create localnetwork6 hash:net -6ipset add localnetwork6 fe80::/10
# 启动 Tunip tuntap add "$TUN_DEVICE" mode tun user $TUN_USERip link set "$TUN_DEVICE" up
# 将对应 IP 段分配给 TUNip address replace "$TUN_ADDRESS" dev "$TUN_DEVICE"
# 给 TUN 设备增加路由ip route replace default dev "$TUN_DEVICE" table "$TUN_ROUTE_TABLE"ip -6 route replace default dev "$TUN_DEVICE" table "$TUN_ROUTE_TABLE"
# 对带有 fwmark 的流量走 TUN_ROUTE_TABLE 路由表ip rule add fwmark "$PROXY_FWMARK" lookup "$TUN_ROUTE_TABLE"ip -6 rule add fwmark "$PROXY_FWMARK" lookup "$TUN_ROUTE_TABLE"
# IPv4 Clash 链iptables -t mangle -N CLASH # 创建 CLASH 链iptables -t mangle -A CLASH -m owner --uid-owner "$TUN_USER" -j RETURN # 绕过指定用户iptables -t mangle -A CLASH -d "$TUN_ADDRESS" -j MARK --set-mark "$PROXY_FWMARK" # 将目标为对应 IP 段的流量打上标记iptables -t mangle -A CLASH -d "$CREATE_AP_ADDR" -j RETURN # 绕过 create_ap 地址iptables -t mangle -A CLASH -m addrtype --dst-type BROADCAST -j RETURN # 绕过广播地址iptables -t mangle -A CLASH -m set --match-set localnetwork dst -j RETURN # 绕过本地地址iptables -t mangle -A CLASH -p tcp -j MARK --set-mark "$PROXY_FWMARK" # 给 TCP 连接打上标记#iptables -t mangle -A CLASH -p udp -j MARK --set-mark "$PROXY_FWMARK" # 给 UDP 连接打上标记iptables -t mangle -A CLASH -p udp -j RETURN
# IPv6 Clash 链ip6tables -t mangle -N CLASH6 # 创建 CLASH6 链ip6tables -t mangle -A CLASH6 -m owner --uid-owner "$TUN_USER" -j RETURN # 绕过指定用户ip6tables -t mangle -A CLASH6 -m set --match-set localnetwork6 dst -j RETURN # 绕过本地地址ip6tables -t mangle -A CLASH6 -p tcp -j MARK --set-mark "$PROXY_FWMARK" # 给 TCP 连接打上标记#ip6tables -t mangle -A CLASH6 -p udp -j MARK --set-mark "$PROXY_FWMARK" # 给 UDP 连接打上标记ip6tables -t mangle -A CLASH6 -p udp -j RETURN
iptables -t mangle -I OUTPUT -j CLASH # IPv4 出口使用 CLASH 链#iptables -t mangle -I PREROUTING -m set ! --match-set localnetwork dst -j MARK --set-mark "$PROXY_FWMARK" # 在 PREROUTING 给所有非本地流量打上标记
ip6tables -t mangle -I OUTPUT -j CLASH6 # IPv6 出口使用 CLASH 链#ip6tables -t mangle -I PREROUTING -m set ! --match-set localnetwork6 dst -j MARK --set-mark "$PROXY_FWMARK" # 在 PREROUTING 给所有非本地流量打上标记
iptables -t nat -A PREROUTING -p tcp -j REDIRECT --to-ports 7892
# REJECT 对应 IP 的 Outputiptables -t filter -I OUTPUT -d "$TUN_ADDRESS" -j REJECT
// TODO: 有空补充一下为什么会炸
MITM?
MITM 的想法是在使用 Surge 脚本的时候浮现出来的,简单来说就是可以改请求改返回之类。由于脚本的功能非常强大,因此我一直想怎么才能在本机上实现这个功能。
在不需要自己手动实现的前提下,我很快就瞄准了 mitmproxy。正好,mitmproxy
也有 upstream
模式。于是在群友的帮助下,我确定了配置的基本架构:

为了简化设计,在 TUN -> MITMProxy 的中间件选择上,我使用了 clash 的 TUN 模式。于是——
方案 1.0
总之话不多说,先上配置好了。首先是第一层的配置:
# mitmproxyport: 1081socks-port: 1080
allow-lan: truebind-address: "*"redir-port: 7892
mode: Rule
log-level: infoexternal-controller: :9090
tun: enable: true
dns: enable: true ipv6: true listen: 0.0.0.0:53 enhanced-mode: redir-host nameserver: - 223.5.5.5 fallback: - 8.8.8.8
Proxy: - name: "mitmproxy" type: http server: localhost port: 20020
- name: "clash" type: socks5 server: localhost port: 11080 udp: true
Rule: # mitmproxy - DOMAIN,mitm.it,mitmproxy # no-mitm - MATCH,clash
先来解释这一层吧。这一层是需要权限的一层,在启动这一层的时候需要执行 iptables
命令,正好和 53 端口分配重合了,于是就放在一起了。但事实证明,如果不放在同一个配置文件里,则会导致整个系统无法正常运行,原因未知。
(个人感觉是因为 TUN 要处理 IP<->Domain,因此 DNS 和 TUN 必须在同一层,有待验证。)
在这里,我们定义了两个规则:一个是需要 MITM 的,默认的 mitm.it 肯定是要的;接下来就是默认的规则,负责处理那些不需要经手的规则,通过前置代理直接送到下一层对应的 socks5 端口。
接下来是之前各个解决方案的本体,也是最终处理链接的层次:
# selfport: 11081socks-port: 11080
mode: Rule
log-level: infoexternal-controller: :19090
Proxy: - ...
Rule: # optional param "no-resolve" for IP rules (GEOIP IP-CIDR) - IP-CIDR,192.168.0.0/16,DIRECT - IP-CIDR,127.0.0.0/8,DIRECT - GEOIP,CN,DIRECT - MATCH,custom-rule
这一层就没什么好说的了。
展望
回顾整个过程,从想法,到思维误区,到跳出习惯的舒服,我学到了 iptables
的基本规则和使用方法,Linux 下 TunTap 的一些常识,并且对网络的认识进一步加深了。
现在的方案存在一定的问题,首先就是 DNS 的问题,需要将所有发往 53 端口的DNS 请求都修改为发送至本机 DNS,换句话说就是 DNS 交给 clash 处理;其次是用户权限,root 还是太大了,在非 root 的情况下其实很多事情也能完成;最后就是这两层能不能相对融合?我什么时候造轮子?(逃
总之,路漫漫其修远兮,坑还有114514个。最近还在讨论 PUG 的设计,总之肯定是不会闲下来的(笑)
那暂时就这样,おやすみなせ〜