proxyctl 0.5.0__tar.gz → 0.5.2__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.
- {proxyctl-0.5.0 → proxyctl-0.5.2}/PKG-INFO +1 -1
- {proxyctl-0.5.0 → proxyctl-0.5.2}/pyproject.toml +1 -1
- {proxyctl-0.5.0 → proxyctl-0.5.2}/src/proxyctl/cli.py +19 -9
- {proxyctl-0.5.0 → proxyctl-0.5.2}/src/proxyctl/explain.py +20 -11
- {proxyctl-0.5.0 → proxyctl-0.5.2}/src/proxyctl/suggest_rules.py +48 -20
- {proxyctl-0.5.0 → proxyctl-0.5.2}/.gitignore +0 -0
- {proxyctl-0.5.0 → proxyctl-0.5.2}/LICENSE +0 -0
- {proxyctl-0.5.0 → proxyctl-0.5.2}/README.md +0 -0
- {proxyctl-0.5.0 → proxyctl-0.5.2}/man/proxyctl.1 +0 -0
- {proxyctl-0.5.0 → proxyctl-0.5.2}/src/proxyctl/__init__.py +0 -0
- {proxyctl-0.5.0 → proxyctl-0.5.2}/src/proxyctl/_io.py +0 -0
- {proxyctl-0.5.0 → proxyctl-0.5.2}/src/proxyctl/audit.py +0 -0
- {proxyctl-0.5.0 → proxyctl-0.5.2}/src/proxyctl/autostart.py +0 -0
- {proxyctl-0.5.0 → proxyctl-0.5.2}/src/proxyctl/builtin_plugins/__init__.py +0 -0
- {proxyctl-0.5.0 → proxyctl-0.5.2}/src/proxyctl/builtin_plugins/connectivity_basic.py +0 -0
- {proxyctl-0.5.0 → proxyctl-0.5.2}/src/proxyctl/builtin_plugins/corp_network.py +0 -0
- {proxyctl-0.5.0 → proxyctl-0.5.2}/src/proxyctl/check.py +0 -0
- {proxyctl-0.5.0 → proxyctl-0.5.2}/src/proxyctl/completion.py +0 -0
- {proxyctl-0.5.0 → proxyctl-0.5.2}/src/proxyctl/core/__init__.py +0 -0
- {proxyctl-0.5.0 → proxyctl-0.5.2}/src/proxyctl/core/plugin.py +0 -0
- {proxyctl-0.5.0 → proxyctl-0.5.2}/src/proxyctl/engine/__init__.py +0 -0
- {proxyctl-0.5.0 → proxyctl-0.5.2}/src/proxyctl/engine/base.py +0 -0
- {proxyctl-0.5.0 → proxyctl-0.5.2}/src/proxyctl/engine/mihomo.py +0 -0
- {proxyctl-0.5.0 → proxyctl-0.5.2}/src/proxyctl/engine/singbox.py +0 -0
- {proxyctl-0.5.0 → proxyctl-0.5.2}/src/proxyctl/status.py +0 -0
- {proxyctl-0.5.0 → proxyctl-0.5.2}/src/proxyctl/subscription.py +0 -0
- {proxyctl-0.5.0 → proxyctl-0.5.2}/src/proxyctl/suggest.py +0 -0
- {proxyctl-0.5.0 → proxyctl-0.5.2}/src/proxyctl/trace.py +0 -0
|
@@ -266,16 +266,17 @@ def get_engine_version(backend_name: str) -> dict | None:
|
|
|
266
266
|
dict 或 None(binary 找不到 / -v 失败 / 解析失败):
|
|
267
267
|
{
|
|
268
268
|
"binary": "/opt/homebrew/bin/mihomo",
|
|
269
|
-
"version": "1.19.25",
|
|
269
|
+
"version": "1.19.25", # 始终不带 v 前缀(规范化)
|
|
270
270
|
"platform": "darwin arm64",
|
|
271
271
|
"go_version": "1.26.3",
|
|
272
|
-
"build_date": "2026-05-16",
|
|
272
|
+
"build_date": "2026-05-16", # ISO 取日期段;非 ISO 保留原样
|
|
273
273
|
"raw": "Mihomo Meta 1.19.25 darwin arm64 with go1.26.3 2026-05-16T14:37:07Z",
|
|
274
274
|
}
|
|
275
275
|
|
|
276
|
-
mihomo -v
|
|
277
|
-
Mihomo Meta 1.19.25 darwin arm64 with go1.26.3 2026-05-16T14:37:07Z
|
|
278
|
-
|
|
276
|
+
mihomo -v 输出在不同构建里有两种格式(v0.5.2 起两种都兼容):
|
|
277
|
+
1) ISO 风: Mihomo Meta 1.19.25 darwin arm64 with go1.26.3 2026-05-16T14:37:07Z
|
|
278
|
+
2) v 前缀 + Unix `date` 风:
|
|
279
|
+
Mihomo Meta v1.19.10 linux amd64 with go1.24.3 Sat May 31 07:51:30 UTC 2025
|
|
279
280
|
|
|
280
281
|
sing-box version 输出范本 (供后续扩展):
|
|
281
282
|
sing-box version 1.10.0
|
|
@@ -298,16 +299,25 @@ def get_engine_version(backend_name: str) -> dict | None:
|
|
|
298
299
|
return None
|
|
299
300
|
raw = out.split("\n", 1)[0].strip()
|
|
300
301
|
info: dict = {"binary": binary, "raw": raw}
|
|
301
|
-
#
|
|
302
|
+
# 匹配两种 mihomo -v 格式(详见 docstring)。build_date 用 (.+)$ 贪婪到行尾,
|
|
303
|
+
# 容纳 Unix `date` 的 "Sat May 31 07:51:30 UTC 2025" 多字段写法。
|
|
302
304
|
m = re.match(
|
|
303
|
-
r"^\S+\s+\S+\s+(\S+)\s+(\S+\s+\S+)\s+with\s+go(\S+)\s+(
|
|
305
|
+
r"^\S+\s+\S+\s+(\S+)\s+(\S+\s+\S+)\s+with\s+go(\S+)\s+(.+)$",
|
|
304
306
|
raw,
|
|
305
307
|
)
|
|
306
308
|
if m:
|
|
307
|
-
|
|
309
|
+
# version 规范化:剥掉可能的 v 前缀,避免渲染层重复加 v(→ "vv1.19.10")
|
|
310
|
+
info["version"] = m.group(1).lstrip("v")
|
|
308
311
|
info["platform"] = m.group(2)
|
|
309
312
|
info["go_version"] = m.group(3)
|
|
310
|
-
|
|
313
|
+
date_raw = m.group(4).strip()
|
|
314
|
+
# ISO 8601 时间戳(YYYY-MM-DDTHH:MM:SS...)取日期段;
|
|
315
|
+
# Unix `date` 多字段格式(如 "Sat May 31 07:51:30 UTC 2025")保留原样。
|
|
316
|
+
# 必须正则强校验:不能用 "T" in date_raw,否则 "UTC" 会被误判 → 截断。
|
|
317
|
+
if re.match(r"^\d{4}-\d{2}-\d{2}T", date_raw):
|
|
318
|
+
info["build_date"] = date_raw.split("T")[0]
|
|
319
|
+
else:
|
|
320
|
+
info["build_date"] = date_raw
|
|
311
321
|
else:
|
|
312
322
|
# 兜底:能拿到 raw 就行,version 字段留空让 agent 自己处理
|
|
313
323
|
info["version"] = None
|
|
@@ -1949,24 +1949,33 @@ _SUGGESTION_DOCS: dict[str, dict] = {
|
|
|
1949
1949
|
# ── Controller / Engine / Data 5 条 ────────────────────────────────
|
|
1950
1950
|
"controller.empty_secret": {
|
|
1951
1951
|
"summary": (
|
|
1952
|
-
"Clash API
|
|
1953
|
-
"
|
|
1952
|
+
"Clash API bind 公网(0.0.0.0 或局域网 IP)且 secret 为空——"
|
|
1953
|
+
"**任何人都能调你的 API**:切换节点、读取节点列表、改路由。"
|
|
1954
|
+
"v0.5.1+ 此规则仅在公网 bind 时触发;127.0.0.1 + 空 secret 不报。"
|
|
1954
1955
|
),
|
|
1955
1956
|
"edit": (
|
|
1956
|
-
" #
|
|
1957
|
-
"
|
|
1957
|
+
" # 优先:把 bind 改回环回(推荐,本机用够了)\n"
|
|
1958
|
+
" # external-controller: 127.0.0.1:9090\n"
|
|
1959
|
+
" # 或:配强 secret\n"
|
|
1960
|
+
" # secret: <用 openssl rand -hex 24 生成>"
|
|
1958
1961
|
),
|
|
1959
|
-
"verify": "grep -E '^secret:' ~/.config/mihomo/config.yaml",
|
|
1960
|
-
"next_commands": ["explain
|
|
1962
|
+
"verify": "grep -E '^secret:|^external-controller:' ~/.config/mihomo/config.yaml",
|
|
1963
|
+
"next_commands": ["explain suggestion:controller.public_bind"],
|
|
1961
1964
|
},
|
|
1962
1965
|
"controller.weak_secret": {
|
|
1963
1966
|
"summary": (
|
|
1964
|
-
"Clash API secret
|
|
1965
|
-
"
|
|
1967
|
+
"Clash API bind 公网(0.0.0.0 或局域网 IP)且 secret < 16 字符 → "
|
|
1968
|
+
"暴露面 + 弱认证 = 易爆破。**v0.5.1+ 此规则仅在公网 bind 时触发**;"
|
|
1969
|
+
"bind 127.0.0.1 时 secret 强度无意义(attack surface 不存在),不报。"
|
|
1966
1970
|
),
|
|
1967
|
-
"edit":
|
|
1968
|
-
|
|
1969
|
-
|
|
1971
|
+
"edit": (
|
|
1972
|
+
" # 优先:bind 改回环回\n"
|
|
1973
|
+
" # external-controller: 127.0.0.1:9090\n"
|
|
1974
|
+
" # 或:升级 secret\n"
|
|
1975
|
+
" # secret: <用 openssl rand -hex 24 生成的强 secret>"
|
|
1976
|
+
),
|
|
1977
|
+
"verify": "grep -E '^secret:|^external-controller:' ~/.config/mihomo/config.yaml",
|
|
1978
|
+
"next_commands": ["explain suggestion:controller.public_bind"],
|
|
1970
1979
|
},
|
|
1971
1980
|
"controller.public_bind": {
|
|
1972
1981
|
"summary": (
|
|
@@ -109,7 +109,18 @@ def _is_public_bind(host: str | None) -> bool:
|
|
|
109
109
|
|
|
110
110
|
|
|
111
111
|
def controller_rules(cfg: dict[str, Any]) -> list[dict[str, Any]]:
|
|
112
|
-
"""
|
|
112
|
+
"""controller 规则 — v0.5.1 修复复合判定 bug。
|
|
113
|
+
|
|
114
|
+
哥 2026-05-19 指出的设计问题:"只有本机才能访问为什么还要看密钥复杂度?"
|
|
115
|
+
|
|
116
|
+
旧逻辑(v0.5.0)独立判断 empty_secret / weak_secret / public_bind,
|
|
117
|
+
导致 bind=127.0.0.1 + secret 短也会触发 weak_secret advisory ——
|
|
118
|
+
但本地 attack surface 不存在,规则属于误报。
|
|
119
|
+
|
|
120
|
+
新逻辑(v0.5.1):以 attack surface 为单一判定轴。
|
|
121
|
+
bind 127.0.0.1 → 任何 secret(含空)都不报
|
|
122
|
+
bind public → 视 secret 强度报 warn / advisory,public_bind 永远 warn
|
|
123
|
+
"""
|
|
113
124
|
if not cfg.get("config_exists"):
|
|
114
125
|
return []
|
|
115
126
|
out: list[dict[str, Any]] = []
|
|
@@ -117,44 +128,61 @@ def controller_rules(cfg: dict[str, Any]) -> list[dict[str, Any]]:
|
|
|
117
128
|
port = cfg.get("controller_port")
|
|
118
129
|
secret = cfg.get("controller_secret")
|
|
119
130
|
|
|
120
|
-
#
|
|
121
|
-
if (host or port)
|
|
122
|
-
|
|
131
|
+
# controller 未配置时整组规则跳过
|
|
132
|
+
if not (host or port):
|
|
133
|
+
return out
|
|
134
|
+
|
|
135
|
+
public = _is_public_bind(host)
|
|
136
|
+
|
|
137
|
+
# 127.0.0.1 / ::1 → attack surface 不存在,secret 强度不评估
|
|
138
|
+
if not public:
|
|
139
|
+
return out
|
|
140
|
+
|
|
141
|
+
# ── 以下分支仅当 bind 暴露给非本机时触发 ─────────────────────────
|
|
142
|
+
|
|
143
|
+
# public_bind 本身永远报 warn(明确告诉用户"你的 API 暴露给了外面")
|
|
144
|
+
out.append({
|
|
145
|
+
"id": "controller.public_bind",
|
|
146
|
+
"severity": "warn",
|
|
147
|
+
"actor": "user",
|
|
148
|
+
"title": f"Clash API bind 到非环回地址:{host}",
|
|
149
|
+
"evidence": {"controller_host": host, "controller_port": port},
|
|
150
|
+
"doc": "suggestion:controller.public_bind",
|
|
151
|
+
"since": "0.5.0",
|
|
152
|
+
})
|
|
153
|
+
|
|
154
|
+
if secret == "" or secret is None:
|
|
155
|
+
# 公网 bind + 无认证 = 任何人可控制代理 → warn
|
|
123
156
|
out.append({
|
|
124
157
|
"id": "controller.empty_secret",
|
|
125
158
|
"severity": "warn",
|
|
126
159
|
"actor": "user",
|
|
127
|
-
"title": "Clash API secret 为空",
|
|
160
|
+
"title": f"Clash API bind 公网({host})但 secret 为空",
|
|
128
161
|
"evidence": {
|
|
129
162
|
"controller_host": host,
|
|
130
163
|
"controller_port": port,
|
|
131
164
|
"secret_set": False,
|
|
132
165
|
},
|
|
133
|
-
"fix_command": "proxyctl explain
|
|
166
|
+
"fix_command": "proxyctl explain suggestion:controller.empty_secret",
|
|
134
167
|
"doc": "suggestion:controller.empty_secret",
|
|
135
168
|
"since": "0.5.0",
|
|
136
169
|
})
|
|
137
|
-
elif
|
|
170
|
+
elif len(secret) < 16:
|
|
171
|
+
# 公网 bind + 弱 secret = 易爆破 → advisory(有 secret 总比无好,
|
|
172
|
+
# 但应该升级)
|
|
138
173
|
out.append({
|
|
139
174
|
"id": "controller.weak_secret",
|
|
140
175
|
"severity": "advisory",
|
|
141
176
|
"actor": "user",
|
|
142
|
-
"title": f"Clash API
|
|
143
|
-
|
|
177
|
+
"title": (f"Clash API bind 公网({host})且 secret 仅 "
|
|
178
|
+
f"{len(secret)} 字符(< 16)"),
|
|
179
|
+
"evidence": {
|
|
180
|
+
"controller_host": host,
|
|
181
|
+
"secret_length": len(secret),
|
|
182
|
+
},
|
|
144
183
|
"doc": "suggestion:controller.weak_secret",
|
|
145
184
|
"since": "0.5.0",
|
|
146
185
|
})
|
|
147
|
-
|
|
148
|
-
if _is_public_bind(host):
|
|
149
|
-
out.append({
|
|
150
|
-
"id": "controller.public_bind",
|
|
151
|
-
"severity": "warn",
|
|
152
|
-
"actor": "user",
|
|
153
|
-
"title": f"Clash API bind 到非环回地址:{host}",
|
|
154
|
-
"evidence": {"controller_host": host, "controller_port": port},
|
|
155
|
-
"doc": "suggestion:controller.public_bind",
|
|
156
|
-
"since": "0.5.0",
|
|
157
|
-
})
|
|
158
186
|
return out
|
|
159
187
|
|
|
160
188
|
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|