devconfig-gen-singbox 2.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- devconfig_gen/__init__.py +67 -0
- devconfig_gen/cli.py +163 -0
- devconfig_gen/engine.py +217 -0
- devconfig_gen/formats.py +708 -0
- devconfig_gen/models.py +135 -0
- devconfig_gen/providers/__init__.py +8 -0
- devconfig_gen/providers/custom.py +92 -0
- devconfig_gen/providers/env_provider.py +184 -0
- devconfig_gen/providers/json_provider.py +84 -0
- devconfig_gen/providers/singbox/__init__.py +9 -0
- devconfig_gen/providers/singbox/data/rules.json +97 -0
- devconfig_gen/providers/singbox/links.py +24 -0
- devconfig_gen/providers/singbox/models.py +76 -0
- devconfig_gen/providers/singbox/plugins/__init__.py +33 -0
- devconfig_gen/providers/singbox/plugins/anytls.py +129 -0
- devconfig_gen/providers/singbox/plugins/base.py +117 -0
- devconfig_gen/providers/singbox/plugins/hysteria2.py +110 -0
- devconfig_gen/providers/singbox/plugins/tuic.py +90 -0
- devconfig_gen/providers/singbox/provider.py +362 -0
- devconfig_gen/providers/singbox/route.py +369 -0
- devconfig_gen/providers/singbox/schema.py +362 -0
- devconfig_gen/py.typed +0 -0
- devconfig_gen/registry.py +39 -0
- devconfig_gen/validation.py +148 -0
- devconfig_gen_singbox-2.0.0.dist-info/METADATA +343 -0
- devconfig_gen_singbox-2.0.0.dist-info/RECORD +30 -0
- devconfig_gen_singbox-2.0.0.dist-info/WHEEL +5 -0
- devconfig_gen_singbox-2.0.0.dist-info/entry_points.txt +3 -0
- devconfig_gen_singbox-2.0.0.dist-info/licenses/LICENSE +201 -0
- devconfig_gen_singbox-2.0.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,362 @@
|
|
|
1
|
+
"""The sing-box provider: validation, variant dispatch, and assembly.
|
|
2
|
+
|
|
3
|
+
This module orchestrates plugins, route assembly, and share-link aggregation.
|
|
4
|
+
It never mentions a protocol field name; adding or changing a variant is a
|
|
5
|
+
plugin-only change.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from typing import Any, Mapping, Sequence
|
|
11
|
+
|
|
12
|
+
from ... import formats
|
|
13
|
+
from ...models import (
|
|
14
|
+
Diagnostic,
|
|
15
|
+
GeneratedArtifact,
|
|
16
|
+
GenerationRequest,
|
|
17
|
+
GenerationResult,
|
|
18
|
+
ProviderField,
|
|
19
|
+
ProviderStep,
|
|
20
|
+
)
|
|
21
|
+
from ...validation import ValidationError
|
|
22
|
+
from . import links, route, schema
|
|
23
|
+
from .models import BuildContext
|
|
24
|
+
from .plugins import get_plugin
|
|
25
|
+
from .schema import ParsedContext
|
|
26
|
+
|
|
27
|
+
CLIENT_TUN_INBOUND_TAG = "tun-in"
|
|
28
|
+
CLIENT_TUN_ADDRESSES = ["172.19.0.1/30"]
|
|
29
|
+
CLIENT_TUN_STACK = "gvisor"
|
|
30
|
+
URLTEST_URL = "https://cp.cloudflare.com/generate_204"
|
|
31
|
+
CLIENT_PROXY_BEST_TAG = "global"
|
|
32
|
+
CLIENT_PROXY_AUTO_TAG = "proxy-auto"
|
|
33
|
+
CLIENT_ROUTE_MODE_TAG = "route-mode"
|
|
34
|
+
CLIENT_ROUTE_TAG = "route"
|
|
35
|
+
DIRECT_DOMAIN_RESOLVER = {"server": "dns-direct", "strategy": "prefer_ipv4"}
|
|
36
|
+
TUN_EXCLUDED_ROUTES = [
|
|
37
|
+
# Android VpnService already excludes loopback/link-local; excluding them
|
|
38
|
+
# explicitly makes `configure tun interface: Bad address` (sing-box #2030).
|
|
39
|
+
"10.0.0.0/8",
|
|
40
|
+
"100.64.0.0/10",
|
|
41
|
+
"172.16.0.0/12",
|
|
42
|
+
"192.168.0.0/16",
|
|
43
|
+
]
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class SingBoxProvider:
|
|
47
|
+
"""Generate sing-box server/client configs and share links."""
|
|
48
|
+
|
|
49
|
+
name = "singbox"
|
|
50
|
+
|
|
51
|
+
steps: Sequence[ProviderStep] = (
|
|
52
|
+
ProviderStep(
|
|
53
|
+
id="network",
|
|
54
|
+
title="Network",
|
|
55
|
+
description="Domain, subdomain prefixes, tunnel mode, and protocols.",
|
|
56
|
+
i18n={"zh": {"title": "网络", "description": "域名、子域前缀、隧道模式与协议。"}},
|
|
57
|
+
fields=(
|
|
58
|
+
ProviderField(
|
|
59
|
+
"network.domain_root",
|
|
60
|
+
type="string",
|
|
61
|
+
required=True,
|
|
62
|
+
title="Domain root",
|
|
63
|
+
description="Base domain; protocol hosts are <prefix>.<domain_root>.",
|
|
64
|
+
i18n={"zh": {"title": "根域名", "description": "基础域名;协议主机为 <前缀>.<根域名>。"}},
|
|
65
|
+
),
|
|
66
|
+
ProviderField(
|
|
67
|
+
"network.subdomain_prefixes",
|
|
68
|
+
type="mapping",
|
|
69
|
+
required=True,
|
|
70
|
+
title="Subdomain prefixes",
|
|
71
|
+
description="Explicit prefixes keyed by reality / tuic / hy2.",
|
|
72
|
+
i18n={"zh": {"title": "子域前缀", "description": "显式前缀,键为 reality / tuic / hy2。"}},
|
|
73
|
+
),
|
|
74
|
+
ProviderField(
|
|
75
|
+
"network.tunnel_mode",
|
|
76
|
+
type="string",
|
|
77
|
+
required=True,
|
|
78
|
+
default="proxy",
|
|
79
|
+
choices=("none", "proxy", "tun"),
|
|
80
|
+
title="Tunnel mode",
|
|
81
|
+
description="Server outbound mode.",
|
|
82
|
+
i18n={"zh": {"title": "隧道模式", "description": "服务端出站模式。"}},
|
|
83
|
+
),
|
|
84
|
+
ProviderField(
|
|
85
|
+
"network.protocols",
|
|
86
|
+
type="document",
|
|
87
|
+
required=True,
|
|
88
|
+
title="Protocols",
|
|
89
|
+
description="One entry per protocol (anytls / tuic / hysteria2).",
|
|
90
|
+
i18n={"zh": {"title": "协议", "description": "每个协议一个条目(anytls / tuic / hysteria2)。"}},
|
|
91
|
+
),
|
|
92
|
+
),
|
|
93
|
+
),
|
|
94
|
+
ProviderStep(
|
|
95
|
+
id="routing",
|
|
96
|
+
title="Routing",
|
|
97
|
+
description="Embedded or custom rules and DNS servers.",
|
|
98
|
+
i18n={"zh": {"title": "路由", "description": "内嵌或自定义规则与 DNS 服务器。"}},
|
|
99
|
+
fields=(
|
|
100
|
+
ProviderField(
|
|
101
|
+
"network.routing",
|
|
102
|
+
type="document",
|
|
103
|
+
title="Routing",
|
|
104
|
+
description="rules_source (embedded/custom), geoip_cn, custom_rules.",
|
|
105
|
+
i18n={"zh": {"title": "路由规则", "description": "rules_source(embedded/custom)、geoip_cn、custom_rules。"}},
|
|
106
|
+
),
|
|
107
|
+
ProviderField(
|
|
108
|
+
"network.dns",
|
|
109
|
+
type="document",
|
|
110
|
+
title="DNS",
|
|
111
|
+
description="direct_servers and remote_server.",
|
|
112
|
+
i18n={"zh": {"title": "DNS", "description": "direct_servers 与 remote_server。"}},
|
|
113
|
+
),
|
|
114
|
+
),
|
|
115
|
+
),
|
|
116
|
+
ProviderStep(
|
|
117
|
+
id="client",
|
|
118
|
+
title="Client",
|
|
119
|
+
description="Client-only options.",
|
|
120
|
+
i18n={"zh": {"title": "客户端", "description": "仅客户端使用的选项。"}},
|
|
121
|
+
fields=(
|
|
122
|
+
ProviderField(
|
|
123
|
+
"client.server_ip",
|
|
124
|
+
type="string",
|
|
125
|
+
title="Server IP",
|
|
126
|
+
description="Excluded from the TUN route.",
|
|
127
|
+
i18n={"zh": {"title": "服务器 IP", "description": "从 TUN 路由中排除。"}},
|
|
128
|
+
),
|
|
129
|
+
ProviderField(
|
|
130
|
+
"client.fingerprint",
|
|
131
|
+
type="string",
|
|
132
|
+
default="chrome",
|
|
133
|
+
title="uTLS fingerprint",
|
|
134
|
+
description="uTLS fingerprint for client outbounds.",
|
|
135
|
+
i18n={"zh": {"title": "uTLS 指纹", "description": "客户端出站的 uTLS 指纹。"}},
|
|
136
|
+
),
|
|
137
|
+
),
|
|
138
|
+
),
|
|
139
|
+
ProviderStep(
|
|
140
|
+
id="output",
|
|
141
|
+
title="Output",
|
|
142
|
+
description="Which artifacts to emit and in which format.",
|
|
143
|
+
i18n={"zh": {"title": "输出", "description": "生成哪些产物以及格式。"}},
|
|
144
|
+
fields=(
|
|
145
|
+
ProviderField(
|
|
146
|
+
"options.target",
|
|
147
|
+
type="string",
|
|
148
|
+
default="both",
|
|
149
|
+
choices=("server", "client", "both"),
|
|
150
|
+
title="Target",
|
|
151
|
+
description="server, client, or both.",
|
|
152
|
+
i18n={"zh": {"title": "目标", "description": "server、client 或 both。"}},
|
|
153
|
+
),
|
|
154
|
+
ProviderField(
|
|
155
|
+
"options.format",
|
|
156
|
+
type="string",
|
|
157
|
+
default="json",
|
|
158
|
+
choices=("json", "yaml"),
|
|
159
|
+
title="Format",
|
|
160
|
+
description="Artifact format.",
|
|
161
|
+
i18n={"zh": {"title": "格式", "description": "产物格式。"}},
|
|
162
|
+
),
|
|
163
|
+
),
|
|
164
|
+
),
|
|
165
|
+
)
|
|
166
|
+
|
|
167
|
+
def describe_schema(self) -> Sequence[ProviderStep]:
|
|
168
|
+
return self.steps
|
|
169
|
+
|
|
170
|
+
def diagnose(self, request: GenerationRequest) -> Sequence[Diagnostic]:
|
|
171
|
+
_, diagnostics = schema.parse(request.context, request.options)
|
|
172
|
+
return tuple(diagnostics)
|
|
173
|
+
|
|
174
|
+
def validate(self, request: GenerationRequest):
|
|
175
|
+
return tuple(item.message for item in self.diagnose(request))
|
|
176
|
+
|
|
177
|
+
def generate(self, request: GenerationRequest) -> GenerationResult:
|
|
178
|
+
parsed, diagnostics = schema.parse(request.context, request.options)
|
|
179
|
+
if diagnostics:
|
|
180
|
+
raise ValidationError(diagnostics)
|
|
181
|
+
|
|
182
|
+
fmt = str(parsed.options.get("format", "json"))
|
|
183
|
+
target = str(parsed.options.get("target", "both"))
|
|
184
|
+
media_type = formats.media_type_for(fmt)
|
|
185
|
+
ctx = _build_context(parsed)
|
|
186
|
+
enabled = parsed.enabled_protocols()
|
|
187
|
+
|
|
188
|
+
artifacts = []
|
|
189
|
+
if target != "client":
|
|
190
|
+
artifacts.append(
|
|
191
|
+
GeneratedArtifact(
|
|
192
|
+
name=f"sing-box.server.{fmt}",
|
|
193
|
+
content=_build_server(parsed, ctx),
|
|
194
|
+
media_type=media_type,
|
|
195
|
+
)
|
|
196
|
+
)
|
|
197
|
+
if target != "server":
|
|
198
|
+
artifacts.append(
|
|
199
|
+
GeneratedArtifact(
|
|
200
|
+
name=f"sing-box.client.{fmt}",
|
|
201
|
+
content=_build_client(parsed, ctx),
|
|
202
|
+
media_type=media_type,
|
|
203
|
+
)
|
|
204
|
+
)
|
|
205
|
+
artifacts.append(
|
|
206
|
+
GeneratedArtifact(
|
|
207
|
+
name="sing-box-links.txt",
|
|
208
|
+
content=links.build_links(enabled, ctx),
|
|
209
|
+
media_type="text/plain",
|
|
210
|
+
)
|
|
211
|
+
)
|
|
212
|
+
return GenerationResult(provider=self.name, artifacts=tuple(artifacts))
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
def _build_context(parsed: ParsedContext) -> BuildContext:
|
|
216
|
+
return BuildContext(
|
|
217
|
+
network=parsed.network,
|
|
218
|
+
client=parsed.client,
|
|
219
|
+
hosts=parsed.hosts,
|
|
220
|
+
options=parsed.options,
|
|
221
|
+
)
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
def _server_outbounds(tunnel_mode: str) -> Mapping[str, Any]:
|
|
225
|
+
if tunnel_mode == "proxy":
|
|
226
|
+
return {
|
|
227
|
+
"tag": "warp-out",
|
|
228
|
+
"outbounds": [
|
|
229
|
+
{
|
|
230
|
+
"type": "socks",
|
|
231
|
+
"tag": "warp-out",
|
|
232
|
+
"server": "127.0.0.1",
|
|
233
|
+
"server_port": 40000,
|
|
234
|
+
"version": "5",
|
|
235
|
+
},
|
|
236
|
+
{"type": "direct", "tag": "direct"},
|
|
237
|
+
],
|
|
238
|
+
}
|
|
239
|
+
if tunnel_mode == "tun":
|
|
240
|
+
return {
|
|
241
|
+
"tag": "warp-out",
|
|
242
|
+
"outbounds": [
|
|
243
|
+
{"type": "direct", "tag": "warp-out"},
|
|
244
|
+
{"type": "direct", "tag": "direct"},
|
|
245
|
+
],
|
|
246
|
+
}
|
|
247
|
+
return {"tag": "direct", "outbounds": [{"type": "direct", "tag": "direct"}]}
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
def _build_server(parsed: Any, ctx: BuildContext) -> dict:
|
|
251
|
+
network = parsed.network
|
|
252
|
+
inbounds = []
|
|
253
|
+
inbound_tags = []
|
|
254
|
+
sniff_inbounds = []
|
|
255
|
+
for cfg in parsed.enabled_protocols():
|
|
256
|
+
plugin = get_plugin(cfg.type)
|
|
257
|
+
if plugin is None:
|
|
258
|
+
continue
|
|
259
|
+
inbounds.append(plugin.build_server_inbound(cfg, ctx))
|
|
260
|
+
inbound_tags.append(plugin.inbound_tag)
|
|
261
|
+
if getattr(plugin, "sniff_inbound", False):
|
|
262
|
+
sniff_inbounds.append(plugin.inbound_tag)
|
|
263
|
+
|
|
264
|
+
outbounds = _server_outbounds(network.tunnel_mode)
|
|
265
|
+
return {
|
|
266
|
+
"log": {"disabled": True},
|
|
267
|
+
"dns": route.build_server_dns(network),
|
|
268
|
+
"inbounds": inbounds,
|
|
269
|
+
"outbounds": outbounds["outbounds"],
|
|
270
|
+
"route": route.build_server_route(
|
|
271
|
+
network, inbound_tags, outbounds["tag"], sniff_inbounds
|
|
272
|
+
),
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
def _protocol_domains(parsed: Any, ctx: BuildContext) -> list:
|
|
277
|
+
domains = []
|
|
278
|
+
for cfg in parsed.enabled_protocols():
|
|
279
|
+
plugin = get_plugin(cfg.type)
|
|
280
|
+
if plugin is None:
|
|
281
|
+
continue
|
|
282
|
+
host = ctx.host(plugin.host_key)
|
|
283
|
+
if host:
|
|
284
|
+
domains.append(host)
|
|
285
|
+
return domains
|
|
286
|
+
|
|
287
|
+
|
|
288
|
+
def _client_outbounds(parsed: Any, ctx: BuildContext) -> list:
|
|
289
|
+
outbound_tags = []
|
|
290
|
+
variant_outbounds = []
|
|
291
|
+
for cfg in parsed.enabled_protocols():
|
|
292
|
+
plugin = get_plugin(cfg.type)
|
|
293
|
+
if plugin is None:
|
|
294
|
+
continue
|
|
295
|
+
outbound_tags.append(plugin.outbound_tag)
|
|
296
|
+
variant_outbounds.append(plugin.build_client_outbound(cfg, ctx))
|
|
297
|
+
|
|
298
|
+
result = [
|
|
299
|
+
{
|
|
300
|
+
"type": "selector",
|
|
301
|
+
"tag": CLIENT_ROUTE_MODE_TAG,
|
|
302
|
+
"outbounds": [CLIENT_ROUTE_TAG, CLIENT_PROXY_BEST_TAG, "direct"],
|
|
303
|
+
"default": CLIENT_ROUTE_TAG,
|
|
304
|
+
"interrupt_exist_connections": True,
|
|
305
|
+
},
|
|
306
|
+
{
|
|
307
|
+
"type": "selector",
|
|
308
|
+
"tag": CLIENT_PROXY_BEST_TAG,
|
|
309
|
+
"outbounds": [CLIENT_PROXY_AUTO_TAG, *outbound_tags, CLIENT_ROUTE_TAG],
|
|
310
|
+
"default": CLIENT_PROXY_AUTO_TAG,
|
|
311
|
+
"interrupt_exist_connections": True,
|
|
312
|
+
},
|
|
313
|
+
{
|
|
314
|
+
"type": "urltest",
|
|
315
|
+
"tag": CLIENT_PROXY_AUTO_TAG,
|
|
316
|
+
"outbounds": outbound_tags,
|
|
317
|
+
"url": URLTEST_URL,
|
|
318
|
+
"interval": "5m",
|
|
319
|
+
"tolerance": 150,
|
|
320
|
+
"interrupt_exist_connections": True,
|
|
321
|
+
},
|
|
322
|
+
]
|
|
323
|
+
result.extend(variant_outbounds)
|
|
324
|
+
result.append({"type": "direct", "tag": CLIENT_ROUTE_TAG})
|
|
325
|
+
result.append({"type": "direct", "tag": "direct", "domain_resolver": dict(DIRECT_DOMAIN_RESOLVER)})
|
|
326
|
+
result.append({"type": "block", "tag": "block"})
|
|
327
|
+
return result
|
|
328
|
+
|
|
329
|
+
|
|
330
|
+
def _tun_route_exclude(server_ip: Any) -> list:
|
|
331
|
+
routes = list(TUN_EXCLUDED_ROUTES)
|
|
332
|
+
if not server_ip:
|
|
333
|
+
return routes
|
|
334
|
+
ip = str(server_ip).strip()
|
|
335
|
+
if not ip:
|
|
336
|
+
return routes
|
|
337
|
+
routes.append(f"{ip}/128" if ":" in ip else f"{ip}/32")
|
|
338
|
+
return routes
|
|
339
|
+
|
|
340
|
+
|
|
341
|
+
def _build_client(parsed: Any, ctx: BuildContext) -> dict:
|
|
342
|
+
network = parsed.network
|
|
343
|
+
return {
|
|
344
|
+
"log": {"level": "debug", "timestamp": True},
|
|
345
|
+
"http_clients": [{"tag": "direct-client", "detour": "direct"}],
|
|
346
|
+
"dns": route.build_client_dns(network, _protocol_domains(parsed, ctx)),
|
|
347
|
+
"inbounds": [
|
|
348
|
+
{
|
|
349
|
+
"type": "tun",
|
|
350
|
+
"tag": CLIENT_TUN_INBOUND_TAG,
|
|
351
|
+
# Android expects the tun addresses as an array.
|
|
352
|
+
"address": list(CLIENT_TUN_ADDRESSES),
|
|
353
|
+
"auto_route": True,
|
|
354
|
+
"strict_route": True,
|
|
355
|
+
"route_exclude_address": _tun_route_exclude(parsed.client.server_ip),
|
|
356
|
+
"stack": CLIENT_TUN_STACK,
|
|
357
|
+
"mtu": 1280,
|
|
358
|
+
}
|
|
359
|
+
],
|
|
360
|
+
"outbounds": _client_outbounds(parsed, ctx),
|
|
361
|
+
"route": route.build_client_route(network, CLIENT_TUN_INBOUND_TAG),
|
|
362
|
+
}
|