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,369 @@
|
|
|
1
|
+
"""DNS and route assembly for the sing-box provider.
|
|
2
|
+
|
|
3
|
+
This module is variant-neutral: it receives the inbound tags, the outbound tag,
|
|
4
|
+
the protocol host domains, and the routing options, and never mentions a
|
|
5
|
+
protocol name. The embedded rule table is loaded from ``data/rules.json`` using
|
|
6
|
+
``Path(__file__).parent`` so the provider works from an installed package with
|
|
7
|
+
no external files.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import json
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
from typing import Any, Mapping, Sequence
|
|
15
|
+
|
|
16
|
+
_RULES_PATH = Path(__file__).parent / "data" / "rules.json"
|
|
17
|
+
|
|
18
|
+
REQUIRED_BUCKETS = (
|
|
19
|
+
"direct_exact",
|
|
20
|
+
"proxy_exact",
|
|
21
|
+
"direct_suffix",
|
|
22
|
+
"proxy_suffix",
|
|
23
|
+
"direct_keyword",
|
|
24
|
+
"proxy_keyword",
|
|
25
|
+
"direct_cidr",
|
|
26
|
+
"proxy_cidr",
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
DNS_DIRECT_SERVER = ["223.5.5.5", "119.29.29.29"]
|
|
30
|
+
DNS_REMOTE_SERVER = "1.1.1.1"
|
|
31
|
+
DNS_REMOTE_PATH = "/dns-query"
|
|
32
|
+
DNS_REMOTE_TLS_SERVER_NAME = "cloudflare-dns.com"
|
|
33
|
+
|
|
34
|
+
SERVER_DNS_TAG = "dns-server"
|
|
35
|
+
CLIENT_DNS_DIRECT_TAG = "dns-direct"
|
|
36
|
+
CLIENT_DNS_REMOTE_TAG = "dns-remote"
|
|
37
|
+
CLIENT_PROXY_TAG = "global"
|
|
38
|
+
|
|
39
|
+
GEOIP_CN_RULESET_URL = "https://testingcf.jsdelivr.net/gh/MetaCubeX/meta-rules-dat@sing/geo/geoip/cn.srs"
|
|
40
|
+
GEOSITE_CN_RULESET_URL = "https://testingcf.jsdelivr.net/gh/MetaCubeX/meta-rules-dat@sing/geo/geosite/cn.srs"
|
|
41
|
+
GEOSITE_GEOLOCATION_NON_CN_RULESET_URL = (
|
|
42
|
+
"https://testingcf.jsdelivr.net/gh/MetaCubeX/meta-rules-dat@sing/geo/geosite/geolocation-!cn.srs"
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
SKIP_PROXY_DOMAINS = ["localhost", "captive.apple.com"]
|
|
46
|
+
SKIP_PROXY_SUFFIXES = ["local"]
|
|
47
|
+
DNS_DIRECT_ONLY_DOMAINS = ["cp.cloudflare.com", "generate_204"]
|
|
48
|
+
DNS_DIRECT_ONLY_SUFFIXES = ["in-addr.arpa", "ip6.arpa"]
|
|
49
|
+
APNS_PROXY_SUFFIXES = ["push.apple.com"]
|
|
50
|
+
APNS_PROXY_CIDR = [
|
|
51
|
+
"17.0.0.0/8",
|
|
52
|
+
"2403:300:a42::/48",
|
|
53
|
+
"2403:300:a51::/48",
|
|
54
|
+
"2620:149:a44::/48",
|
|
55
|
+
"2a01:b740:a42::/48",
|
|
56
|
+
]
|
|
57
|
+
APNS_PROXY_PORTS = [443, 5223, 2197]
|
|
58
|
+
|
|
59
|
+
ROUTE_FINAL = "route-mode"
|
|
60
|
+
DIRECT_RULE_OUTBOUND = ROUTE_FINAL
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _merge_unique(*groups: Sequence[Any]) -> list:
|
|
64
|
+
merged: list = []
|
|
65
|
+
for group in groups:
|
|
66
|
+
for item in group:
|
|
67
|
+
if item not in merged:
|
|
68
|
+
merged.append(item)
|
|
69
|
+
return merged
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def _load_embedded() -> dict:
|
|
73
|
+
raw = json.loads(_RULES_PATH.read_text(encoding="utf-8"))
|
|
74
|
+
if not isinstance(raw, Mapping):
|
|
75
|
+
raise ValueError("rules.json must contain a mapping")
|
|
76
|
+
for key in REQUIRED_BUCKETS:
|
|
77
|
+
if key not in raw or not isinstance(raw[key], list):
|
|
78
|
+
raise ValueError(f"rules.json missing required list bucket: {key}")
|
|
79
|
+
return raw
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def load_rules(routing: Mapping[str, Any]) -> dict:
|
|
83
|
+
"""Return the effective rule buckets for the given ``network.routing``."""
|
|
84
|
+
|
|
85
|
+
routing = routing if isinstance(routing, Mapping) else {}
|
|
86
|
+
source = str(routing.get("rules_source") or "embedded").strip().lower()
|
|
87
|
+
custom = routing.get("custom_rules")
|
|
88
|
+
custom = custom if isinstance(custom, Mapping) else {}
|
|
89
|
+
|
|
90
|
+
if source == "custom":
|
|
91
|
+
buckets = {key: [] for key in REQUIRED_BUCKETS}
|
|
92
|
+
else:
|
|
93
|
+
embedded = _load_embedded()
|
|
94
|
+
buckets = {key: list(embedded[key]) for key in REQUIRED_BUCKETS}
|
|
95
|
+
|
|
96
|
+
for key, value in custom.items():
|
|
97
|
+
if key not in buckets or not isinstance(value, list):
|
|
98
|
+
continue
|
|
99
|
+
buckets[key] = _merge_unique(buckets[key], value)
|
|
100
|
+
return buckets
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def _geoip_enabled(routing: Mapping[str, Any]) -> bool:
|
|
104
|
+
if not isinstance(routing, Mapping):
|
|
105
|
+
return True
|
|
106
|
+
return bool(routing.get("geoip_cn", True))
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def _dns_direct_servers(dns: Mapping[str, Any]) -> list:
|
|
110
|
+
servers = dns.get("direct_servers") if isinstance(dns, Mapping) else None
|
|
111
|
+
if isinstance(servers, list) and servers:
|
|
112
|
+
return [str(item) for item in servers]
|
|
113
|
+
return list(DNS_DIRECT_SERVER)
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def _dns_remote_server(dns: Mapping[str, Any]) -> str:
|
|
117
|
+
server = dns.get("remote_server") if isinstance(dns, Mapping) else None
|
|
118
|
+
if isinstance(server, str) and server.strip():
|
|
119
|
+
return server.strip()
|
|
120
|
+
return DNS_REMOTE_SERVER
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def build_server_dns(network: Any) -> dict:
|
|
124
|
+
"""Server-side DNS: a single DoH server used for resolution/sniffing."""
|
|
125
|
+
|
|
126
|
+
remote = _dns_remote_server(network.dns)
|
|
127
|
+
return {
|
|
128
|
+
"servers": [
|
|
129
|
+
{
|
|
130
|
+
"type": "https",
|
|
131
|
+
"tag": SERVER_DNS_TAG,
|
|
132
|
+
"server": remote,
|
|
133
|
+
"path": DNS_REMOTE_PATH,
|
|
134
|
+
"tls": {
|
|
135
|
+
"enabled": True,
|
|
136
|
+
"server_name": DNS_REMOTE_TLS_SERVER_NAME,
|
|
137
|
+
"alpn": ["h2", "http/1.1"],
|
|
138
|
+
},
|
|
139
|
+
}
|
|
140
|
+
],
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def build_server_route(
|
|
145
|
+
network: Any,
|
|
146
|
+
inbound_tags: Sequence[str],
|
|
147
|
+
outbound_tag: str,
|
|
148
|
+
sniff_inbounds: Sequence[str] = (),
|
|
149
|
+
) -> dict:
|
|
150
|
+
"""Server route: resolve/sniff selected inbounds, then route them out."""
|
|
151
|
+
|
|
152
|
+
rules: list = []
|
|
153
|
+
for tag in sniff_inbounds:
|
|
154
|
+
rules.append(
|
|
155
|
+
{
|
|
156
|
+
"inbound": tag,
|
|
157
|
+
"action": "resolve",
|
|
158
|
+
"server": SERVER_DNS_TAG,
|
|
159
|
+
"strategy": "prefer_ipv4",
|
|
160
|
+
}
|
|
161
|
+
)
|
|
162
|
+
rules.append({"inbound": tag, "action": "sniff", "timeout": "1s"})
|
|
163
|
+
rules.append(
|
|
164
|
+
{
|
|
165
|
+
"inbound": list(inbound_tags),
|
|
166
|
+
"action": "route",
|
|
167
|
+
"outbound": outbound_tag,
|
|
168
|
+
}
|
|
169
|
+
)
|
|
170
|
+
return {
|
|
171
|
+
"rules": rules,
|
|
172
|
+
"final": outbound_tag,
|
|
173
|
+
"default_domain_resolver": SERVER_DNS_TAG,
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def build_client_dns(network: Any, protocol_domains: Sequence[str]) -> dict:
|
|
178
|
+
"""Client-side split DNS driven by the embedded/custom rule buckets."""
|
|
179
|
+
|
|
180
|
+
rules_data = load_rules(network.routing)
|
|
181
|
+
direct_servers = _dns_direct_servers(network.dns)
|
|
182
|
+
remote_server = _dns_remote_server(network.dns)
|
|
183
|
+
|
|
184
|
+
dns_rules: list = []
|
|
185
|
+
if protocol_domains:
|
|
186
|
+
dns_rules.append({"domain": list(protocol_domains), "server": CLIENT_DNS_DIRECT_TAG})
|
|
187
|
+
if DNS_DIRECT_ONLY_DOMAINS:
|
|
188
|
+
dns_rules.append({"domain": list(DNS_DIRECT_ONLY_DOMAINS), "server": CLIENT_DNS_DIRECT_TAG})
|
|
189
|
+
|
|
190
|
+
direct_exact = _merge_unique(SKIP_PROXY_DOMAINS, rules_data["direct_exact"])
|
|
191
|
+
direct_suffix = _merge_unique(
|
|
192
|
+
SKIP_PROXY_SUFFIXES, DNS_DIRECT_ONLY_SUFFIXES, rules_data["direct_suffix"]
|
|
193
|
+
)
|
|
194
|
+
proxy_suffix = _merge_unique(APNS_PROXY_SUFFIXES, rules_data["proxy_suffix"])
|
|
195
|
+
|
|
196
|
+
dns_rules.append({"domain_suffix": list(APNS_PROXY_SUFFIXES), "server": CLIENT_DNS_REMOTE_TAG})
|
|
197
|
+
if direct_exact:
|
|
198
|
+
dns_rules.append({"domain": direct_exact, "server": CLIENT_DNS_DIRECT_TAG})
|
|
199
|
+
if rules_data["proxy_exact"]:
|
|
200
|
+
dns_rules.append({"domain": list(rules_data["proxy_exact"]), "server": CLIENT_DNS_REMOTE_TAG})
|
|
201
|
+
if direct_suffix:
|
|
202
|
+
dns_rules.append({"domain_suffix": direct_suffix, "server": CLIENT_DNS_DIRECT_TAG})
|
|
203
|
+
if proxy_suffix:
|
|
204
|
+
dns_rules.append({"domain_suffix": proxy_suffix, "server": CLIENT_DNS_REMOTE_TAG})
|
|
205
|
+
if rules_data["direct_keyword"]:
|
|
206
|
+
dns_rules.append(
|
|
207
|
+
{"domain_keyword": list(rules_data["direct_keyword"]), "server": CLIENT_DNS_DIRECT_TAG}
|
|
208
|
+
)
|
|
209
|
+
if rules_data["proxy_keyword"]:
|
|
210
|
+
dns_rules.append(
|
|
211
|
+
{"domain_keyword": list(rules_data["proxy_keyword"]), "server": CLIENT_DNS_REMOTE_TAG}
|
|
212
|
+
)
|
|
213
|
+
|
|
214
|
+
if _geoip_enabled(network.routing):
|
|
215
|
+
dns_rules.append({"rule_set": "geosite-geolocation-!cn", "server": CLIENT_DNS_REMOTE_TAG})
|
|
216
|
+
dns_rules.append({"rule_set": "geosite-cn", "server": CLIENT_DNS_DIRECT_TAG})
|
|
217
|
+
|
|
218
|
+
direct_dns_servers = [
|
|
219
|
+
{
|
|
220
|
+
"type": "udp",
|
|
221
|
+
"tag": CLIENT_DNS_DIRECT_TAG if index == 0 else f"{CLIENT_DNS_DIRECT_TAG}-{index}",
|
|
222
|
+
"server": server_ip,
|
|
223
|
+
}
|
|
224
|
+
for index, server_ip in enumerate(direct_servers)
|
|
225
|
+
]
|
|
226
|
+
|
|
227
|
+
return {
|
|
228
|
+
"servers": [
|
|
229
|
+
*direct_dns_servers,
|
|
230
|
+
{
|
|
231
|
+
"type": "https",
|
|
232
|
+
"tag": CLIENT_DNS_REMOTE_TAG,
|
|
233
|
+
"server": remote_server,
|
|
234
|
+
"path": DNS_REMOTE_PATH,
|
|
235
|
+
"tls": {
|
|
236
|
+
"enabled": True,
|
|
237
|
+
"server_name": DNS_REMOTE_TLS_SERVER_NAME,
|
|
238
|
+
"alpn": ["h2", "http/1.1"],
|
|
239
|
+
},
|
|
240
|
+
"detour": CLIENT_PROXY_TAG,
|
|
241
|
+
"domain_resolver": CLIENT_DNS_DIRECT_TAG,
|
|
242
|
+
},
|
|
243
|
+
],
|
|
244
|
+
"rules": dns_rules,
|
|
245
|
+
"final": CLIENT_DNS_REMOTE_TAG,
|
|
246
|
+
"strategy": "prefer_ipv4",
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
def build_client_route(network: Any, sniff_inbound: str = "tun-in") -> dict:
|
|
251
|
+
"""Client route rules, APNs exceptions, and the geo rule sets."""
|
|
252
|
+
|
|
253
|
+
rules_data = load_rules(network.routing)
|
|
254
|
+
direct_servers = _dns_direct_servers(network.dns)
|
|
255
|
+
|
|
256
|
+
rules: list = [
|
|
257
|
+
{"protocol": "dns", "action": "hijack-dns"},
|
|
258
|
+
{"ip_is_private": True, "action": "route", "outbound": "direct"},
|
|
259
|
+
{"ip_cidr": direct_servers, "action": "route", "outbound": "direct"},
|
|
260
|
+
]
|
|
261
|
+
|
|
262
|
+
if sniff_inbound:
|
|
263
|
+
rules.insert(0, {"inbound": sniff_inbound, "action": "sniff", "timeout": "1s"})
|
|
264
|
+
rules.insert(0, {"inbound": sniff_inbound, "action": "resolve", "strategy": "prefer_ipv4"})
|
|
265
|
+
|
|
266
|
+
direct_exact = _merge_unique(SKIP_PROXY_DOMAINS, rules_data["direct_exact"])
|
|
267
|
+
direct_suffix = _merge_unique(SKIP_PROXY_SUFFIXES, rules_data["direct_suffix"])
|
|
268
|
+
proxy_suffix = _merge_unique(APNS_PROXY_SUFFIXES, rules_data["proxy_suffix"])
|
|
269
|
+
|
|
270
|
+
rules.append({"domain_suffix": list(APNS_PROXY_SUFFIXES), "action": "route", "outbound": CLIENT_PROXY_TAG})
|
|
271
|
+
if direct_exact:
|
|
272
|
+
rules.append({"domain": direct_exact, "action": "route", "outbound": DIRECT_RULE_OUTBOUND})
|
|
273
|
+
if rules_data["proxy_exact"]:
|
|
274
|
+
rules.append(
|
|
275
|
+
{"domain": list(rules_data["proxy_exact"]), "action": "route", "outbound": CLIENT_PROXY_TAG}
|
|
276
|
+
)
|
|
277
|
+
if direct_suffix:
|
|
278
|
+
rules.append(
|
|
279
|
+
{"domain_suffix": direct_suffix, "action": "route", "outbound": DIRECT_RULE_OUTBOUND}
|
|
280
|
+
)
|
|
281
|
+
|
|
282
|
+
rules.append(
|
|
283
|
+
{
|
|
284
|
+
"ip_cidr": list(APNS_PROXY_CIDR),
|
|
285
|
+
"port": list(APNS_PROXY_PORTS),
|
|
286
|
+
"action": "route",
|
|
287
|
+
"outbound": CLIENT_PROXY_TAG,
|
|
288
|
+
}
|
|
289
|
+
)
|
|
290
|
+
|
|
291
|
+
if proxy_suffix:
|
|
292
|
+
rules.append(
|
|
293
|
+
{"domain_suffix": proxy_suffix, "action": "route", "outbound": CLIENT_PROXY_TAG}
|
|
294
|
+
)
|
|
295
|
+
if rules_data["direct_keyword"]:
|
|
296
|
+
rules.append(
|
|
297
|
+
{
|
|
298
|
+
"domain_keyword": list(rules_data["direct_keyword"]),
|
|
299
|
+
"action": "route",
|
|
300
|
+
"outbound": DIRECT_RULE_OUTBOUND,
|
|
301
|
+
}
|
|
302
|
+
)
|
|
303
|
+
if rules_data["proxy_keyword"]:
|
|
304
|
+
rules.append(
|
|
305
|
+
{
|
|
306
|
+
"domain_keyword": list(rules_data["proxy_keyword"]),
|
|
307
|
+
"action": "route",
|
|
308
|
+
"outbound": CLIENT_PROXY_TAG,
|
|
309
|
+
}
|
|
310
|
+
)
|
|
311
|
+
if rules_data["direct_cidr"]:
|
|
312
|
+
rules.append(
|
|
313
|
+
{
|
|
314
|
+
"ip_cidr": list(rules_data["direct_cidr"]),
|
|
315
|
+
"action": "route",
|
|
316
|
+
"outbound": DIRECT_RULE_OUTBOUND,
|
|
317
|
+
}
|
|
318
|
+
)
|
|
319
|
+
if rules_data["proxy_cidr"]:
|
|
320
|
+
rules.append(
|
|
321
|
+
{
|
|
322
|
+
"ip_cidr": list(rules_data["proxy_cidr"]),
|
|
323
|
+
"action": "route",
|
|
324
|
+
"outbound": CLIENT_PROXY_TAG,
|
|
325
|
+
}
|
|
326
|
+
)
|
|
327
|
+
|
|
328
|
+
route = {
|
|
329
|
+
"rules": rules,
|
|
330
|
+
"final": ROUTE_FINAL,
|
|
331
|
+
"auto_detect_interface": True,
|
|
332
|
+
"default_domain_resolver": CLIENT_DNS_DIRECT_TAG,
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
if _geoip_enabled(network.routing):
|
|
336
|
+
route["rule_set"] = [
|
|
337
|
+
{
|
|
338
|
+
"type": "remote",
|
|
339
|
+
"tag": "geosite-geolocation-!cn",
|
|
340
|
+
"format": "binary",
|
|
341
|
+
"url": GEOSITE_GEOLOCATION_NON_CN_RULESET_URL,
|
|
342
|
+
"http_client": "direct-client",
|
|
343
|
+
},
|
|
344
|
+
{
|
|
345
|
+
"type": "remote",
|
|
346
|
+
"tag": "geosite-cn",
|
|
347
|
+
"format": "binary",
|
|
348
|
+
"url": GEOSITE_CN_RULESET_URL,
|
|
349
|
+
"http_client": "direct-client",
|
|
350
|
+
},
|
|
351
|
+
{
|
|
352
|
+
"type": "remote",
|
|
353
|
+
"tag": "geoip-cn",
|
|
354
|
+
"format": "binary",
|
|
355
|
+
"url": GEOIP_CN_RULESET_URL,
|
|
356
|
+
"http_client": "direct-client",
|
|
357
|
+
},
|
|
358
|
+
]
|
|
359
|
+
route["rules"].append(
|
|
360
|
+
{"rule_set": "geosite-geolocation-!cn", "action": "route", "outbound": CLIENT_PROXY_TAG}
|
|
361
|
+
)
|
|
362
|
+
route["rules"].append(
|
|
363
|
+
{"rule_set": "geosite-cn", "action": "route", "outbound": DIRECT_RULE_OUTBOUND}
|
|
364
|
+
)
|
|
365
|
+
route["rules"].append(
|
|
366
|
+
{"rule_set": "geoip-cn", "action": "route", "outbound": DIRECT_RULE_OUTBOUND}
|
|
367
|
+
)
|
|
368
|
+
|
|
369
|
+
return route
|