Skip to content

[v1.0] Tun+MITMProxy 初探

Published: at 05:55

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

香不香啊,太香了这个
香不香啊,太香了这个

嘛,总之是配置完了,现在一切就都正常多了,整体的架构也很符合直觉。当然了,还有一些比较 dirty 的实现需要后期修正,那就是之后的事了(逃

当然,这些这是后话了。话说这一切,都要从……


目标?

其实原来目标很简单。本机能够实现流畅访问互联网。透明代理真的太快乐了

然后后来就开始慢慢加戏了。你说本机流畅了,那 create_ap 的热点呢?这个需求是解决了,但如果我要深度去广告怎么办?这又涉及到 MITM 和脚本的问题。还不是给刘大爷惯的

于是,最终的目标就成型了。我们的目标必须满足下列要求:

方案 -2.0

最开始我使用的方案是旧白话文里的普通配置,这个配置最大的优点就是简单。我们这里不妨摸一份过来(有修改):

Terminal window
1
# TCP
2
iptables -t nat -N RUA # 新建一个名为 RUA 的链
3
iptables -t nat -A RUA -d 192.168.0.0/16 -j RETURN # 直连 192.168.0.0/16
4
iptables -t nat -A RUA -p tcp -m mark --mark 0xff -j RETURN # 直连 SO_MARK 为 0xff 的流量(0xff 是 16 进制数,数值上等同与上面配置的 255),此规则目的是避免代理本机(网关)流量出现回环问题
5
iptables -t nat -A RUA -p tcp -j REDIRECT --to-ports 12345 # 其余流量转发到 12345 端口(即 RUA)
6
iptables -t nat -A PREROUTING -p tcp -j RUA # 局域网其他设备
7
iptables -t nat -A OUTPUT -p tcp -j RUA # 本机
8
9
# UDP
10
ip rule add fwmark 1 table 100
11
ip route add local 0.0.0.0/0 dev lo table 100
12
iptables -t mangle -N RUA_MASK
13
iptables -t mangle -A RUA_MASK -d 192.168.0.0/16 -j RETURN
14
iptables -t mangle -A RUA_MASK -p udp -j TPROXY --on-port 12345 --tproxy-mark 1
15
iptables -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(原文)。

你去用 ip6tables 啊 kora!

方案 -1.0

这个版本的方案就是新白话文中的 TProxy 了,这也是上一个方案中处理 UDP 的做法,就一并放到这里讲好了。

TProxy 的详细原理在我后来写的这篇文章中有详细解释,感兴趣的不妨去看看(

TProxy 的方案非常优秀,整个过程中没有任何问题,这也是我使用时间最长的方案,将近一年了吧,但最后我还是放弃了这个方案主要的原因其实并不是它的过错,而是 JSON 的配置格式极大地限制了整个文档的修改与扩充。于是,在第114513次尝试扩充之后,我放弃了。

方案 0.0

这时候我遇到了 clashyaml 的格式很对我胃口,类 Surge 的配置方式也很顺眼(谁让我几个月前刚买了一年订阅呢)。正好,有新的 PR 支持了 Tun,虽然并不是太稳定,但也能用了不是,于是我就准备尝试一下。

这个过程持续了大约两个月的时间,使用的脚本是 AUR clash-tun 的个人修改版本,期间给 PR 解决了一个 [bug](https://github.com/Dreamacro/clash/pull/393#discussion_r379273286)。但是这个方案有一个最大的问题:热点共享失效了。

现在回过头来看其实原因很简单,是动了 PREROUTING 的奶酪,但这个问题着实困扰了我长大一个月的时间,期间各种补课,总算理解了整个过程。

1
#!/bin/bash
2
3
TUN_USER="root"
4
TUN_DEVICE="utun"
5
PROXY_FWMARK="0x162"
6
TUN_ROUTE_TABLE="0x162"
7
TUN_ADDRESS="198.18.0.1/16"
8
CREATE_AP_ADDR="192.168.114.0/24"
9
10
/srv/clash/scripts/clean.sh
11
12
# IPv4 环回地址
13
ipset create localnetwork hash:net
14
ipset add localnetwork 127.0.0.0/8
15
ipset add localnetwork "$CREATE_AP_ADDR"
16
17
# IPv6 环回地址
18
ipset create localnetwork6 hash:net -6
19
ipset add localnetwork6 fe80::/10
20
21
# 启动 Tun
22
ip tuntap add "$TUN_DEVICE" mode tun user $TUN_USER
23
ip link set "$TUN_DEVICE" up
24
25
# 将对应 IP 段分配给 TUN
26
ip address replace "$TUN_ADDRESS" dev "$TUN_DEVICE"
27
28
# 给 TUN 设备增加路由
29
ip route replace default dev "$TUN_DEVICE" table "$TUN_ROUTE_TABLE"
30
ip -6 route replace default dev "$TUN_DEVICE" table "$TUN_ROUTE_TABLE"
31
32
# 对带有 fwmark 的流量走 TUN_ROUTE_TABLE 路由表
33
ip rule add fwmark "$PROXY_FWMARK" lookup "$TUN_ROUTE_TABLE"
34
ip -6 rule add fwmark "$PROXY_FWMARK" lookup "$TUN_ROUTE_TABLE"
35
36
# IPv4 Clash 链
37
iptables -t mangle -N CLASH # 创建 CLASH 链
38
iptables -t mangle -A CLASH -m owner --uid-owner "$TUN_USER" -j RETURN # 绕过指定用户
39
iptables -t mangle -A CLASH -d "$TUN_ADDRESS" -j MARK --set-mark "$PROXY_FWMARK" # 将目标为对应 IP 段的流量打上标记
40
iptables -t mangle -A CLASH -d "$CREATE_AP_ADDR" -j RETURN # 绕过 create_ap 地址
41
iptables -t mangle -A CLASH -m addrtype --dst-type BROADCAST -j RETURN # 绕过广播地址
42
iptables -t mangle -A CLASH -m set --match-set localnetwork dst -j RETURN # 绕过本地地址
43
iptables -t mangle -A CLASH -p tcp -j MARK --set-mark "$PROXY_FWMARK" # 给 TCP 连接打上标记
44
#iptables -t mangle -A CLASH -p udp -j MARK --set-mark "$PROXY_FWMARK" # 给 UDP 连接打上标记
45
iptables -t mangle -A CLASH -p udp -j RETURN
46
47
# IPv6 Clash 链
48
ip6tables -t mangle -N CLASH6 # 创建 CLASH6 链
49
ip6tables -t mangle -A CLASH6 -m owner --uid-owner "$TUN_USER" -j RETURN # 绕过指定用户
50
ip6tables -t mangle -A CLASH6 -m set --match-set localnetwork6 dst -j RETURN # 绕过本地地址
51
ip6tables -t mangle -A CLASH6 -p tcp -j MARK --set-mark "$PROXY_FWMARK" # 给 TCP 连接打上标记
52
#ip6tables -t mangle -A CLASH6 -p udp -j MARK --set-mark "$PROXY_FWMARK" # 给 UDP 连接打上标记
53
ip6tables -t mangle -A CLASH6 -p udp -j RETURN
54
55
iptables -t mangle -I OUTPUT -j CLASH # IPv4 出口使用 CLASH 链
56
#iptables -t mangle -I PREROUTING -m set ! --match-set localnetwork dst -j MARK --set-mark "$PROXY_FWMARK" # 在 PREROUTING 给所有非本地流量打上标记
57
58
ip6tables -t mangle -I OUTPUT -j CLASH6 # IPv6 出口使用 CLASH 链
59
#ip6tables -t mangle -I PREROUTING -m set ! --match-set localnetwork6 dst -j MARK --set-mark "$PROXY_FWMARK" # 在 PREROUTING 给所有非本地流量打上标记
60
61
iptables -t nat -A PREROUTING -p tcp -j REDIRECT --to-ports 7892
62
63
# REJECT 对应 IP 的 Output
64
iptables -t filter -I OUTPUT -d "$TUN_ADDRESS" -j REJECT

// TODO: 有空补充一下为什么会炸

MITM?

MITM 的想法是在使用 Surge 脚本的时候浮现出来的,简单来说就是可以改请求改返回之类。由于脚本的功能非常强大,因此我一直想怎么才能在本机上实现这个功能。

在不需要自己手动实现的前提下,我很快就瞄准了 mitmproxy。正好,mitmproxy 也有 upstream 模式。于是在群友的帮助下,我确定了配置的基本架构:

字丑点丑点吧(悲)
字丑点丑点吧(悲)

为了简化设计,在 TUN -> MITMProxy 的中间件选择上,我使用了 clash 的 TUN 模式。于是——

方案 1.0

总之话不多说,先上配置好了。首先是第一层的配置:

1
# mitmproxy
2
port: 1081
3
socks-port: 1080
4
5
allow-lan: true
6
bind-address: "*"
7
redir-port: 7892
8
9
mode: Rule
10
11
log-level: info
12
external-controller: :9090
13
14
tun:
15
enable: true
16
17
dns:
18
enable: true
19
ipv6: true
20
listen: 0.0.0.0:53
21
enhanced-mode: redir-host
22
nameserver:
23
- 223.5.5.5
24
fallback:
25
- 8.8.8.8
26
27
Proxy:
28
- name: "mitmproxy"
29
type: http
30
server: localhost
31
port: 20020
32
33
- name: "clash"
34
type: socks5
35
server: localhost
36
port: 11080
37
udp: true
38
39
Rule:
40
# mitmproxy
41
- DOMAIN,mitm.it,mitmproxy
42
# no-mitm
43
- MATCH,clash

先来解释这一层吧。这一层是需要权限的一层,在启动这一层的时候需要执行 iptables 命令,正好和 53 端口分配重合了,于是就放在一起了。但事实证明,如果不放在同一个配置文件里,则会导致整个系统无法正常运行,原因未知。

(个人感觉是因为 TUN 要处理 IP<->Domain,因此 DNS 和 TUN 必须在同一层,有待验证。)

在这里,我们定义了两个规则:一个是需要 MITM 的,默认的 mitm.it 肯定是要的;接下来就是默认的规则,负责处理那些不需要经手的规则,通过前置代理直接送到下一层对应的 socks5 端口。

接下来是之前各个解决方案的本体,也是最终处理链接的层次:

1
# self
2
port: 11081
3
socks-port: 11080
4
5
mode: Rule
6
7
log-level: info
8
external-controller: :19090
9
10
Proxy:
11
- ...
12
13
Rule:
14
# optional param "no-resolve" for IP rules (GEOIP IP-CIDR)
15
- IP-CIDR,192.168.0.0/16,DIRECT
16
- IP-CIDR,127.0.0.0/8,DIRECT
17
- GEOIP,CN,DIRECT
18
- MATCH,custom-rule

这一层就没什么好说的了。

展望

回顾整个过程,从想法,到思维误区,到跳出习惯的舒服,我学到了 iptables 的基本规则和使用方法,Linux 下 TunTap 的一些常识,并且对网络的认识进一步加深了。

现在的方案存在一定的问题,首先就是 DNS 的问题,需要将所有发往 53 端口的DNS 请求都修改为发送至本机 DNS,换句话说就是 DNS 交给 clash 处理;其次是用户权限,root 还是太大了,在非 root 的情况下其实很多事情也能完成;最后就是这两层能不能相对融合?我什么时候造轮子?(逃

总之,路漫漫其修远兮,坑还有114514个。最近还在讨论 PUG 的设计,总之肯定是不会闲下来的(笑)

那暂时就这样,おやすみなせ〜