devconfig-gen-singbox 2.1.7__tar.gz → 2.1.8__tar.gz

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.
Files changed (98) hide show
  1. {devconfig_gen_singbox-2.1.7/src/devconfig_gen_singbox.egg-info → devconfig_gen_singbox-2.1.8}/PKG-INFO +2 -2
  2. {devconfig_gen_singbox-2.1.7 → devconfig_gen_singbox-2.1.8}/README.md +1 -1
  3. {devconfig_gen_singbox-2.1.7 → devconfig_gen_singbox-2.1.8}/pyproject.toml +1 -1
  4. {devconfig_gen_singbox-2.1.7 → devconfig_gen_singbox-2.1.8}/src/devconfig_gen/__init__.py +1 -1
  5. {devconfig_gen_singbox-2.1.7 → devconfig_gen_singbox-2.1.8/src/devconfig_gen_singbox.egg-info}/PKG-INFO +2 -2
  6. {devconfig_gen_singbox-2.1.7 → devconfig_gen_singbox-2.1.8}/src/singbox_ops/cli.py +43 -6
  7. {devconfig_gen_singbox-2.1.7 → devconfig_gen_singbox-2.1.8}/src/singbox_ops/core/context_builder.py +28 -13
  8. {devconfig_gen_singbox-2.1.7 → devconfig_gen_singbox-2.1.8}/src/singbox_ops/core/plan.py +8 -0
  9. {devconfig_gen_singbox-2.1.7 → devconfig_gen_singbox-2.1.8}/src/singbox_ops/core/protocols.py +11 -0
  10. {devconfig_gen_singbox-2.1.7 → devconfig_gen_singbox-2.1.8}/tests/test_singbox_ops_context_builder.py +23 -0
  11. devconfig_gen_singbox-2.1.8/tests/test_singbox_ops_wizard.py +71 -0
  12. devconfig_gen_singbox-2.1.7/tests/test_singbox_ops_wizard.py +0 -67
  13. {devconfig_gen_singbox-2.1.7 → devconfig_gen_singbox-2.1.8}/LICENSE +0 -0
  14. {devconfig_gen_singbox-2.1.7 → devconfig_gen_singbox-2.1.8}/setup.cfg +0 -0
  15. {devconfig_gen_singbox-2.1.7 → devconfig_gen_singbox-2.1.8}/src/devconfig_gen/cli.py +0 -0
  16. {devconfig_gen_singbox-2.1.7 → devconfig_gen_singbox-2.1.8}/src/devconfig_gen/engine.py +0 -0
  17. {devconfig_gen_singbox-2.1.7 → devconfig_gen_singbox-2.1.8}/src/devconfig_gen/formats.py +0 -0
  18. {devconfig_gen_singbox-2.1.7 → devconfig_gen_singbox-2.1.8}/src/devconfig_gen/interactive.py +0 -0
  19. {devconfig_gen_singbox-2.1.7 → devconfig_gen_singbox-2.1.8}/src/devconfig_gen/models.py +0 -0
  20. {devconfig_gen_singbox-2.1.7 → devconfig_gen_singbox-2.1.8}/src/devconfig_gen/providers/__init__.py +0 -0
  21. {devconfig_gen_singbox-2.1.7 → devconfig_gen_singbox-2.1.8}/src/devconfig_gen/providers/custom.py +0 -0
  22. {devconfig_gen_singbox-2.1.7 → devconfig_gen_singbox-2.1.8}/src/devconfig_gen/providers/env_provider.py +0 -0
  23. {devconfig_gen_singbox-2.1.7 → devconfig_gen_singbox-2.1.8}/src/devconfig_gen/providers/json_provider.py +0 -0
  24. {devconfig_gen_singbox-2.1.7 → devconfig_gen_singbox-2.1.8}/src/devconfig_gen/providers/singbox/__init__.py +0 -0
  25. {devconfig_gen_singbox-2.1.7 → devconfig_gen_singbox-2.1.8}/src/devconfig_gen/providers/singbox/data/rules.json +0 -0
  26. {devconfig_gen_singbox-2.1.7 → devconfig_gen_singbox-2.1.8}/src/devconfig_gen/providers/singbox/links.py +0 -0
  27. {devconfig_gen_singbox-2.1.7 → devconfig_gen_singbox-2.1.8}/src/devconfig_gen/providers/singbox/models.py +0 -0
  28. {devconfig_gen_singbox-2.1.7 → devconfig_gen_singbox-2.1.8}/src/devconfig_gen/providers/singbox/plugins/__init__.py +0 -0
  29. {devconfig_gen_singbox-2.1.7 → devconfig_gen_singbox-2.1.8}/src/devconfig_gen/providers/singbox/plugins/anytls.py +0 -0
  30. {devconfig_gen_singbox-2.1.7 → devconfig_gen_singbox-2.1.8}/src/devconfig_gen/providers/singbox/plugins/base.py +0 -0
  31. {devconfig_gen_singbox-2.1.7 → devconfig_gen_singbox-2.1.8}/src/devconfig_gen/providers/singbox/plugins/hysteria2.py +0 -0
  32. {devconfig_gen_singbox-2.1.7 → devconfig_gen_singbox-2.1.8}/src/devconfig_gen/providers/singbox/plugins/tuic.py +0 -0
  33. {devconfig_gen_singbox-2.1.7 → devconfig_gen_singbox-2.1.8}/src/devconfig_gen/providers/singbox/provider.py +0 -0
  34. {devconfig_gen_singbox-2.1.7 → devconfig_gen_singbox-2.1.8}/src/devconfig_gen/providers/singbox/route.py +0 -0
  35. {devconfig_gen_singbox-2.1.7 → devconfig_gen_singbox-2.1.8}/src/devconfig_gen/providers/singbox/schema.py +0 -0
  36. {devconfig_gen_singbox-2.1.7 → devconfig_gen_singbox-2.1.8}/src/devconfig_gen/py.typed +0 -0
  37. {devconfig_gen_singbox-2.1.7 → devconfig_gen_singbox-2.1.8}/src/devconfig_gen/registry.py +0 -0
  38. {devconfig_gen_singbox-2.1.7 → devconfig_gen_singbox-2.1.8}/src/devconfig_gen/validation.py +0 -0
  39. {devconfig_gen_singbox-2.1.7 → devconfig_gen_singbox-2.1.8}/src/devconfig_gen/web_ui.py +0 -0
  40. {devconfig_gen_singbox-2.1.7 → devconfig_gen_singbox-2.1.8}/src/devconfig_gen_singbox.egg-info/SOURCES.txt +0 -0
  41. {devconfig_gen_singbox-2.1.7 → devconfig_gen_singbox-2.1.8}/src/devconfig_gen_singbox.egg-info/dependency_links.txt +0 -0
  42. {devconfig_gen_singbox-2.1.7 → devconfig_gen_singbox-2.1.8}/src/devconfig_gen_singbox.egg-info/entry_points.txt +0 -0
  43. {devconfig_gen_singbox-2.1.7 → devconfig_gen_singbox-2.1.8}/src/devconfig_gen_singbox.egg-info/requires.txt +0 -0
  44. {devconfig_gen_singbox-2.1.7 → devconfig_gen_singbox-2.1.8}/src/devconfig_gen_singbox.egg-info/top_level.txt +0 -0
  45. {devconfig_gen_singbox-2.1.7 → devconfig_gen_singbox-2.1.8}/src/singbox_ops/__init__.py +0 -0
  46. {devconfig_gen_singbox-2.1.7 → devconfig_gen_singbox-2.1.8}/src/singbox_ops/adapters/__init__.py +0 -0
  47. {devconfig_gen_singbox-2.1.7 → devconfig_gen_singbox-2.1.8}/src/singbox_ops/adapters/acme/__init__.py +0 -0
  48. {devconfig_gen_singbox-2.1.7 → devconfig_gen_singbox-2.1.8}/src/singbox_ops/adapters/acme/base.py +0 -0
  49. {devconfig_gen_singbox-2.1.7 → devconfig_gen_singbox-2.1.8}/src/singbox_ops/adapters/acme/cloudflare_dns01.py +0 -0
  50. {devconfig_gen_singbox-2.1.7 → devconfig_gen_singbox-2.1.8}/src/singbox_ops/adapters/acme/prune.py +0 -0
  51. {devconfig_gen_singbox-2.1.7 → devconfig_gen_singbox-2.1.8}/src/singbox_ops/adapters/base.py +0 -0
  52. {devconfig_gen_singbox-2.1.7 → devconfig_gen_singbox-2.1.8}/src/singbox_ops/adapters/dns/__init__.py +0 -0
  53. {devconfig_gen_singbox-2.1.7 → devconfig_gen_singbox-2.1.8}/src/singbox_ops/adapters/dns/base.py +0 -0
  54. {devconfig_gen_singbox-2.1.7 → devconfig_gen_singbox-2.1.8}/src/singbox_ops/adapters/dns/cloudflare.py +0 -0
  55. {devconfig_gen_singbox-2.1.7 → devconfig_gen_singbox-2.1.8}/src/singbox_ops/adapters/export/__init__.py +0 -0
  56. {devconfig_gen_singbox-2.1.7 → devconfig_gen_singbox-2.1.8}/src/singbox_ops/adapters/export/local_files.py +0 -0
  57. {devconfig_gen_singbox-2.1.7 → devconfig_gen_singbox-2.1.8}/src/singbox_ops/adapters/runtime/__init__.py +0 -0
  58. {devconfig_gen_singbox-2.1.7 → devconfig_gen_singbox-2.1.8}/src/singbox_ops/adapters/runtime/auto_update.py +0 -0
  59. {devconfig_gen_singbox-2.1.7 → devconfig_gen_singbox-2.1.8}/src/singbox_ops/adapters/runtime/nftables.py +0 -0
  60. {devconfig_gen_singbox-2.1.7 → devconfig_gen_singbox-2.1.8}/src/singbox_ops/adapters/runtime/packages.py +0 -0
  61. {devconfig_gen_singbox-2.1.7 → devconfig_gen_singbox-2.1.8}/src/singbox_ops/adapters/runtime/systemd.py +0 -0
  62. {devconfig_gen_singbox-2.1.7 → devconfig_gen_singbox-2.1.8}/src/singbox_ops/adapters/runtime/warp_watchdog.py +0 -0
  63. {devconfig_gen_singbox-2.1.7 → devconfig_gen_singbox-2.1.8}/src/singbox_ops/adapters/secrets/__init__.py +0 -0
  64. {devconfig_gen_singbox-2.1.7 → devconfig_gen_singbox-2.1.8}/src/singbox_ops/adapters/secrets/base.py +0 -0
  65. {devconfig_gen_singbox-2.1.7 → devconfig_gen_singbox-2.1.8}/src/singbox_ops/adapters/secrets/python_secrets.py +0 -0
  66. {devconfig_gen_singbox-2.1.7 → devconfig_gen_singbox-2.1.8}/src/singbox_ops/adapters/secrets/singbox_subprocess.py +0 -0
  67. {devconfig_gen_singbox-2.1.7 → devconfig_gen_singbox-2.1.8}/src/singbox_ops/adapters/secrets/static.py +0 -0
  68. {devconfig_gen_singbox-2.1.7 → devconfig_gen_singbox-2.1.8}/src/singbox_ops/adapters/state/__init__.py +0 -0
  69. {devconfig_gen_singbox-2.1.7 → devconfig_gen_singbox-2.1.8}/src/singbox_ops/adapters/state/local_json.py +0 -0
  70. {devconfig_gen_singbox-2.1.7 → devconfig_gen_singbox-2.1.8}/src/singbox_ops/core/__init__.py +0 -0
  71. {devconfig_gen_singbox-2.1.7 → devconfig_gen_singbox-2.1.8}/src/singbox_ops/core/command.py +0 -0
  72. {devconfig_gen_singbox-2.1.7 → devconfig_gen_singbox-2.1.8}/src/singbox_ops/core/exceptions.py +0 -0
  73. {devconfig_gen_singbox-2.1.7 → devconfig_gen_singbox-2.1.8}/src/singbox_ops/core/network.py +0 -0
  74. {devconfig_gen_singbox-2.1.7 → devconfig_gen_singbox-2.1.8}/src/singbox_ops/core/orchestrator.py +0 -0
  75. {devconfig_gen_singbox-2.1.7 → devconfig_gen_singbox-2.1.8}/src/singbox_ops/core/ui.py +0 -0
  76. {devconfig_gen_singbox-2.1.7 → devconfig_gen_singbox-2.1.8}/src/singbox_ops/templates/singbox_auto_update.py.template +0 -0
  77. {devconfig_gen_singbox-2.1.7 → devconfig_gen_singbox-2.1.8}/src/singbox_ops/templates/warp_watchdog.sh.template +0 -0
  78. {devconfig_gen_singbox-2.1.7 → devconfig_gen_singbox-2.1.8}/tests/test_api_parity.py +0 -0
  79. {devconfig_gen_singbox-2.1.7 → devconfig_gen_singbox-2.1.8}/tests/test_cli_e2e.py +0 -0
  80. {devconfig_gen_singbox-2.1.7 → devconfig_gen_singbox-2.1.8}/tests/test_custom_provider.py +0 -0
  81. {devconfig_gen_singbox-2.1.7 → devconfig_gen_singbox-2.1.8}/tests/test_devconfig_core.py +0 -0
  82. {devconfig_gen_singbox-2.1.7 → devconfig_gen_singbox-2.1.8}/tests/test_env_provider.py +0 -0
  83. {devconfig_gen_singbox-2.1.7 → devconfig_gen_singbox-2.1.8}/tests/test_formats.py +0 -0
  84. {devconfig_gen_singbox-2.1.7 → devconfig_gen_singbox-2.1.8}/tests/test_interactive.py +0 -0
  85. {devconfig_gen_singbox-2.1.7 → devconfig_gen_singbox-2.1.8}/tests/test_merge.py +0 -0
  86. {devconfig_gen_singbox-2.1.7 → devconfig_gen_singbox-2.1.8}/tests/test_provider_metadata.py +0 -0
  87. {devconfig_gen_singbox-2.1.7 → devconfig_gen_singbox-2.1.8}/tests/test_singbox_ops_acme.py +0 -0
  88. {devconfig_gen_singbox-2.1.7 → devconfig_gen_singbox-2.1.8}/tests/test_singbox_ops_cli.py +0 -0
  89. {devconfig_gen_singbox-2.1.7 → devconfig_gen_singbox-2.1.8}/tests/test_singbox_ops_dns.py +0 -0
  90. {devconfig_gen_singbox-2.1.7 → devconfig_gen_singbox-2.1.8}/tests/test_singbox_ops_orchestrator.py +0 -0
  91. {devconfig_gen_singbox-2.1.7 → devconfig_gen_singbox-2.1.8}/tests/test_singbox_ops_plan.py +0 -0
  92. {devconfig_gen_singbox-2.1.7 → devconfig_gen_singbox-2.1.8}/tests/test_singbox_ops_runtime.py +0 -0
  93. {devconfig_gen_singbox-2.1.7 → devconfig_gen_singbox-2.1.8}/tests/test_singbox_ops_secrets.py +0 -0
  94. {devconfig_gen_singbox-2.1.7 → devconfig_gen_singbox-2.1.8}/tests/test_singbox_ops_state_export.py +0 -0
  95. {devconfig_gen_singbox-2.1.7 → devconfig_gen_singbox-2.1.8}/tests/test_singbox_provider.py +0 -0
  96. {devconfig_gen_singbox-2.1.7 → devconfig_gen_singbox-2.1.8}/tests/test_validation.py +0 -0
  97. {devconfig_gen_singbox-2.1.7 → devconfig_gen_singbox-2.1.8}/tests/test_web_ui.py +0 -0
  98. {devconfig_gen_singbox-2.1.7 → devconfig_gen_singbox-2.1.8}/tests/test_web_ui_widgets.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: devconfig_gen_singbox
3
- Version: 2.1.7
3
+ Version: 2.1.8
4
4
  Summary: Provider-based developer configuration generation engine and CLI
5
5
  Author-email: henryliu443 <henryliu443@gmail.com>
6
6
  License-Expression: Apache-2.0
@@ -28,7 +28,7 @@ Dynamic: license-file
28
28
  > **A sing-box domain implementation built on top of DevConfig-Gen.**
29
29
  > **构建在 `DevConfig-Gen` 之上的 sing-box 领域实现(child / fork)。**
30
30
 
31
- [![Version](https://img.shields.io/badge/version-2.1.7-blue.svg)](CHANGELOG.md)
31
+ [![Version](https://img.shields.io/badge/version-2.1.8-blue.svg)](CHANGELOG.md)
32
32
  [![License: Apache-2.0](https://img.shields.io/badge/license-Apache--2.0-blue.svg)](LICENSE)
33
33
  ![Python 3.8+](https://img.shields.io/badge/python-3.8%2B-blue.svg)
34
34
  ![Tests](https://img.shields.io/badge/tests-136%20passing-brightgreen.svg)
@@ -3,7 +3,7 @@
3
3
  > **A sing-box domain implementation built on top of DevConfig-Gen.**
4
4
  > **构建在 `DevConfig-Gen` 之上的 sing-box 领域实现(child / fork)。**
5
5
 
6
- [![Version](https://img.shields.io/badge/version-2.1.7-blue.svg)](CHANGELOG.md)
6
+ [![Version](https://img.shields.io/badge/version-2.1.8-blue.svg)](CHANGELOG.md)
7
7
  [![License: Apache-2.0](https://img.shields.io/badge/license-Apache--2.0-blue.svg)](LICENSE)
8
8
  ![Python 3.8+](https://img.shields.io/badge/python-3.8%2B-blue.svg)
9
9
  ![Tests](https://img.shields.io/badge/tests-136%20passing-brightgreen.svg)
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "devconfig_gen_singbox"
7
- version = "2.1.7"
7
+ version = "2.1.8"
8
8
  authors = [
9
9
  { name = "henryliu443", email = "henryliu443@gmail.com" },
10
10
  ]
@@ -66,4 +66,4 @@ __all__ = [
66
66
  "loads",
67
67
  "validate_request",
68
68
  ]
69
- __version__ = "2.1.7"
69
+ __version__ = "2.1.8"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: devconfig_gen_singbox
3
- Version: 2.1.7
3
+ Version: 2.1.8
4
4
  Summary: Provider-based developer configuration generation engine and CLI
5
5
  Author-email: henryliu443 <henryliu443@gmail.com>
6
6
  License-Expression: Apache-2.0
@@ -28,7 +28,7 @@ Dynamic: license-file
28
28
  > **A sing-box domain implementation built on top of DevConfig-Gen.**
29
29
  > **构建在 `DevConfig-Gen` 之上的 sing-box 领域实现(child / fork)。**
30
30
 
31
- [![Version](https://img.shields.io/badge/version-2.1.7-blue.svg)](CHANGELOG.md)
31
+ [![Version](https://img.shields.io/badge/version-2.1.8-blue.svg)](CHANGELOG.md)
32
32
  [![License: Apache-2.0](https://img.shields.io/badge/license-Apache--2.0-blue.svg)](LICENSE)
33
33
  ![Python 3.8+](https://img.shields.io/badge/python-3.8%2B-blue.svg)
34
34
  ![Tests](https://img.shields.io/badge/tests-136%20passing-brightgreen.svg)
@@ -25,7 +25,12 @@ from .core.context_builder import build_context
25
25
  from .core.exceptions import OpsError
26
26
  from .core.plan import DeployPlan, load_plan
27
27
  from .core.orchestrator import DRY_RUN_IP, build_suite, deploy, destroy, make_runner
28
- from .core.protocols import PROTOCOLS
28
+ from .core.protocols import (
29
+ DEFAULT_ANYTLS_DECOY_PORT,
30
+ DEFAULT_ANYTLS_DECOY_SERVER,
31
+ DEFAULT_HY2_MASQUERADE,
32
+ PROTOCOLS,
33
+ )
29
34
  from .core.ui import UI
30
35
 
31
36
  PROG = "devconfig_gen_singbox"
@@ -151,7 +156,7 @@ def interactive_plan(domain=None, protocols=None, reader=None, writer=None, ui=N
151
156
  ui = ui or UI(writer)
152
157
  ui.banner(f"{PROG} · 交互式部署向导", "逐项回答;回车使用默认值。Ctrl-C 可随时退出。")
153
158
 
154
- ui.section("1/4 基础")
159
+ ui.section("1/5 基础")
155
160
  while not domain:
156
161
  domain = _ask(ui, "主域名", "例如 example.com;协议主机名 = <前缀>.<主域名>",
157
162
  required=True, reader=reader, writer=writer)
@@ -167,7 +172,32 @@ def interactive_plan(domain=None, protocols=None, reader=None, writer=None, ui=N
167
172
  )
168
173
  server_ip = _ask(ui, "服务器公网 IP", "留空=自动探测", reader=reader, writer=writer)
169
174
 
170
- ui.section("2/4 Cloudflare(DNS 必填)")
175
+ protocol_list = [item.strip() for item in str(protocols).split(",") if item.strip()]
176
+ protocol_params: dict = {}
177
+ if "anytls" in protocol_list or "hysteria2" in protocol_list:
178
+ ui.section("2/5 协议参数")
179
+ if "anytls" in protocol_list:
180
+ protocol_params["anytls"] = {
181
+ "decoy_server": _ask(
182
+ ui, "REALITY 伪装站 (decoy server)",
183
+ f"REALITY 握手伪装的目标站点;证书 SNI 也用这个(默认 {DEFAULT_ANYTLS_DECOY_SERVER})",
184
+ default=DEFAULT_ANYTLS_DECOY_SERVER, reader=reader, writer=writer,
185
+ ),
186
+ "decoy_port": _ask(
187
+ ui, "REALITY 伪装端口", f"通常 {DEFAULT_ANYTLS_DECOY_PORT}",
188
+ default=str(DEFAULT_ANYTLS_DECOY_PORT), reader=reader, writer=writer,
189
+ ),
190
+ }
191
+ if "hysteria2" in protocol_list:
192
+ protocol_params["hysteria2"] = {
193
+ "masquerade": _ask(
194
+ ui, "Hysteria2 伪装网址 (masquerade)",
195
+ f"HTTP 伪装目标 URL(默认 {DEFAULT_HY2_MASQUERADE})",
196
+ default=DEFAULT_HY2_MASQUERADE, reader=reader, writer=writer,
197
+ ),
198
+ }
199
+
200
+ ui.section("3/5 Cloudflare(DNS 必填)")
171
201
  ui.warn("DNS A 记录由 Cloudflare 管理,因此 CF 凭据必填(仅本次内存使用,不写盘)")
172
202
  env = {
173
203
  "CF_Token": _ask_secret(
@@ -185,23 +215,24 @@ def interactive_plan(domain=None, protocols=None, reader=None, writer=None, ui=N
185
215
  default=True, reader=reader, writer=writer,
186
216
  )
187
217
 
188
- ui.section("3/4 运行时")
218
+ ui.section("4/5 运行时")
189
219
  use_runtime = _ask_yes_no(
190
220
  ui, "启用运行时适配器?",
191
221
  "systemd / nftables / watchdog / 每日自动升级(生产建议 Y)",
192
222
  default=True, reader=reader, writer=writer,
193
223
  )
194
224
 
195
- ui.section("4/4 产物路径")
225
+ ui.section("5/5 产物路径")
196
226
  server_out = _ask(ui, "服务端配置", "sing-box 主配置", default=DEFAULT_SERVER_OUT, reader=reader, writer=writer)
197
227
  client_out = _ask(ui, "客户端配置", "可导入 GUI 的完整客户端配置", default=DEFAULT_CLIENT_OUT, reader=reader, writer=writer)
198
228
  links_out = _ask(ui, "分享链接", "anytls:// tuic:// hy2:// 文本", default=DEFAULT_LINKS_OUT, reader=reader, writer=writer)
199
229
 
200
230
  plan = {
201
231
  "domain_root": domain,
202
- "protocols": [item.strip() for item in str(protocols).split(",") if item.strip()],
232
+ "protocols": protocol_list,
203
233
  "tunnel_mode": tunnel_mode,
204
234
  "server_ip": server_ip or "auto",
235
+ "protocol_params": protocol_params,
205
236
  "adapters": {
206
237
  "secrets": "singbox-subprocess",
207
238
  "dns": "cloudflare",
@@ -235,6 +266,12 @@ def _confirm(ui: UI, plan, dry_run: bool, reader=None, writer=None) -> bool:
235
266
  ui.kv("DNS", adapters.get("dns") or "跳过(自己管理)")
236
267
  ui.kv("证书", adapters.get("acme") or "跳过(自己管理)")
237
268
  ui.kv("运行时", ", ".join(f"{k}={v}" for k, v in runtime.items() if v) or "关闭")
269
+ params = plan.protocol_params or {}
270
+ if params.get("anytls"):
271
+ anytls = params["anytls"]
272
+ ui.kv("REALITY decoy", f"{anytls.get('decoy_server')}:{anytls.get('decoy_port', 443)}")
273
+ if params.get("hysteria2"):
274
+ ui.kv("HY2 masquerade", params["hysteria2"].get("masquerade"))
238
275
  ui.kv("服务端配置", plan.outputs.get("server_config", "?"))
239
276
  if dry_run:
240
277
  ui.warn("DRY-RUN:只打印将执行的步骤,不改动系统")
@@ -12,19 +12,24 @@ from .exceptions import AdapterError
12
12
  from .plan import DeployPlan
13
13
  from .protocols import (
14
14
  CERT_PATHS,
15
+ DEFAULT_ANYTLS_DECOY_PORT,
16
+ DEFAULT_ANYTLS_DECOY_SERVER,
17
+ DEFAULT_HY2_CLIENT_DOWN_MBPS,
18
+ DEFAULT_HY2_CLIENT_UP_MBPS,
19
+ DEFAULT_HY2_MASQUERADE,
20
+ DEFAULT_HY2_SERVER_DOWN_MBPS,
21
+ DEFAULT_HY2_SERVER_UP_MBPS,
15
22
  HOST_KEYS,
16
23
  PROTOCOL_PORTS,
17
24
  TLS_PROTOCOLS,
18
25
  requires_tls,
19
26
  )
20
27
 
21
- DEFAULT_ANYTLS_DECOY_SERVER = "www.cloudflare.com"
22
- DEFAULT_ANYTLS_DECOY_PORT = 443
23
- DEFAULT_MASQUERADE = "https://www.cloudflare.com"
24
- DEFAULT_SERVER_UP_MBPS = 500
25
- DEFAULT_SERVER_DOWN_MBPS = 500
26
- DEFAULT_CLIENT_UP_MBPS = 50
27
- DEFAULT_CLIENT_DOWN_MBPS = 200
28
+ DEFAULT_MASQUERADE = DEFAULT_HY2_MASQUERADE
29
+ DEFAULT_SERVER_UP_MBPS = DEFAULT_HY2_SERVER_UP_MBPS
30
+ DEFAULT_SERVER_DOWN_MBPS = DEFAULT_HY2_SERVER_DOWN_MBPS
31
+ DEFAULT_CLIENT_UP_MBPS = DEFAULT_HY2_CLIENT_UP_MBPS
32
+ DEFAULT_CLIENT_DOWN_MBPS = DEFAULT_HY2_CLIENT_DOWN_MBPS
28
33
 
29
34
 
30
35
  def _require(secrets: Mapping[str, Any], path: str, context_name: str) -> Any:
@@ -59,7 +64,8 @@ def build_context(
59
64
  protocols = []
60
65
  for protocol in plan.protocols:
61
66
  credentials = secrets.get(protocol) or {}
62
- entry = _build_protocol(protocol, credentials, prefixes, cert_paths)
67
+ params = dict(plan.protocol_params.get(protocol) or {})
68
+ entry = _build_protocol(protocol, credentials, prefixes, cert_paths, params)
63
69
  protocols.append(entry)
64
70
 
65
71
  client = {"fingerprint": plan.fingerprint}
@@ -97,7 +103,9 @@ def _build_protocol(
97
103
  credentials: Mapping[str, Any],
98
104
  prefixes: Mapping[str, str],
99
105
  cert_paths: Optional[Mapping[str, str]],
106
+ params: Optional[Mapping[str, Any]] = None,
100
107
  ) -> dict:
108
+ params = params or {}
101
109
  host_key = HOST_KEYS[protocol]
102
110
  if host_key not in prefixes:
103
111
  raise AdapterError(f"subdomain prefix for {host_key!r} is required ({protocol})")
@@ -111,8 +119,11 @@ def _build_protocol(
111
119
  if protocol == "anytls":
112
120
  entry["auth"] = {"password": _require(credentials, "password", protocol)}
113
121
  entry["reality"] = {
114
- "decoy_server": credentials.get("decoy_server", DEFAULT_ANYTLS_DECOY_SERVER),
115
- "decoy_port": int(credentials.get("decoy_port", DEFAULT_ANYTLS_DECOY_PORT)),
122
+ "decoy_server": params.get("decoy_server")
123
+ or credentials.get("decoy_server", DEFAULT_ANYTLS_DECOY_SERVER),
124
+ "decoy_port": int(
125
+ params.get("decoy_port") or credentials.get("decoy_port", DEFAULT_ANYTLS_DECOY_PORT)
126
+ ),
116
127
  "private_key": _require(credentials, "private_key", protocol),
117
128
  "public_key": _require(credentials, "public_key", protocol),
118
129
  "short_id": _require(credentials, "short_id", protocol),
@@ -130,10 +141,14 @@ def _build_protocol(
130
141
  "obfs_password": _require(credentials, "obfs_password", protocol),
131
142
  }
132
143
  entry["bandwidth"] = {
133
- "up_mbps": int(credentials.get("up_mbps", DEFAULT_SERVER_UP_MBPS)),
134
- "down_mbps": int(credentials.get("down_mbps", DEFAULT_SERVER_DOWN_MBPS)),
144
+ "up_mbps": int(params.get("up_mbps") or credentials.get("up_mbps", DEFAULT_SERVER_UP_MBPS)),
145
+ "down_mbps": int(
146
+ params.get("down_mbps") or credentials.get("down_mbps", DEFAULT_SERVER_DOWN_MBPS)
147
+ ),
135
148
  }
136
- entry["masquerade"] = credentials.get("masquerade", DEFAULT_MASQUERADE)
149
+ entry["masquerade"] = (
150
+ params.get("masquerade") or credentials.get("masquerade", DEFAULT_MASQUERADE)
151
+ )
137
152
  cert, key = _resolve_cert(protocol, cert_paths)
138
153
  entry["tls"] = {"cert_path": cert, "key_path": key}
139
154
  else: # pragma: no cover - guarded by DeployPlan validation
@@ -106,6 +106,7 @@ class DeployPlan:
106
106
  subdomain_prefixes: Mapping[str, str] = field(default_factory=dict)
107
107
  routing: Mapping[str, Any] = field(default_factory=dict)
108
108
  dns: Mapping[str, Any] = field(default_factory=dict)
109
+ protocol_params: Mapping[str, Mapping[str, Any]] = field(default_factory=dict)
109
110
  fingerprint: str = DEFAULT_FINGERPRINT
110
111
  target: str = "both"
111
112
  output_format: str = DEFAULT_FORMAT
@@ -150,6 +151,8 @@ class DeployPlan:
150
151
  data["subdomain_prefixes"] = dict(self.subdomain_prefixes)
151
152
  data.setdefault("routing", dict(self.routing))
152
153
  data.setdefault("dns", dict(self.dns))
154
+ if self.protocol_params:
155
+ data["protocol_params"] = {key: dict(value) for key, value in self.protocol_params.items()}
153
156
  client = dict(data.get("client") or {})
154
157
  client.setdefault("fingerprint", self.fingerprint)
155
158
  if self.server_ip:
@@ -231,6 +234,11 @@ class DeployPlan:
231
234
  subdomain_prefixes=prefixes,
232
235
  routing=dict(_as_mapping(raw.get("routing"), "routing")),
233
236
  dns=dict(_as_mapping(raw.get("dns"), "dns")),
237
+ protocol_params={
238
+ str(proto): dict(value)
239
+ for proto, value in _as_mapping(raw.get("protocol_params"), "protocol_params").items()
240
+ if isinstance(value, Mapping)
241
+ },
234
242
  fingerprint=_as_str(client.get("fingerprint"), "client.fingerprint")
235
243
  or DEFAULT_FINGERPRINT,
236
244
  target=target,
@@ -36,6 +36,17 @@ DEFAULT_FINGERPRINT = "chrome"
36
36
  DEFAULT_TUNNEL_MODE = "proxy"
37
37
  DEFAULT_FORMAT = "json"
38
38
 
39
+ # Single source of truth for protocol-level defaults. Anything that wants a
40
+ # fallback (context builder, wizard prompts, docs) reads these, so nothing is
41
+ # hard-coded in more than one place and operators can reason about one value.
42
+ DEFAULT_ANYTLS_DECOY_SERVER = "www.cloudflare.com"
43
+ DEFAULT_ANYTLS_DECOY_PORT = 443
44
+ DEFAULT_HY2_MASQUERADE = "https://www.cloudflare.com"
45
+ DEFAULT_HY2_SERVER_UP_MBPS = 500
46
+ DEFAULT_HY2_SERVER_DOWN_MBPS = 500
47
+ DEFAULT_HY2_CLIENT_UP_MBPS = 50
48
+ DEFAULT_HY2_CLIENT_DOWN_MBPS = 200
49
+
39
50
  #: TCP / UDP classification, used for firewall rules.
40
51
  UDP_PROTOCOLS: Tuple[str, ...] = ("tuic", "hysteria2")
41
52
 
@@ -49,6 +49,29 @@ class ContextBuilderTests(unittest.TestCase):
49
49
  needed = needed_certificates(self.plan)
50
50
  self.assertEqual(needed, {"tuic": "tuic", "hysteria2": "hy2"})
51
51
 
52
+ def test_protocol_params_override_defaults(self):
53
+ plan = DeployPlan.from_mapping(
54
+ {
55
+ "domain_root": "example.com",
56
+ "protocols": ["anytls", "hysteria2"],
57
+ "protocol_params": {
58
+ "anytls": {"decoy_server": "react.dev", "decoy_port": 8443},
59
+ "hysteria2": {"masquerade": "https://react.dev"},
60
+ },
61
+ }
62
+ )
63
+ secrets = static_secrets(("anytls", "hysteria2"))
64
+ context = build_context(
65
+ plan,
66
+ secrets["credentials"],
67
+ "203.0.113.10",
68
+ subdomain_prefixes=secrets["subdomain_prefixes"],
69
+ )
70
+ protos = {item["type"]: item for item in context["network"]["protocols"]}
71
+ self.assertEqual(protos["anytls"]["reality"]["decoy_server"], "react.dev")
72
+ self.assertEqual(protos["anytls"]["reality"]["decoy_port"], 8443)
73
+ self.assertEqual(protos["hysteria2"]["masquerade"], "https://react.dev")
74
+
52
75
  def test_missing_prefix_raises(self):
53
76
  from singbox_ops.core.exceptions import AdapterError
54
77
 
@@ -0,0 +1,71 @@
1
+ import io
2
+ import unittest
3
+
4
+ from singbox_ops.cli import interactive_plan
5
+ from singbox_ops.core.ui import UI
6
+
7
+
8
+ def run_wizard(inputs):
9
+ reader = io.StringIO(inputs)
10
+ writer = io.StringIO()
11
+ return interactive_plan(reader=reader, writer=writer, ui=UI(writer))
12
+
13
+
14
+ class WizardTests(unittest.TestCase):
15
+ def test_collects_dns_creds_and_protocol_params(self):
16
+ inputs = (
17
+ "example.com\n" # domain
18
+ "\n" # protocols (default: all)
19
+ "none\n" # tunnel mode
20
+ "\n" # server ip (auto)
21
+ "react.dev\n" # anytls decoy server
22
+ "443\n" # anytls decoy port
23
+ "https://react.dev\n" # hy2 masquerade
24
+ "tok123\n" # CF token
25
+ "zone456\n" # CF zone
26
+ "y\n" # acme
27
+ "y\n" # runtime
28
+ "\n\n\n" # outputs
29
+ )
30
+ plan, env = run_wizard(inputs)
31
+ self.assertEqual(plan["domain_root"], "example.com")
32
+ self.assertEqual(plan["adapters"]["dns"], "cloudflare")
33
+ self.assertEqual(env["CF_Token"], "tok123")
34
+ self.assertEqual(plan["protocol_params"]["anytls"]["decoy_server"], "react.dev")
35
+ self.assertEqual(plan["protocol_params"]["anytls"]["decoy_port"], "443")
36
+ self.assertEqual(plan["protocol_params"]["hysteria2"]["masquerade"], "https://react.dev")
37
+
38
+ def test_acme_can_be_skipped_but_dns_cannot(self):
39
+ inputs = (
40
+ "example.com\n\nnone\n\n"
41
+ "\n443\n\n" # decoy defaults, masquerade default
42
+ "tok\nzone\n"
43
+ "n\n" # no acme
44
+ "y\n" # runtime
45
+ "\n\n\n"
46
+ )
47
+ plan, env = run_wizard(inputs)
48
+ self.assertEqual(plan["adapters"]["dns"], "cloudflare")
49
+ self.assertIsNone(plan["adapters"]["acme"])
50
+ self.assertEqual(plan["protocol_params"]["anytls"]["decoy_server"], "www.cloudflare.com")
51
+
52
+ def test_domain_is_required(self):
53
+ inputs = (
54
+ "\n" # empty domain -> rejected
55
+ "example.com\n"
56
+ "\n" # protocols
57
+ "\n" # tunnel (default proxy)
58
+ "\n" # ip
59
+ "\n443\n\n" # protocol params defaults
60
+ "tok\nzone\n"
61
+ "n\nn\n" # no acme, no runtime
62
+ "\n\n\n"
63
+ )
64
+ plan, env = run_wizard(inputs)
65
+ self.assertEqual(plan["domain_root"], "example.com")
66
+ self.assertEqual(plan["tunnel_mode"], "proxy")
67
+ self.assertIsNone(plan["adapters"]["runtime"]["systemd"])
68
+
69
+
70
+ if __name__ == "__main__":
71
+ unittest.main()
@@ -1,67 +0,0 @@
1
- import io
2
- import unittest
3
-
4
- from singbox_ops.cli import interactive_plan
5
- from singbox_ops.core.ui import UI
6
-
7
-
8
- def run_wizard(inputs):
9
- reader = io.StringIO(inputs)
10
- writer = io.StringIO()
11
- return interactive_plan(reader=reader, writer=writer, ui=UI(writer))
12
-
13
-
14
- class WizardTests(unittest.TestCase):
15
- def test_dns_is_always_cloudflare_and_creds_are_collected(self):
16
- inputs = (
17
- "example.com\n" # domain
18
- "\n" # protocols (default)
19
- "none\n" # tunnel mode
20
- "\n" # server ip (auto)
21
- "tok123\n" # CF token
22
- "zone456\n" # CF zone
23
- "y\n" # acme
24
- "y\n" # runtime adapters
25
- "\n\n\n" # output paths
26
- )
27
- plan, env = run_wizard(inputs)
28
- self.assertEqual(plan["domain_root"], "example.com")
29
- self.assertEqual(plan["tunnel_mode"], "none")
30
- # DNS is mandatory, never optional
31
- self.assertEqual(plan["adapters"]["dns"], "cloudflare")
32
- self.assertEqual(env["CF_Token"], "tok123")
33
- self.assertEqual(env["CF_Zone_ID"], "zone456")
34
- self.assertEqual(plan["adapters"]["acme"], "cloudflare-dns01")
35
-
36
- def test_acme_can_be_skipped_but_dns_cannot(self):
37
- inputs = (
38
- "example.com\n\nnone\n\n"
39
- "tok\nzone\n"
40
- "n\n" # no acme
41
- "y\n" # runtime
42
- "\n\n\n"
43
- )
44
- plan, env = run_wizard(inputs)
45
- self.assertEqual(plan["adapters"]["dns"], "cloudflare")
46
- self.assertIsNone(plan["adapters"]["acme"])
47
- self.assertEqual(plan["adapters"]["runtime"]["auto_update"], "auto-update")
48
-
49
- def test_domain_is_required(self):
50
- inputs = (
51
- "\n" # empty domain -> rejected
52
- "example.com\n"
53
- "\n" # protocols
54
- "\n" # tunnel (default proxy)
55
- "\n" # ip
56
- "tok\nzone\n"
57
- "n\nn\n" # no acme, no runtime
58
- "\n\n\n"
59
- )
60
- plan, env = run_wizard(inputs)
61
- self.assertEqual(plan["domain_root"], "example.com")
62
- self.assertEqual(plan["tunnel_mode"], "proxy")
63
- self.assertIsNone(plan["adapters"]["runtime"]["systemd"])
64
-
65
-
66
- if __name__ == "__main__":
67
- unittest.main()