linux 透明代理原理

两年前在自己的 linux 设备上边理解边摆弄过透明代理,本来觉得没什么值得记录的。最近买了 steam deck, 给它搞透明代理的过程中忽然发现有很多细节都忘记了,在这里小记一笔。

装 Clash

1
2
#安装 clash-meta:
nix-env -iA nixpkgs.clash-meta

透明代理

除了 Tun 以外,Linux 通过 iptables 有两种透明代理的机制, REDIRECT 和 TPROXY.

REDIRECT

原理是将包的 DESTINATION IP/PORT 修改为本机上代理监听的 IP/PORT。文档里有介绍:

https://www.frozentux.net/iptables-tutorial/iptables-tutorial.html?utm_source=pocket_saves#REDIRECTTARGET

但包的目标地址被改了,代理如何得知原始目标地址呢?对于 TCP 连接来说,可以读取 socket option SO_ORIGINAL_DST获得。

著名的服务网格 Linkerd 就使用这一机制从 sidecar 代理进出 Pod 的流量。

但对于 UDP 流量来说就没有这一选项了。

TPROXY

TPROXY 本身的文档有个很好的例子。
https://www.kernel.org/doc/html/latest/networking/tproxy.html

原理是在本机 iptables mangle 为需要代理的包打一个 mark.

1
iptables -t mangle -A DIVERT -j MARK --set-mark 1

这个 mark 是 netfilter policy routing 的一个机制,它不在实际包当中,只是本机进行流量控制打的一个标记。

关于 MARK target: https://www.frozentux.net/iptables-tutorial/iptables-tutorial.html?utm_source=pocket_saves#MARKTARGET

接下来利用 policy routing 把带有 mark 的流量 route 到 loopback 设备,

1
2
# ip rule add fwmark 1 lookup 100
# ip route add local 0.0.0.0/0 dev lo table 100

关于 policy routing: https://theplant.slack.com/archives/C02KZTUKBH8/p1645587411325009

然后在 TPROXY 中把这些接受到的流量发送给代理监听的端口即可。

1
2
iptables -t mangle -A PREROUTING -p tcp --dport 80 -j TPROXY \
--tproxy-mark 0x1/0x1 --on-port 50080

解决回环

在实际使用中会遇到一个问题,即一个本来要发出去的包发给代理,代理又要发出去的时候,如果不能区分,可能又会发回给代理。

一个解决方法是,让代理以一个专门的 linux user 运行,这样代理发出的流量都能通过 iptables 的 --uid-owner 来识别。只要先把 uid 为代理的包先 RETURN 即可。

一个简单例子

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

#!/bin/bash

[ -z "$(ip ro show tab 100)" ] && ip route add local default dev lo table 100
[ -z "$(ip ru show from all fwmark 0x1)" ] && ip rule add fwmark 1 lookup 100

iptables -t mangle -S clash 2&>1 >/dev/null
[ "$?" != 0 ] && iptables -t mangle -N clash
iptables -t mangle -S clash_out 2&>1 >/dev/null
[ "$?" != 0 ] && iptables -t mangle -N clash_out
iptables -t nat -S clash_dns 2&>1 >/dev/null
[ "$?" != 0 ] && iptables -t nat -N clash_dns

# wifi/蓝牙网络连接
[ -z "$(iptables -t mangle -S OUTPUT | grep clash)" ] && iptables -t mangle -I OUTPUT -o wlp2s0 -j clash_out && iptables -t mangle -I OUTPUT -o bnep0 -j clash_out
[ -z "$(iptables -t mangle -S PREROUTING | grep clash)" ] && iptables -t mangle -I PREROUTING -j clash

iptables -t mangle -F clash
iptables -t mangle -F clash_out
iptables -t mangle -A clash_out -m owner --uid-owner clash -j RETURN

iptables -t mangle -A clash_out -j MARK --set-mark 1 -p tcp
iptables -t mangle -A clash -j TPROXY --tproxy-mark 0x1/0x1 -p tcp --on-port 8891
iptables -t mangle -A clash_out -j MARK --set-mark 1 -p udp
iptables -t mangle -A clash -j TPROXY --tproxy-mark 0x1/0x1 -p udp --on-port 8891

# DNS 请求 redirect 到 clash
[ -z "$(iptables -t nat -S OUTPUT | grep clash_dns)" ] && iptables -t nat -A OUTPUT -p udp --dport 53 -j clash_dns

iptables -t nat -F clash_dns
iptables -t nat -A clash_dns -m owner --uid-owner clash -j RETURN
iptables -t nat -A clash_dns -p udp --dport 53 -j REDIRECT --to-ports 1053

Clash systemd, 以单独用户启动及相关权限:

1
2
3
4
5
6
7
8
9
10
11
12
13
[Unit]
Description=clash
After=network.target

[Service]
User=clash
Group=clash
AmbientCapabilities=CAP_NET_BIND_SERVICE CAP_NET_ADMIN
ExecStart=/home/clash/.nix-profile/bin/clash -d /home/clash/.config/clash
Restart=on-failure

[Install]
WantedBy=multi-user.target