转载自:https://www.arloor.com/posts/clash-tpproxy/

clash透明代理可以使用ShellClash,这里记录下其中的核心技术,掌握核心技术才好。

前言

一开始我使用Clash tun模式,遇到的最大问题是dns污染。后面看到了Shell Clash看到了使用redir和nftables相关的东西。

利用iptables-redirct来做透明代理

clash配置

  1. 关闭tun
  2. 使用dns的fallback配置来避免dns污染相关问题
mixed-port: 7890
redir-port: 7892
authentication: [""]
allow-lan: true
mode: Rule
log-level: info
ipv6: false
external-controller: :9999
external-ui: ui
secret: 
tun: {enable: false}
experimental: {ignore-resolve-fail: true, interface-name: en0}
dns: {enable: true, ipv6: false, listen: 0.0.0.0:1053, use-hosts: true, enhanced-mode: redir-host, default-nameserver: [114.114.114.114, 223.5.5.5, 127.0.0.1:53], nameserver: [114.114.114.114, 223.5.5.5], fallback: [1.0.0.1, 8.8.4.4], fallback-filter: {geoip: true}}

store-selected: true
hosts:
   'localhost': 127.0.0.1

使用iptables-redirct规则

img
Project X透明代理文档

关键摘要:

  1. 为非网关设备:控制PREROUTING,将非网关的其他设备redirect到clash的redir端口上
  2. 为网关自己:控制OUTPUT链,将网关本身发出的流量也走到clash中,并重新走一次PREROUTING一边进行透明代理
  3. 因为使用的是redirect模式,所以需要打开内核的ip_forward特性。(iptables-tpproxy则需要根据fwmark进行增加ip route

开启内核的ip_forward

sed -i '/^net.ipv4.ip_forward=0/'d /etc/sysctl.conf
sed -n '/^net.ipv4.ip_forward=1/'p /etc/sysctl.conf | grep -q "net.ipv4.ip_forward=1"
if [ $? -ne 0 ]; then
    echo -e "net.ipv4.ip_forward=1" >> /etc/sysctl.conf && sysctl -p
fi

nftables的redirect(以下只代理tcp,不代理udp)

cat > /lib/systemd/system/con.service <<EOF
[Unit]
Description=clash
After=network.target

[Service]
Type=simple
User=root
ExecStartPre=nft flush ruleset
ExecStart=/bin/su shellclash -c "/opt/con/clashnet -d /opt/con/"
ExecStartPost=nft -f /etc/nftables/nftables-redirect-clash.nft
Restart=on-failure
RestartSec=3s
LimitNOFILE=999999


[Install]
WantedBy=multi-user.target
EOF
mkdir /etc/nftables
cat > /etc/nftables/nftables-redirect-clash.nft <<EOF
table ip nat {
    chain clash_dns {
        meta l4proto udp redirect to :1053
    }

    chain PREROUTING {
        type nat hook prerouting priority dstnat; policy accept;
        meta l4proto udp udp dport 53 jump clash_dns
        meta l4proto tcp tcp dport { 22,53,587,465,995,993,143,80,443,8080} jump clash
    }

    chain clash {
        ip daddr 0.0.0.0/8 return
        ip daddr 10.0.0.0/8 return
        ip daddr 127.0.0.0/8 return
        ip daddr 100.64.0.0/10 return
        ip daddr 169.254.0.0/16 return
        ip daddr 172.16.0.0/12 return
        ip daddr 192.168.0.0/16 return
        ip daddr 224.0.0.0/4 return
        ip daddr 240.0.0.0/4 return
        meta l4proto tcp ip saddr 192.168.0.0/16 redirect to :7892
        meta l4proto tcp ip saddr 10.0.0.0/8 redirect to :7892
    }
}
table ip6 nat {
    chain clashv6_dns {
        meta l4proto udp redirect to :1053
    }

    chain PREROUTING {
        type nat hook prerouting priority dstnat; policy accept;
        meta l4proto udp udp dport 53 jump clashv6_dns
    }
}
table ip filter {
    chain INPUT {
        type filter hook input priority filter; policy accept;
        meta l4proto tcp ip saddr 10.0.0.0/8 tcp dport 7890 accept
        meta l4proto tcp ip saddr 127.0.0.0/8 tcp dport 7890 accept
        meta l4proto tcp ip saddr 192.168.0.0/16 tcp dport 7890 accept
        meta l4proto tcp ip saddr 172.16.0.0/12 tcp dport 7890 accept
        meta l4proto tcp tcp dport 7890 reject
    }
}
table ip6 filter {
    chain INPUT {
        type filter hook input priority filter; policy accept;
        meta l4proto tcp tcp dport 7890 reject
    }
}
EOF

if [ -z "$(id shellclash 2>/dev/null | grep 'root')" ];then
            if ckcmd userdel useradd groupmod; then
                userdel shellclash 2>/dev/null
                useradd shellclash -u 7890
                groupmod shellclash -g 7890
                sed -Ei s/7890:7890/0:7890/g /etc/passwd
            else
                grep -qw shellclash /etc/passwd || echo "shellclash:x:0:7890:::" >> /etc/passwd
            fi
        fi

cat > /etc/nftables/nftables-redirect-clash-local.nft <<EOF
table ip nat {
    chain PREROUTING {
        type nat hook prerouting priority dstnat; policy accept;
        meta l4proto udp udp dport 53 jump clash_dns
        meta l4proto tcp tcp dport { 22,53,587,465,995,993,143,80,443,8080} jump clash
        meta l4proto tcp jump clash
    }

    chain clash {
        ip daddr 0.0.0.0/8 return
        ip daddr 10.0.0.0/8 return
        ip daddr 127.0.0.0/8 return
        ip daddr 100.64.0.0/10 return
        ip daddr 169.254.0.0/16 return
        ip daddr 172.16.0.0/12 return
        ip daddr 192.168.0.0/16 return
        ip daddr 224.0.0.0/4 return
        ip daddr 240.0.0.0/4 return
        meta l4proto tcp ip saddr 192.168.0.0/16 redirect to :7892
        meta l4proto tcp ip saddr 10.0.0.0/8 redirect to :7892
    }

    chain clash_dns {
        meta l4proto udp redirect to :1053
    }

    chain clash_out {
        skgid 7890 return
        ip daddr 0.0.0.0/8 return
        ip daddr 10.0.0.0/8 return
        ip daddr 100.64.0.0/10 return
        ip daddr 127.0.0.0/8 return
        ip daddr 169.254.0.0/16 return
        ip daddr 192.168.0.0/16 return
        ip daddr 224.0.0.0/4 return
        ip daddr 240.0.0.0/4 return
        meta l4proto tcp redirect to :7892
    }

    chain OUTPUT {
        type nat hook output priority -100; policy accept;
        meta l4proto tcp jump clash_out
        meta l4proto udp udp dport 53 jump clash_dns_out
    }

    chain clash_dns_out {
        skgid 7890 return
        meta l4proto udp redirect to :1053
    }
}
table ip6 nat {
    chain PREROUTING {
        type nat hook prerouting priority dstnat; policy accept;
        meta l4proto udp udp dport 53 jump clashv6_dns
    }

    chain clashv6_dns {
        meta l4proto udp redirect to :1053
    }
}
table ip filter {
    chain INPUT {
        type filter hook input priority filter; policy accept;
        meta l4proto tcp ip saddr 10.0.0.0/8 tcp dport 7890 accept
        meta l4proto tcp ip saddr 127.0.0.0/8 tcp dport 7890 accept
        meta l4proto tcp ip saddr 192.168.0.0/16 tcp dport 7890 accept
        meta l4proto tcp ip saddr 172.16.0.0/12 tcp dport 7890 accept
        meta l4proto tcp tcp dport 7890 reject
    }
}
table ip6 filter {
    chain INPUT {
        type filter hook input priority filter; policy accept;
        meta l4proto tcp tcp dport 7890 reject
    }
}
EOF
nft flush ruleset
service con restart
nft -f /etc/nftables/nftables-redirect-clash-local.nft

iptables-tpproxy

Istio的流量劫持和Linux下透明代理实现

后续再补充,和redirect的主要区别是不需要ip_forward的特性,nftables的语句将主要是–tp-proxy等。因为tpproxy需要给流量标记,还需要单独对有流量标记的流量配置路由表

除了利用REDIRECT模式,Istio还提供TPROXY模式,当然也是借助Linux内核提供的功能实现的,对于TPROXY模式,实现的原理要相对复杂不少,需要借助iptables和路由:通过iptables将数据包打上mark,然后使用一个特殊的路由,将数据包指向本地,由于使用了mangle表,所以数据包的原始和目的地址都是不会被修改的。下面是一个例子:

iptables -t mangle -A PREROUTING -p tcp -j TPROXY --tproxy-mark 0x1/0x1 --on-port 8888
ip rule add fwmark 0x1/0x1 pref 100 table 100
ip route add local default dev lo table 100

一、注意事项

本文中内网 CIDR 为 192.168.0.0/16, 即所有地址段规则、配置都是针对当前内网 CIDR 进行处理的; clash fake-ip 的 CIDR 为 198.18.0.0/16, 请不要写错成 192, 这是 198(也不要问我为什么强调).

二、安装 Clash

本文所采用的透明代理方式不依赖于 TUN, 所有是否是增强版本不重要, 如果可以请尽量使用最新版本.

# x86 用户请自行替换
wget https://github.com/Dreamacro/clash/releases/download/v1.9.0/clash-linux-armv8-v1.9.0.gz

# 解压
gzip -d clash-linux-armv8-v1.9.0.gz

# 安装到系统 PATH
chmod +x clash-linux-armv8-v1.9.0
mv clash-linux-armv8-v1.9.0 /usr/bin/clash

创建专用的 clash 用户:

useradd -M -s /usr/sbin/nologin clash

编写 Systemd 配置文件:

cat > /lib/systemd/system/clash.service <<EOF
[Unit]
Description=Clash TProxy
After=network.target

[Service]
Type=simple
User=clash
Group=clash
CapabilityBoundingSet=CAP_NET_ADMIN CAP_NET_BIND_SERVICE CAP_NET_RAW
AmbientCapabilities=CAP_NET_ADMIN CAP_NET_BIND_SERVICE CAP_NET_RAW
Restart=on-failure

ExecStartPre=+/usr/bin/bash /etc/clash/clean.sh
ExecStart=/usr/bin/clash -d /etc/clash
ExecStartPost=+/usr/bin/bash /etc/clash/iptables.sh

ExecStopPost=+/usr/bin/bash /etc/clash/clean.sh

[Install]
WantedBy=multi-user.target
EOF

三、调整配置

本文中 Clash 配置文件、脚本等统一存放到 /etc/clash 目录中, 针对于 Clash 配置文件, 着重说明重点配置, 完整配置请从官方 Wiki 复制: https://github.com/Dreamacro/clash/wiki/configuration#all-configuration-options

3.1、端口配置

端口配置请尽量保持默认, 如果需要调整端口, 请同步修改后面相关脚本中的端口(TProxy):

# 注释掉 port 端口配置, 使用 mixed-port
#port: 7890

# 注释掉 socks-port 端口配置, 使用 mixed-port
#socks-port: 7891

# 注释掉 redir-port 端口配置, 因为全部采用 TProxy 模式
#redir-port: 7892

# TProxy 的透明代理端口
tproxy-port: 7893

# mixed-port 端口将同时支持 SOCKS5/HTTP
mixed-port: 7890

# 允许来自局域网的连接
allow-lan: true

# 绑定到所有接口
bind-address: '*'

3.2、DNS 配置

Clash 配置中请开启 DNS, 并使用 fake-ip 模式, 样例配置如下:

dns:
  enable: true
  listen: 0.0.0.0:1053
  ipv6: false
  default-nameserver:
    - 114.114.114.114
    - 8.8.8.8
  enhanced-mode: fake-ip

3.3、防火墙规则

为了保证防火墙规则不被破坏, 本文采用脚本暴力操作, 如果宿主机有其他 iptables 控制程序, 则推荐手动执行并通过 iptables-persistent 等工具进行持久化;

/etc/clash/iptables.sh: 负责启动时添加 iptables 规则

#!/usr/bin/env bash

set -ex

# ENABLE ipv4 forward
#sysctl -w net.ipv4.ip_forward=1

# ROUTE RULES
ip rule add fwmark 666 lookup 666
ip route add local 0.0.0.0/0 dev lo table 666

# clash 链负责处理转发流量 
iptables -t mangle -N clash

# 目标地址为局域网或保留地址的流量跳过处理
# 保留地址参考: https://zh.wikipedia.org/wiki/%E5%B7%B2%E5%88%86%E9%85%8D%E7%9A%84/8_IPv4%E5%9C%B0%E5%9D%80%E5%9D%97%E5%88%97%E8%A1%A8
iptables -t mangle -A clash -d 0.0.0.0/8 -j RETURN
iptables -t mangle -A clash -d 127.0.0.0/8 -j RETURN
iptables -t mangle -A clash -d 10.0.0.0/8 -j RETURN
iptables -t mangle -A clash -d 172.16.0.0/12 -j RETURN
iptables -t mangle -A clash -d 192.168.0.0/16 -j RETURN
iptables -t mangle -A clash -d 169.254.0.0/16 -j RETURN

iptables -t mangle -A clash -d 224.0.0.0/4 -j RETURN
iptables -t mangle -A clash -d 240.0.0.0/4 -j RETURN

# 其他所有流量转向到 7893 端口,并打上 mark
iptables -t mangle -A clash -p tcp -j TPROXY --on-port 7893 --tproxy-mark 666
iptables -t mangle -A clash -p udp -j TPROXY --on-port 7893 --tproxy-mark 666

# 转发所有 DNS 查询到 1053 端口
# 此操作会导致所有 DNS 请求全部返回虚假 IP(fake ip 198.18.0.1/16)
#iptables -t nat -I PREROUTING -p udp --dport 53 -j REDIRECT --to 1053

# 如果想要 dig 等命令可用, 可以只处理 DNS SERVER 设置为当前内网的 DNS 请求
#iptables -t nat -I PREROUTING -p udp --dport 53 -d 192.168.0.0/16 -j REDIRECT --to 1053
iptables -t nat -I PREROUTING -p tcp -m tcp --dport 53 -j REDIRECT --to-ports 53
iptables -t nat -I PREROUTING -p udp -m udp --dport 53 -j REDIRECT --to-ports 53

# 最后让所有流量通过 clash 链进行处理
iptables -t mangle -I PREROUTING -m set --match-set china_net dst -j ACCEPT
iptables -t mangle -A PREROUTING -j clash

# clash_local 链负责处理网关本身发出的流量
iptables -t mangle -N clash_local

# nerdctl 容器流量重新路由
#iptables -t mangle -A clash_local -i nerdctl2 -p udp -j MARK --set-mark 666
#iptables -t mangle -A clash_local -i nerdctl2 -p tcp -j MARK --set-mark 666

# 跳过内网流量
iptables -t mangle -A clash_local -d 0.0.0.0/8 -j RETURN
iptables -t mangle -A clash_local -d 127.0.0.0/8 -j RETURN
iptables -t mangle -A clash_local -d 10.0.0.0/8 -j RETURN
iptables -t mangle -A clash_local -d 172.16.0.0/12 -j RETURN
iptables -t mangle -A clash_local -d 192.168.0.0/16 -j RETURN
iptables -t mangle -A clash_local -d 169.254.0.0/16 -j RETURN

iptables -t mangle -A clash_local -d 224.0.0.0/4 -j RETURN
iptables -t mangle -A clash_local -d 240.0.0.0/4 -j RETURN

# 为本机发出的流量打 mark
iptables -t mangle -A clash_local -p tcp -j MARK --set-mark 666
iptables -t mangle -A clash_local -p udp -j MARK --set-mark 666

# 跳过 clash 程序本身发出的流量, 防止死循环(clash 程序需要使用 "clash" 用户启动) 
iptables -t mangle -A OUTPUT -p tcp -m owner --uid-owner clash -j RETURN
iptables -t mangle -A OUTPUT -p udp -m owner --uid-owner clash -j RETURN

# 让本机发出的流量跳转到 clash_local
# clash_local 链会为本机流量打 mark, 打过 mark 的流量会重新回到 PREROUTING 上
iptables -t mangle -A OUTPUT -j clash_local

# 修复 ICMP(ping)
# 这并不能保证 ping 结果有效(clash 等不支持转发 ICMP), 只是让它有返回结果而已
# --to-destination 设置为一个可达的地址即可
#sysctl -w net.ipv4.conf.all.route_localnet=1
#iptables -t nat -A PREROUTING -p icmp -d 198.18.0.0/16 -j DNAT --to-destination 127.0.0.1

/etc/clash/clean.sh: 负责启动前/停止后清理 iptables 规则(暴力清理)

#!/usr/bin/env bash

set -ex

ip rule del fwmark 666 table 666 || true
ip route del local 0.0.0.0/0 dev lo table 666 || true

#iptables -t nat -F
#iptables -t nat -X
iptables -t nat -D PREROUTING 2
iptables -t nat -D PREROUTING 1
iptables -t mangle -F
iptables -t mangle -X clash || true
iptables -t mangle -X clash_local || true

3.4、最终目录结构

所有配置编写完成后, 其目录结构如下:

root@openrpi # ❯❯❯ tree -L 1 /etc/clash
/etc/clash
├── clean.sh
├── config.yaml
└── iptables.sh

最后需要修复 /etc/clash 目录权限, 因为 Clash 启动后会写入其他文件:

chown -R clash:clash /etc/clash

四、启动及测试

如果所有配置和文件安装没问题的话, 可以直接通过 Systemd 启动:

# 启动
systemctl start clash

# 查看日志
journalctl -fu clash

如果启动成功, 那么此时内网设备将网关设置到当前 Clash 所在机器即可完成透明代理; 如果 Clash 机器足够稳定, 也可以一步到位将内网路由器的 DHCP 设置中下发的网关直接填写为 Clash 机器 IP(Clash 机器需要使用静态 IP).