clash nftables 透明代理(TPROXY)

因为某些原因决定放弃使用 openclash,直接裸跑核心,并配置 nftables 规则实现透明代理。本文中的配置是示例,也算是我的一个折腾记录,如果要使用请根据实际情况自己修改。

环境

Debian11: 专门用来做透明代理的网关服务器,或者叫旁路网关,并且在/etc/network/interfaces将网络设置为静态:

1
2
3
4
5
iface ens16 inet static
address 10.0.0.20
netmask 255.255.255.0
gateway 10.0.0.1
dns-nameservers 10.0.0.1

Clash.Meta: 运行在网关服务器上做dns服务、规则分流和代理

配置

路径/root/ruanjian/clash/nftables.sh

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
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
#!/bin/bash

INTERFACE='ens16'
TPROXY_PORT=7895 # 和 clash 中定义的一致
DNS_PORT=1053 # 和 clash 中定义的一致
ROUTING_MARK=666 # 和 clash 中定义的 routing-mark 一致
PROXY_FWMARK=1
PROXY_ROUTE_TABLE=100

# https://en.wikipedia.org/wiki/Reserved_IP_addresses
LocalNetworkBypass()
{
nft add rule inet $1 $2 ip daddr 127.0.0.0/8 accept
nft add rule inet $1 $2 ip daddr 100.64.0.0/10 accept
nft add rule inet $1 $2 ip daddr 169.254.0.0/16 accept
nft add rule inet $1 $2 ip daddr 172.16.0.0/12 accept
nft add rule inet $1 $2 ip daddr 224.0.0.0/4 accept
nft add rule inet $1 $2 ip daddr 240.0.0.0/4 accept
nft add rule inet $1 $2 ip daddr 255.255.255.255/32 accept

nft add rule inet $1 $2 ip daddr 10.0.0.0/16 accept
nft add rule inet $1 $2 ip daddr 192.168.0.0/16 accept
}

ProxyAddrBypass()
{
# 改成你的代理ip
#nft add rule inet $1 $2 ip daddr x.x.x.x/32 accept
}

clearFirewallRules()
{
IPRULE=$(ip rule show | grep $PROXY_ROUTE_TABLE)
if [ -n "$IPRULE" ]
then
ip -f inet rule del fwmark $PROXY_FWMARK lookup $PROXY_ROUTE_TABLE
ip -f inet route del local default dev $INTERFACE table $PROXY_ROUTE_TABLE
echo "clear ip rule"
fi

nft flush ruleset
echo "clear nftables"
}

if [ $1 = 'set' ]
then

clearFirewallRules

ip -f inet rule add fwmark $PROXY_FWMARK lookup $PROXY_ROUTE_TABLE
ip -f inet route add local default dev $INTERFACE table $PROXY_ROUTE_TABLE
sysctl -w net.ipv4.ip_forward=1 > /dev/null

nft add table inet clash

# ----- prerouting 局域网设备透明代理 -----
nft add chain inet clash prerouting_tproxy { type filter hook prerouting priority -150\; policy accept\; }
nft add rule inet clash prerouting_tproxy meta l4proto {tcp,udp} th dport 53 accept # 绕过dns查询查询流量,以便在prerouting hook的nat类型中重定向到clash的dns端口
ProxyAddrBypass 'clash' 'prerouting_tproxy' # 绕过目标地址为代理ip的地址 (局域网的设备直接访问vps的流量,比如ssh,网站等)。
nft add rule inet clash prerouting_tproxy fib daddr type local accept # 绕过目标地址为局域网或保留的地址
LocalNetworkBypass 'clash' 'prerouting_tproxy'
nft add rule inet clash prerouting_tproxy meta l4proto udp accept # 绕过udp流量(udp不进行透明代理,玩游戏时透明代理udp不太稳定,不清楚是不是clash的问题)
nft add rule inet clash prerouting_tproxy meta l4proto {tcp,udp} socket transparent 1 meta mark set $PROXY_FWMARK accept # 绕过已经建立的连接
nft add rule inet clash prerouting_tproxy meta l4proto {tcp,udp} tproxy to :$TPROXY_PORT meta mark set $PROXY_FWMARK # 其他流量透明代理到clash

nft add chain inet clash prerouting_dns_redirect { type nat hook prerouting priority -100\; policy accept\; }
nft add rule inet clash prerouting_dns_redirect meta l4proto {tcp,udp} th dport 53 redirect to :$DNS_PORT # 重定向dns查询流量到clash dns端口

# ----- output 网关本机透明代理 -----
nft add chain inet clash output_tproxy { type route hook output priority -150\; policy accept\; }
nft add rule inet clash output_tproxy meta oifname != $INTERFACE accept # 绕过本机内部通信的流量(接口lo)
nft add rule inet clash output_tproxy meta mark $ROUTING_MARK accept # 绕过本机clash发出的流量
nft add rule inet clash output_tproxy meta l4proto {tcp,udp} th dport 53 accept # 绕过本机dns查询查询流量,以便在output hook的nat类型中重定向到clash的dns端口
nft add rule inet clash output_tproxy udp dport {123,137} accept # 绕过NTP、NBNS流量
ProxyAddrBypass 'clash' 'output_tproxy' # 绕过目标地址为代理ip的地址 (本机其他软件访问vps的流量,比如clash通过socks5套本机的naiveproxy)。
nft add rule inet clash output_tproxy fib daddr type local accept # 绕过目标地址为局域网或保留的地址
LocalNetworkBypass 'clash' 'output_tproxy'
nft add rule inet clash output_tproxy meta l4proto {tcp,udp} meta mark set $PROXY_FWMARK # 其他流量重路由到prerouting

nft add chain inet clash output_dns_redirect { type nat hook output priority -100\; policy accept\; }
nft add rule inet clash output_dns_redirect meta mark $ROUTING_MARK accept # 绕过本机clash发出的流量
nft add rule inet clash output_dns_redirect meta l4proto {tcp,udp} th dport 53 redirect to :$DNS_PORT # 重定向dns查询流量到clash dns端口

# ----- output quic拒绝 -----
# 防止 YouTube 等使用 QUIC 导致速度不佳, 慎用
nft add chain inet clash output_quic_reject { type filter hook output priority 0\; policy accept\; }
nft add rule inet clash output_quic_reject udp dport 443 reject with icmpx type host-unreachable

# ----- forward quic拒绝 -----
# 防止 YouTube 等使用 QUIC 导致速度不佳, 慎用
nft add chain inet clash forward_quic_reject { type filter hook forward priority 0\; policy accept\; }
nft add rule inet clash forward_quic_reject udp dport 443 reject with icmpx type host-unreachable

echo "set nftables"

elif [ $1 = 'clear' ]
then
clearFirewallRules
fi

路径/etc/systemd/system/clash.service

1
2
3
4
5
6
7
8
9
10
11
12
13
14
[Unit]
Description=Clash-Meta tproxy daemon.
After=network.target

[Service]
Type=simple
LimitNOFILE=1000000
Restart=always
ExecStart=/root/ruanjian/clash/clash_meta -d /root/ruanjian/clash/config
ExecStartPost=sh /root/ruanjian/clash/nftables.sh set
ExecStop=sh /root/ruanjian/clash/nftables.sh clear

[Install]
WantedBy=multi-user.target

路径/root/ruanjian/clash/config/config.yaml

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
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
# https://github.com/MetaCubeX/Clash.Meta/blob/Alpha/docs/config.yaml
# https://github.com/Dreamacro/clash/wiki/Configuration

tproxy-port: 7895
routing-mark: 666
mixed-port: 7900

allow-lan: true
bind-address: '*'

mode: rule
log-level: warning

geodata-mode: true
geodata-loader: standard
geox-url:
geoip: "https://cdn.jsdelivr.net/gh/Loyalsoldier/v2ray-rules-dat@release/geoip.dat"
geosite: "https://cdn.jsdelivr.net/gh/Loyalsoldier/v2ray-rules-dat@release/geosite.dat"
mmdb: "https://cdn.jsdelivr.net/gh/Loyalsoldier/geoip@release/Country.mmdb"

ipv6: false

# Yacd: https://github.com/MetaCubeX/Yacd-meta/archive/refs/heads/gh-pages.zip
# clash: https://github.com/MetaCubeX/Razord-meta/archive/refs/heads/gh-pages.zip
external-ui: ui
external-controller: 0.0.0.0:9090

find-process-mode: 'off'
global-client-fingerprint: chrome

profile:
store-selected: false
store-fake-ip: true

dns:
enable: true
ipv6: false
listen: 0.0.0.0:1053

default-nameserver:
- 223.6.6.6
- 119.29.29.29

enhanced-mode: fake-ip
fake-ip-range: 198.18.0.1/16
fake-ip-filter:
- '*.lan'
- 'ntp.*.com'
- '+.pool.ntp.org'
nameserver:
- https://223.6.6.6/dns-query
- https://1.12.12.12/dns-query

proxies:

proxy-groups:

# https://github.com/Loyalsoldier/geoip
# https://github.com/Loyalsoldier/v2ray-rules-dat
# https://github.com/v2fly/domain-list-community
rules:
- GEOSITE,github,PROXY
- GEOSITE,apple,APPLE
- GEOSITE,google,PROXY
- GEOSITE,gfw,PROXY
- GEOIP,telegram,PROXY

- GEOIP,private,DIRECT,no-resolve
- GEOSITE,private,DIRECT

- GEOSITE,microsoft@cn,DIRECT
- GEOSITE,cn,DIRECT
- GEOIP,cn,DIRECT
- GEOSITE,steam,STEAM

- MATCH,PROXY

nftables.sh看起来不太直观,使用nft list ruleset输出的格式会更容易理解

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
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
table inet clash {
chain prerouting_tproxy {
type filter hook prerouting priority mangle; policy accept;
meta l4proto { tcp, udp } th dport 53 accept
ip daddr x.x.x.x accept
fib daddr type local accept
ip daddr 127.0.0.0/8 accept
ip daddr 100.64.0.0/10 accept
ip daddr 169.254.0.0/16 accept
ip daddr 172.16.0.0/12 accept
ip daddr 224.0.0.0/4 accept
ip daddr 240.0.0.0/4 accept
ip daddr 255.255.255.255 accept
ip daddr 10.0.0.0/16 accept
ip daddr 192.168.0.0/16 accept
meta l4proto udp accept
meta l4proto { tcp, udp } socket transparent 1 meta mark set 0x00000001 accept
meta l4proto { tcp, udp } tproxy to :7895 meta mark set 0x00000001
}

chain prerouting_dns_redirect {
type nat hook prerouting priority dstnat; policy accept;
meta l4proto { tcp, udp } th dport 53 redirect to :1053
}

chain output_tproxy {
type route hook output priority mangle; policy accept;
oifname != "ens16" accept
meta mark 0x0000029a accept
meta l4proto { tcp, udp } th dport 53 accept
udp dport { 123, 137 } accept
ip daddr x.x.x.x accept
fib daddr type local accept
ip daddr 127.0.0.0/8 accept
ip daddr 100.64.0.0/10 accept
ip daddr 169.254.0.0/16 accept
ip daddr 172.16.0.0/12 accept
ip daddr 224.0.0.0/4 accept
ip daddr 240.0.0.0/4 accept
ip daddr 255.255.255.255 accept
ip daddr 10.0.0.0/16 accept
ip daddr 192.168.0.0/16 accept
meta l4proto { tcp, udp } meta mark set 0x00000001
}

chain output_dns_redirect {
type nat hook output priority -100; policy accept;
meta mark 0x0000029a accept
meta l4proto { tcp, udp } th dport 53 redirect to :1053
}

chain output_quic_reject {
type filter hook output priority filter; policy accept;
udp dport 443 reject with icmpx type host-unreachable
}

chain forward_quic_reject {
type filter hook forward priority filter; policy accept;
udp dport 443 reject with icmpx type host-unreachable
}
}

启动Clash

启动systemctl start clash
停止systemctl stop clash
重启systemctl restart clash
日志journalctl -f -u clash

如果最后测试没问题将clash设为开机自启动systemctl enable clash

使用

将局域网其他设备的网关和DNS服务器手动指定到该机器上,测试看看是否正常工作。没问题后可以在主路由器的DHCP附加选项(dhcp_option)中将网关和DNS服务器自动通告给客户端,这样就不用在其他设备手动指定了,这里以OpenWrt为例:

外部web控制面板用Yacd-meta,实现基本的控制和信息查看。

关于nftables

由于 nftables 更现代化、更高效、更灵活,具有很多优势,是时候抛弃使用 iptables 命令了。和 iptables 的逻辑不同,nftables 默认没有表和链,都需要你自己创建。创建链时需要注意:

1
type filter hook prerouting priority -150; policy accept;
  • type是什么。filterroutenat,每种类型功能不一样,能在什么hook工作也不一样,比如redirect需要在nat类型进行。在iptables体现为4个预先定义的表(raw,mangle,nat,filter),并且默认定义了优先级不能改。
  • hook什么阶段。preroutinginputforwardoutputpostrouting,取决于网络数据包在路由中的位置。在iptables体现为5个预先定义的链(PREROUTING,INPUT,FORWARD,OUTPUT,POSTROUTING)。
  • priority怎么样。在当前hook中,各个自定义链的优先级和执行顺序。

Docker

Docker 目前仍然操纵 iptables 规则来使 docker 网络工作。所以不建议在该网关服务器上使用 docker 相关服务,在以下链接查看更多信息:

参考