sing-box tproxy

sing-box 透明代理配置。 最近从 clash 切换到 sing-box,对透明代理做一个记录。大体逻辑和 clash nftables 透明代理 保持一致。

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
#!/bin/bash

INTERFACE='ens16'
TPROXY_PORT=7895 ## 和 sing-box 中定义的一致
ROUTING_MARK=666 ## 和 sing-box 中定义的一致
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 sing-box

# ----- prerouting 局域网设备透明代理 -----
nft add chain inet sing-box prerouting_tproxy { type filter hook prerouting priority -150\; policy accept\; }
nft add rule inet sing-box prerouting_tproxy meta l4proto {tcp,udp} th dport 53 tproxy to :$TPROXY_PORT accept ## DNS透明代理
ProxyAddrBypass 'sing-box' 'prerouting_tproxy' ## 代理地址绕过
nft add rule inet sing-box prerouting_tproxy fib daddr type local accept ## 本机地址绕过
LocalNetworkBypass 'sing-box' 'prerouting_tproxy' ## 局域网地址绕过
nft add rule inet sing-box prerouting_tproxy meta l4proto udp accept ## UDP绕过
nft add rule inet sing-box prerouting_tproxy meta l4proto {tcp,udp} socket transparent 1 meta mark set $PROXY_FWMARK accept ## 绕过已经建立的tproxy连接
nft add rule inet sing-box prerouting_tproxy meta l4proto {tcp,udp} tproxy to :$TPROXY_PORT meta mark set $PROXY_FWMARK ## 其他流量透明代理

# ----- output 网关本机透明代理 -----
nft add chain inet sing-box output_tproxy { type route hook output priority -150\; policy accept\; }
nft add rule inet sing-box output_tproxy meta oifname != $INTERFACE accept ## 绕过本机内部通信的流量(接口lo)
nft add rule inet sing-box output_tproxy meta mark $ROUTING_MARK accept ## 绕过本机sing-box发出的流量
nft add rule inet sing-box output_tproxy meta l4proto {tcp,udp} th dport 53 meta mark set $PROXY_FWMARK accept ## DNS重路由到prerouting
nft add rule inet sing-box output_tproxy udp dport {123,137} accept ## 绕过NTP、NBNS流量
ProxyAddrBypass 'sing-box' 'output_tproxy' ## 代理地址绕过
nft add rule inet sing-box output_tproxy fib daddr type local accept ## 本机地址绕过
LocalNetworkBypass 'sing-box' 'output_tproxy' ## 局域网地址绕过
nft add rule inet sing-box output_tproxy meta l4proto {tcp,udp} meta mark set $PROXY_FWMARK ## 其他流量重路由到prerouting

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

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

echo "set nftables"

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

sing-box-tproxy.json

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
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
{
"log": {
"disabled": false,
"level": "warn",
"timestamp": true
},
"dns": {
"servers": [
{
"tag": "mainDNS",
"address": "https://223.6.6.6/dns-query",
"detour": "DIRECT"
},
{
"tag": "localDNS",
"address": "10.0.0.1",
"detour": "DIRECT"
},
{
"tag": "cloudflareDNS",
"address": "https://1.1.1.1/dns-query",
"detour": "PROXY"
},
{
"tag": "refusedDNS",
"address": "rcode://refused"
},
{
"tag": "fakeipDNS",
"address": "fakeip"
}
],
"rules": [
{
"outbound": "any",
"server": "mainDNS"
},
{
"geosite": "private",
"server": "localDNS"
},
{
"clash_mode": "direct",
"server": "mainDNS"
},
{
"query_type": ["HTTPS", "AAAA"],
"server": "refusedDNS"
},
{
"geosite": [
"tencent",
"golang",
"apple",
"category-bank-cn",
"xunlei"
],
"domain_keyword": [
"time",
"ntp",
"music",
"stun",
"bank",
"msftconnecttest",
"msftncsi"
],
"server": "mainDNS"
},
{
"query_type": "A",
"server": "fakeipDNS"
}
],
"final": "localDNS",
"strategy": "ipv4_only",
"disable_cache": false,
"disable_expire": false,
"independent_cache": true,
"reverse_mapping": false,
"fakeip": {
"enabled": true,
"inet4_range": "198.18.0.0/15",
"inet6_range": "fc00::/18"
}
},
"inbounds": [
{
"tag": "TPROXY-IN",
"type": "tproxy",
"listen": "0.0.0.0",
"listen_port": 7895,
"sniff": true
},
{
"tag": "MIXED-MAIN-IN",
"type": "mixed",
"listen": "0.0.0.0",
"listen_port": 7900,
"sniff": true
}
],
"outbounds": [
{
"tag": "PROXY",
"type": "selector",
"outbounds": [
"REALITY",
"TROJAN"
],
"default": "REALITY",
"interrupt_exist_connections": false
},
{
"tag": "FINAL",
"type": "selector",
"outbounds": [
"PROXY",
"DIRECT"
],
"default": "PROXY",
"interrupt_exist_connections": false
},
{
"tag": "APPLE",
"type": "selector",
"outbounds": [
"PROXY",
"DIRECT",
"REALITY",
"TROJAN"
],
"default": "DIRECT",
"interrupt_exist_connections": false
},
{
"tag": "STEAM",
"type": "selector",
"outbounds": [
"PROXY",
"DIRECT",
"REALITY",
"TROJAN"
],
"default": "PROXY",
"interrupt_exist_connections": false
},
{
"tag": "BING",
"type": "selector",
"outbounds": [
"PROXY",
"DIRECT",
"REALITY",
"TROJAN"
],
"default": "PROXY",
"interrupt_exist_connections": false
},
{
"tag": "REALITY",
"type": "vless",
"server": "x.x.x.x",
"server_port": 443,
"uuid": "uuid",
"flow": "xtls-rprx-vision",
"tls": {
"enabled": true,
"server_name": "example.com",
"utls": {
"enabled": true,
"fingerprint": "chrome"
},
"reality": {
"enabled": true,
"public_key": "public_key",
"short_id": ""
}
},
"packet_encoding": "xudp"
},
{
"tag": "TROJAN",
"type": "trojan",
"server": "example.com",
"server_port": 443,
"password": "password",
"tls": {
"enabled": true,
"server_name": "example.com",
"insecure": false,
"utls": {
"enabled": true,
"fingerprint": "chrome"
}
}
},
{
"tag": "DIRECT",
"type": "direct"
},
{
"tag": "DNS-OUT",
"type": "dns"
},
{
"tag": "BLOCK",
"type": "block"
}
],
"route": {
"geoip": {
"download_url": "https://github.com/MetaCubeX/meta-rules-dat/releases/download/latest/geoip.db",
"download_detour": "PROXY"
},
"geosite": {
"download_url": "https://github.com/MetaCubeX/meta-rules-dat/releases/download/latest/geosite.db",
"download_detour": "PROXY"
},
"rules": [
{
"protocol": "dns",
"outbound": "DNS-OUT"
},
{
"port": 53,
"outbound": "DNS-OUT"
},
{
"geosite": "private",
"outbound": "DIRECT"
},
{
"clash_mode": "direct",
"outbound": "DIRECT"
},
{
"clash_mode": "global",
"outbound": "PROXY"
},
{
"geosite": "apple",
"outbound": "APPLE"
},
{
"geosite": "bing",
"outbound": "BING"
},
{
"geosite": [
"github",
"google",
"telegram",
"gfw"
],
"geoip": "telegram",
"outbound": "PROXY"
},
{
"geoip": ["private", "cn"],
"geosite": "cn",
"outbound": "DIRECT"
},
{
"geosite": "steam",
"outbound": "STEAM"
}
],
"final": "FINAL",
"auto_detect_interface": true,
"default_mark": 666
},
"experimental": {
"clash_api": {
"external_controller": "0.0.0.0:9090",
"external_ui": "ui",
"external_ui_download_url": "https://github.com/MetaCubeX/Yacd-meta/archive/gh-pages.zip",
"external_ui_download_detour": "PROXY",
"secret": "secret",
"default_mode": "rule",
"store_mode": false,
"store_selected": false,
"store_fakeip": true
}
}
}

踩过的一些坑

  1. 得益于 sing-box 强大的路由功能,DNS 劫持可以直接从 tproxy 进,sing-box 只需在出站 route 时写上 DNS 检测规则并分流到 DNS 模块处理,不需要入站监听 DNS 端口。
  2. 出站 route 检测 DNS 时,仅仅靠"protocol": "dns"是不够的,还需检测"port": 53, 防止一些错误的 DNS 查询没分流到 DNS 模块,而是直接出站了导致 nftables tproxy 又劫持 53 传回 sing-box 形成死循环。比如 这个域名,传整个 URL 当域名查询也是人才。
  3. 想要出站 route 时匹配域名规则,一般就三种方法:1.sniff。2.fakeip。3.redir-host。有些域名不适合 fakeip,在 dns.route 时返回了正常的 DNS 解析结果,这时就需要 sniff,以便出站 route 时提供域名匹配域名规则。
  4. 如果出站不是 fakeip。默认出站是传IP到服务端,建议在服务端入站设置"sniff": true, "sniff_override_destination": true,让服务端恢复成域名重新解析 DNS,而不是直接使用客户端传递的IP。