sing-box tproxy

sing-box 透明代理配置。 本文中的配置是示例,如果要使用请根据实际情况自己修改。

环境

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

1
2
3
iface ens16 inet static
address 10.0.0.20/24
gateway 10.0.0.1

sing-box:运行在网关服务器上做DNS服务、规则分流和代理。

配置

注意替换里面的路径。

路径/path/to/sing-box/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
#!/bin/sh

INTERFACE=$(ip route show default | awk '/default/ {print $5}')
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
ReservedIP4='{ 127.0.0.0/8, 10.0.0.0/8, 192.168.0.0/16, 100.64.0.0/10, 169.254.0.0/16, 172.16.0.0/12, 224.0.0.0/4, 240.0.0.0/4, 255.255.255.255/32 }'
CustomBypassIP='{ 192.168.0.0/16 }' ## 添加你的代理IP地址或其他不想透明代理的地址

if [ $# != 1 ]
then
echo "Use $(basename "$0") <set|clear>"
exit 1;
fi

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 -f - <<EOF
table inet sing-box {
chain prerouting_tproxy {
type filter hook prerouting priority mangle; policy accept;
meta l4proto { tcp, udp } th dport 53 tproxy to :$TPROXY_PORT accept comment "DNS透明代理"
ip daddr $CustomBypassIP accept comment "绕过某些地址"
fib daddr type local meta l4proto { tcp, udp } th dport $TPROXY_PORT reject with icmpx type host-unreachable comment "直接访问tproxy端口拒绝, 防止回环"
fib daddr type local accept comment "本机绕过"
ip daddr $ReservedIP4 accept comment "保留地址绕过"
meta l4proto tcp socket transparent 1 meta mark set $PROXY_FWMARK accept comment "绕过已经建立的透明代理"
meta l4proto { tcp, udp } tproxy to :$TPROXY_PORT meta mark set $PROXY_FWMARK comment "其他流量透明代理"
}

chain output_tproxy {
type route hook output priority mangle; policy accept;
oifname != $INTERFACE accept comment "绕过本机内部通信的流量(接口lo)"
meta mark $ROUTING_MARK accept comment "绕过本机sing-box发出的流量"
meta l4proto { tcp, udp } th dport 53 meta mark set $PROXY_FWMARK accept comment "DNS重路由到prerouting"
udp dport { netbios-ns, netbios-dgm, netbios-ssn } accept comment "绕过NBNS流量"
ip daddr $CustomBypassIP accept comment "绕过某些地址"
fib daddr type local accept comment "本机绕过"
ip daddr $ReservedIP4 accept comment "保留地址绕过"
meta l4proto { tcp, udp } meta mark set $PROXY_FWMARK comment "其他流量重路由到prerouting"
}
}
EOF

echo "set nftables"

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

路径/etc/systemd/system/sing-box.service

1
2
3
4
5
6
7
8
9
10
11
12
13
[Unit]
Description=sing-box tproxy daemon.
After=network.target nss-lookup.target network-online.target

[Service]
Type=simple
LimitNOFILE=1000000
ExecStart=/path/to/sing-box/sing-box run -c /path/to/sing-box/sing-box-tproxy.json -D /path/to/sing-box/data
ExecStartPost=sh /path/to/sing-box/nftables.sh set
ExecStop=sh /path/to/sing-box/nftables.sh clear

[Install]
WantedBy=multi-user.target

客户端配置文件,路径/path/to/sing-box/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
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
{
"log": {
"disabled": false,
"level": "warn",
"timestamp": true
},
"dns": {
"servers": [
{
"tag": "tencentDNS",
"address": "tls://120.53.53.53",
"detour": "DIRECT"
},
{
"tag": "cloudflareDNS",
"address": "https://1.1.1.1/dns-query",
"detour": "PROXY"
},
{
"tag": "localDNS",
"address": "10.0.0.1",
"detour": "DIRECT"
},
{
"tag": "refusedDNS",
"address": "rcode://success"
},
{
"tag": "fakeipDNS",
"address": "fakeip"
}
],
"rules": [
{
"outbound": "any",
"server": "tencentDNS"
},
{
"query_type": [
"HTTPS",
"AAAA",
"SVCB"
],
"server": "refusedDNS",
"disable_cache": true
},
{
"invert": true,
"protocol": "dns",
"server": "refusedDNS",
"disable_cache": true
},
{
"rule_set": "geosite-private",
"server": "localDNS"
},
{
"rule_set": [
"geosite-geolocation-cn",
"geosite-win-update",
"geosite-apple"
],
"server": "tencentDNS"
},
{
"query_type": "A",
"rule_set": [
"geosite-google",
"geosite-github",
"geosite-telegram",
"geosite-steam",
"geosite-gfw"
],
"server": "fakeipDNS",
"rewrite_ttl": 10
},
{
"query_type": "A",
"server": "cloudflareDNS"
}
],
"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"
}
},
"inbounds": [
{
"tag": "TPROXY-IN",
"type": "tproxy",
"listen": "0.0.0.0",
"listen_port": 7895,
"udp_timeout": 180,
"sniff": true
},
{
"tag": "MIXED-MAIN-IN",
"type": "mixed",
"listen": "0.0.0.0",
"listen_port": 7900,
"udp_timeout": 180,
"sniff": true
}
],
"outbounds": [
{
"tag": "PROXY",
"type": "selector",
"outbounds": [
"PROXY_REALITY",
"PROXY_TROJAN"
],
"default": "PROXY_REALITY",
"interrupt_exist_connections": true
},
{
"tag": "FINAL",
"type": "selector",
"outbounds": [
"PROXY",
"DIRECT"
],
"default": "PROXY",
"interrupt_exist_connections": true
},
{
"tag": "APPLE",
"type": "selector",
"outbounds": [
"PROXY",
"DIRECT",
"PROXY_REALITY",
"PROXY_TROJAN"
],
"default": "DIRECT",
"interrupt_exist_connections": true
},
{
"tag": "STEAM",
"type": "selector",
"outbounds": [
"PROXY",
"DIRECT",
"PROXY_REALITY",
"PROXY_TROJAN"
],
"default": "PROXY",
"interrupt_exist_connections": true
},
{
"tag": "TELEGRAM",
"type": "selector",
"outbounds": [
"PROXY",
"DIRECT",
"PROXY_REALITY",
"PROXY_TROJAN"
],
"default": "PROXY_REALITY",
"interrupt_exist_connections": true
},
{
"tag": "DOWNLOADER",
"type": "selector",
"outbounds": [
"PROXY",
"DIRECT",
"PROXY_REALITY",
"PROXY_TROJAN"
],
"default": "DIRECT",
"interrupt_exist_connections": true
},
{
"tag": "PROXY_TROJAN",
"type": "trojan",
"server": "exp1.com",
"server_port": 443,
"password": "passwd",
"tls": {
"enabled": true,
"server_name": "exp1.com",
"insecure": false,
"utls": {
"enabled": true,
"fingerprint": "chrome"
}
},
"multiplex": {
"enabled": true,
"protocol": "h2mux",
"max_streams": 6,
"padding": true
}
},
{
"tag": "PROXY_REALITY",
"type": "vless",
"server": "11.22.33.44",
"server_port": 443,
"uuid": "uuid",
"tls": {
"enabled": true,
"server_name": "exp2.com",
"utls": {
"enabled": true,
"fingerprint": "chrome"
},
"reality": {
"enabled": true,
"public_key": "public_key",
"short_id": "short_id"
}
},
"multiplex": {
"enabled": true,
"protocol": "h2mux",
"max_streams": 6,
"padding": true
}
},
{
"tag": "DIRECT",
"type": "direct"
},
{
"tag": "DNS-OUT",
"type": "dns"
},
{
"tag": "BLOCK",
"type": "block"
}
],
"route": {
"rules": [
{
"protocol": "dns",
"outbound": "DNS-OUT"
},
{
"port": 53,
"outbound": "DNS-OUT"
},
{
"rule_set": "geosite-private",
"outbound": "DIRECT"
},
{
"clash_mode": "direct",
"outbound": "DIRECT"
},
{
"clash_mode": "global",
"outbound": "PROXY"
},
{
"rule_set": "geosite-apple",
"outbound": "APPLE"
},
{
"rule_set": [
"geosite-telegram",
"geoip-telegram"
],
"outbound": "TELEGRAM"
},
{
"rule_set": [
"geosite-google",
"geosite-github",
"geosite-gfw"
],
"outbound": "PROXY"
},
{
"rule_set": [
"geoip-private",
"geoip-cn",
"geosite-geolocation-cn",
"geosite-win-update"
],
"outbound": "DIRECT"
},
{
"rule_set": "geosite-steam",
"outbound": "STEAM"
},
{
"source_ip_cidr": "10.0.0.10/32",
"outbound": "DOWNLOADER"
}
],
"rule_set": [
{
"tag": "geosite-private",
"type": "remote",
"format": "source",
"url": "https://raw.githubusercontent.com/MetaCubeX/meta-rules-dat/sing/geo/geosite/private.json",
"download_detour": "PROXY_TROJAN",
"update_interval": "30d"
},
{
"tag": "geosite-google",
"type": "remote",
"format": "source",
"url": "https://raw.githubusercontent.com/MetaCubeX/meta-rules-dat/sing/geo/geosite/google.json",
"download_detour": "PROXY_TROJAN",
"update_interval": "30d"
},
{
"tag": "geosite-geolocation-cn",
"type": "remote",
"format": "source",
"url": "https://raw.githubusercontent.com/MetaCubeX/meta-rules-dat/sing/geo/geosite/geolocation-cn.json",
"download_detour": "PROXY_TROJAN",
"update_interval": "30d"
},
{
"tag": "geosite-win-update",
"type": "remote",
"format": "source",
"url": "https://raw.githubusercontent.com/MetaCubeX/meta-rules-dat/sing/geo/geosite/win-update.json",
"download_detour": "PROXY_TROJAN",
"update_interval": "30d"
},
{
"tag": "geosite-apple",
"type": "remote",
"format": "source",
"url": "https://raw.githubusercontent.com/MetaCubeX/meta-rules-dat/sing/geo/geosite/apple.json",
"download_detour": "PROXY_TROJAN",
"update_interval": "30d"
},
{
"tag": "geosite-github",
"type": "remote",
"format": "source",
"url": "https://raw.githubusercontent.com/MetaCubeX/meta-rules-dat/sing/geo/geosite/github.json",
"download_detour": "PROXY_TROJAN",
"update_interval": "30d"
},
{
"tag": "geoip-telegram",
"type": "remote",
"format": "source",
"url": "https://raw.githubusercontent.com/MetaCubeX/meta-rules-dat/sing/geo/geoip/telegram.json",
"download_detour": "PROXY_TROJAN",
"update_interval": "30d"
},
{
"tag": "geosite-telegram",
"type": "remote",
"format": "source",
"url": "https://raw.githubusercontent.com/MetaCubeX/meta-rules-dat/sing/geo/geosite/telegram.json",
"download_detour": "PROXY_TROJAN",
"update_interval": "30d"
},
{
"tag": "geosite-steam",
"type": "remote",
"format": "source",
"url": "https://raw.githubusercontent.com/MetaCubeX/meta-rules-dat/sing/geo/geosite/steam.json",
"download_detour": "PROXY_TROJAN",
"update_interval": "30d"
},
{
"tag": "geosite-gfw",
"type": "remote",
"format": "source",
"url": "https://raw.githubusercontent.com/MetaCubeX/meta-rules-dat/sing/geo/geosite/gfw.json",
"download_detour": "PROXY_TROJAN",
"update_interval": "30d"
},
{
"tag": "geoip-private",
"type": "remote",
"format": "source",
"url": "https://raw.githubusercontent.com/MetaCubeX/meta-rules-dat/sing/geo/geoip/private.json",
"download_detour": "PROXY_TROJAN",
"update_interval": "30d"
},
{
"tag": "geoip-cn",
"type": "remote",
"format": "source",
"url": "https://raw.githubusercontent.com/MetaCubeX/meta-rules-dat/sing/geo/geoip/cn.json",
"download_detour": "PROXY_TROJAN",
"update_interval": "30d"
}
],
"final": "FINAL",
"auto_detect_interface": true,
"default_mark": 666
},
"experimental": {
"clash_api": {
"external_controller": "0.0.0.0:9090",
"external_ui": "yacd",
"external_ui_download_url": "https://github.com/MetaCubeX/Yacd-meta/archive/gh-pages.zip",
"external_ui_download_detour": "PROXY_TROJAN",
"secret": "secret",
"default_mode": "rule"
}
}
}

服务端配置文件,前置haproxy复用443端口

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
{
"log": {
"disabled": false,
"level": "warn",
"timestamp": true
},
"inbounds": [
{
"type": "vless",
"tag": "vless-reality",
"listen": "0.0.0.0",
"listen_port": 500,
"users": [
{
"uuid": "uuid"
}
],
"tls": {
"enabled": true,
"server_name": "exp2.com",
"reality": {
"enabled": true,
"handshake": {
"server": "xx.com",
"server_port": 443
},
"private_key": "private_key",
"short_id": ["short_id"]
}
},
"multiplex": {
"enabled": true,
"padding": true
}
},
{
"type": "trojan",
"tag": "trojan",
"listen": "0.0.0.0",
"listen_port": 501,
"users": [
{
"password": "password"
}
],
"tls": {
"enabled": true,
"server_name": "exp1.com",
"alpn": ["h2", "http/1.1"],
"acme": {
"domain": "exp1.com",
"data_directory": "/path/to/sing-box/data/cert",
"email": "[email protected]",
"provider": "letsencrypt",
"dns01_challenge": {
"provider": "cloudflare",
"api_token": "api_token"
}
}
},
"multiplex": {
"enabled": true,
"padding": true
}
}
],
"outbounds": [
{
"type": "direct",
"tag": "direct"
}
]
}

路由和规则匹配

在透明代理环境中,程序在发起连接前首先需要知道目标IP地址,也就是DNS查询,拿到IP后再建立连接。这在sing-box路由中体现为2次入站:

  • 第一次:nftables判断目标端口是否为53,透明代理入站sing-box,route.rules中匹配DNS规则分流到DNS模块处理DNS查询。
  • 第二次:程序拿着IP透明代理入站sing-box,路由和规则匹配,走代理或直连出站。

telegram等程序已经有目标IP的情况下,则没有DNS查询步骤,入站就是IP。

如果程序使用socks5或http代理入站,DNS查询通常是由代理服务器负责,此时入站是域名,DNS查询发生在出站的时候(本地直接出站或走代理到服务端出站)。

如果使用fakeIP呢?

如果DNS全返回fakeIP

  • 对于需要走代理的连接,本地出站时是传域名到服务器重新进行DNS查询建立连接。
  • 对于需要直连的连接,本地出站时也是域名,此时sing-box会发起DNS查询,dns.rules时匹配outound:any返回正常的IP地址再建立连接。

从以上不难看出,fakeIP对于明确需要走代理的连接很有用,相当于在本地省略了一次DNS查询(sing-box立即返回fakeIP),由服务端进行DNS查询建立连接。

但由于DNS查询都返回fakeIP,匹配IP类规则失去意义,两者在路由时都只能开启sniff匹配域名规则。

如果DNS全返回正常IP

  • 对于需要走代理的连接,出站时传IP到服务端建立连接。
  • 对于需要直连的连接,就直接IP出站建立连接。

对于此种方案,优点是可以开启sniff同时匹配域名规则和IP规则。但缺点是对于需要走代理的连接,传到服务端的可能是被污染的IP,或者干脆在本地就对污染的IP匹配到错误的规则导致分流不符合预期。如果全使用国外DNS服务器走代理查询,速度上也很慢。

sniff_override_destination

如果在服务端入站启用sniff_override_destination,无论入站是IP还是域名,通通覆盖IP为域名重新进行DNS查询是否可行?这样不就解决服务端拿到IP是污染的问题了吗?但这样有个弊端。

有些连接目标IP地址是确定的,或者说是硬编码的,这种不需要DNS查询直接IP入站。这种情况下,如果强行用sniff到的域名覆盖目标地址重新进行DNS查询建立连接,可能会导致连接出现各种问题,比如sniff到的域名解析出的IP地址和程序的预期不符,或者干脆就是无效的域名。

折中方案

那么有没有一种方案既可以保留fakeIP优点,又可以最大限度的保证DNS查询的速度和准确,并且尽可能同时匹配域名和IP类规则?折中的方案是:

  • 仅对明确需要走代理的域名返回fakeIP,出站走代理。
  • 需要直连的域名就用国内的DNS服务器查询,出站走直连。
  • 其他连接用国外DNS服务器走代理查询,拿到可信IP后出站走代理。

启动

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

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

使用

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

关于nftables

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

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

Docker

Docker 目前仍然操纵 iptables 规则来使 docker 网络工作,建议不要在该网关服务器上使用 docker 相关服务,以免出现各种问题。

参考