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.
@@ -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
+ }