Skip to content

[v1.0] Tun+MITMProxy 初探

Published: at 05:55

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

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

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

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


目标?

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

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

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

方案 -2.0

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

Terminal window
# TCP
iptables -t nat -N RUA # 新建一个名为 RUA 的链
iptables -t nat -A RUA -d 192.168.0.0/16 -j RETURN # 直连 192.168.0.0/16
iptables -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 # 本机
# UDP
ip rule add fwmark 1 table 100
ip route add local 0.0.0.0/0 dev lo table 100
iptables -t mangle -N RUA_MASK
iptables -t mangle -A RUA_MASK -d 192.168.0.0/16 -j RETURN
iptables -t mangle -A RUA_MASK -p udp -j TPROXY --on-port 12345 --tproxy-mark 1
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 的奶酪,但这个问题着实困扰了我长大一个月的时间,期间各种补课,总算理解了整个过程。

#!/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:net
ipset add localnetwork 127.0.0.0/8
ipset add localnetwork "$CREATE_AP_ADDR"
# IPv6 环回地址
ipset create localnetwork6 hash:net -6
ipset add localnetwork6 fe80::/10
# 启动 Tun
ip tuntap add "$TUN_DEVICE" mode tun user $TUN_USER
ip link set "$TUN_DEVICE" up
# 将对应 IP 段分配给 TUN
ip 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 的 Output
iptables -t filter -I OUTPUT -d "$TUN_ADDRESS" -j REJECT

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

MITM?

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

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

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

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

方案 1.0

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

# mitmproxy
port: 1081
socks-port: 1080
allow-lan: true
bind-address: "*"
redir-port: 7892
mode: Rule
log-level: info
external-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 端口。

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

# self
port: 11081
socks-port: 11080
mode: Rule
log-level: info
external-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 的设计,总之肯定是不会闲下来的(笑)

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


Previous Post
*nix 权限初探
Next Post
从无法创建的 5GHz 热点说开去