proxyctl 0.5.10__tar.gz → 0.5.11__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 (33) hide show
  1. {proxyctl-0.5.10 → proxyctl-0.5.11}/PKG-INFO +1 -1
  2. {proxyctl-0.5.10 → proxyctl-0.5.11}/pyproject.toml +1 -1
  3. {proxyctl-0.5.10 → proxyctl-0.5.11}/src/proxyctl/trace.py +83 -2
  4. {proxyctl-0.5.10 → proxyctl-0.5.11}/.gitignore +0 -0
  5. {proxyctl-0.5.10 → proxyctl-0.5.11}/LICENSE +0 -0
  6. {proxyctl-0.5.10 → proxyctl-0.5.11}/README.md +0 -0
  7. {proxyctl-0.5.10 → proxyctl-0.5.11}/man/proxyctl.1 +0 -0
  8. {proxyctl-0.5.10 → proxyctl-0.5.11}/src/proxyctl/__init__.py +0 -0
  9. {proxyctl-0.5.10 → proxyctl-0.5.11}/src/proxyctl/_io.py +0 -0
  10. {proxyctl-0.5.10 → proxyctl-0.5.11}/src/proxyctl/audit.py +0 -0
  11. {proxyctl-0.5.10 → proxyctl-0.5.11}/src/proxyctl/autostart.py +0 -0
  12. {proxyctl-0.5.10 → proxyctl-0.5.11}/src/proxyctl/builtin_plugins/__init__.py +0 -0
  13. {proxyctl-0.5.10 → proxyctl-0.5.11}/src/proxyctl/builtin_plugins/connectivity_basic.py +0 -0
  14. {proxyctl-0.5.10 → proxyctl-0.5.11}/src/proxyctl/builtin_plugins/corp_network.py +0 -0
  15. {proxyctl-0.5.10 → proxyctl-0.5.11}/src/proxyctl/check.py +0 -0
  16. {proxyctl-0.5.10 → proxyctl-0.5.11}/src/proxyctl/cli.py +0 -0
  17. {proxyctl-0.5.10 → proxyctl-0.5.11}/src/proxyctl/completion.py +0 -0
  18. {proxyctl-0.5.10 → proxyctl-0.5.11}/src/proxyctl/connections.py +0 -0
  19. {proxyctl-0.5.10 → proxyctl-0.5.11}/src/proxyctl/connections_filters.py +0 -0
  20. {proxyctl-0.5.10 → proxyctl-0.5.11}/src/proxyctl/connections_human.py +0 -0
  21. {proxyctl-0.5.10 → proxyctl-0.5.11}/src/proxyctl/core/__init__.py +0 -0
  22. {proxyctl-0.5.10 → proxyctl-0.5.11}/src/proxyctl/core/plugin.py +0 -0
  23. {proxyctl-0.5.10 → proxyctl-0.5.11}/src/proxyctl/engine/__init__.py +0 -0
  24. {proxyctl-0.5.10 → proxyctl-0.5.11}/src/proxyctl/engine/base.py +0 -0
  25. {proxyctl-0.5.10 → proxyctl-0.5.11}/src/proxyctl/engine/mihomo.py +0 -0
  26. {proxyctl-0.5.10 → proxyctl-0.5.11}/src/proxyctl/engine/singbox.py +0 -0
  27. {proxyctl-0.5.10 → proxyctl-0.5.11}/src/proxyctl/explain.py +0 -0
  28. {proxyctl-0.5.10 → proxyctl-0.5.11}/src/proxyctl/status.py +0 -0
  29. {proxyctl-0.5.10 → proxyctl-0.5.11}/src/proxyctl/subscription.py +0 -0
  30. {proxyctl-0.5.10 → proxyctl-0.5.11}/src/proxyctl/suggest.py +0 -0
  31. {proxyctl-0.5.10 → proxyctl-0.5.11}/src/proxyctl/suggest_rules.py +0 -0
  32. {proxyctl-0.5.10 → proxyctl-0.5.11}/src/proxyctl/traffic.py +0 -0
  33. {proxyctl-0.5.10 → proxyctl-0.5.11}/src/proxyctl/traffic_store.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: proxyctl
3
- Version: 0.5.10
3
+ Version: 0.5.11
4
4
  Summary: Proxy configuration lifecycle management for macOS and Linux
5
5
  Project-URL: Homepage, https://github.com/crhan/proxyctl
6
6
  Project-URL: Issues, https://github.com/crhan/proxyctl/issues
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "proxyctl"
3
- version = "0.5.10"
3
+ version = "0.5.11"
4
4
  description = "Proxy configuration lifecycle management for macOS and Linux"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.10"
@@ -93,6 +93,52 @@ def _is_ip(s: str) -> bool:
93
93
  return False
94
94
 
95
95
 
96
+ def _cidr_contains_any(cidr: str, ips: list) -> bool:
97
+ """resolved_ips 里是否有任一 IP 落在 cidr 网段内。"""
98
+ try:
99
+ net = ipaddress.ip_network(cidr, strict=False)
100
+ except ValueError:
101
+ return False
102
+ for ip in ips:
103
+ try:
104
+ if ipaddress.ip_address(ip) in net:
105
+ return True
106
+ except ValueError:
107
+ pass
108
+ return False
109
+
110
+
111
+ def _load_noresolve_payloads(config_path: str = None) -> set:
112
+ """读取 mihomo 配置 rules 段,返回带 no-resolve 的 IP 规则 payload 集合。
113
+
114
+ Clash /rules API 不暴露 no-resolve 标志,预测器只能从配置文件解析。
115
+ 这些规则在域名连接(HTTP 代理 / fake-ip)下引擎不会解析域名,故不命中——
116
+ 预测器必须据此跳过,否则会误报命中(典型:100.64.0.0/10,DIRECT,no-resolve)。
117
+ """
118
+ import os
119
+ if config_path is None:
120
+ config_path = os.path.expanduser("~/.config/mihomo/config.yaml")
121
+ payloads: set = set()
122
+ try:
123
+ text = open(config_path).read()
124
+ except OSError:
125
+ return payloads
126
+ in_rules = False
127
+ for line in text.splitlines():
128
+ if re.match(r'^rules:\s*$', line):
129
+ in_rules = True
130
+ continue
131
+ if not in_rules:
132
+ continue
133
+ # rules 段结束:遇到顶格的新顶层键(非缩进、非列表项)
134
+ if line and not line[0].isspace() and not line.lstrip().startswith('-'):
135
+ break
136
+ m = re.match(r'\s*-\s*(?:IP-CIDR6?|IP-SUFFIX),([^,]+),[^,]+,\s*no-resolve\b', line)
137
+ if m:
138
+ payloads.add(m.group(1).strip())
139
+ return payloads
140
+
141
+
96
142
  def _section_dns(domain: str, api: str, secret: str,
97
143
  fakeip_active: bool = False, corp_dns: dict = None) -> list:
98
144
  """
@@ -208,9 +254,16 @@ def _section_dns(domain: str, api: str, secret: str,
208
254
  return resolved_ips
209
255
 
210
256
 
211
- def _section_rules(domain: str, resolved_ips: list, api: str, secret: str) -> tuple:
257
+ def _section_rules(domain: str, resolved_ips: list, api: str, secret: str,
258
+ noresolve_can_match: bool = True,
259
+ noresolve_payloads: set = None) -> tuple:
212
260
  """
213
261
  [2/4] 规则匹配 — 按引擎规则顺序逐条预测。
262
+
263
+ Args:
264
+ noresolve_can_match: 当前连接是否带真实目的 IP。域名经 HTTP 代理 / fake-ip
265
+ 进来时为 False——此时引擎走到 no-resolve 的 IP 规则尚未解析,会跳过它们。
266
+ noresolve_payloads: 配置里带 no-resolve 的 IP 规则 payload 集合(_load_noresolve_payloads)。
214
267
  返回 (predicted_rule, predicted_proxy)。
215
268
  """
216
269
  print(f"\n{BOLD}[2/4] 规则匹配{NC}")
@@ -219,6 +272,9 @@ def _section_rules(domain: str, resolved_ips: list, api: str, secret: str) -> tu
219
272
  print(f" {RED}无法获取规则列表{NC}")
220
273
  return None, None
221
274
 
275
+ noresolve_payloads = noresolve_payloads or set()
276
+ skipped_noresolve: list = [] # 因 no-resolve 被跳过、但本可命中的规则(用于提示)
277
+
222
278
  predicted_rule = predicted_proxy = None
223
279
  for rule in rules_data.get("rules", []):
224
280
  rtype = rule.get("type", "")
@@ -241,6 +297,14 @@ def _section_rules(domain: str, resolved_ips: list, api: str, secret: str) -> tu
241
297
  matched = True
242
298
  detail = f"精确域名 {payload}"
243
299
  elif rtype == "IPCIDR" and resolved_ips:
300
+ # no-resolve 的 IP 规则只在连接本身带真实目的 IP 时才会被引擎匹配。
301
+ # 域名经 HTTP 代理 / fake-ip 进来时,引擎走到这条还没解析 → 跳过,
302
+ # 否则误报命中(典型坑:100.64.0.0/10,DIRECT,no-resolve 被预测成 DIRECT,
303
+ # 实际却落兜底走 proxy)。
304
+ if (not noresolve_can_match) and payload in noresolve_payloads:
305
+ if _cidr_contains_any(payload, resolved_ips):
306
+ skipped_noresolve.append((rtype, payload, proxy))
307
+ continue
244
308
  try:
245
309
  net = ipaddress.ip_network(payload, strict=False)
246
310
  for ip in resolved_ips:
@@ -276,6 +340,13 @@ def _section_rules(domain: str, resolved_ips: list, api: str, secret: str) -> tu
276
340
  )
277
341
  print(f" {DIM}注: 跳过了 {len(uncertain)} 条 GeoSite/GeoIP 规则 "
278
342
  f"(无法客户端匹配): {types_str}{NC}")
343
+
344
+ if skipped_noresolve:
345
+ nr_str = ", ".join(
346
+ f"{t}({p})→{pr}" for t, p, pr in skipped_noresolve[-3:]
347
+ )
348
+ print(f" {YELLOW}注: 跳过了 {len(skipped_noresolve)} 条 no-resolve IP 规则 "
349
+ f"(域名连接下引擎不解析,本应命中却不命中): {nr_str}{NC}")
279
350
  break
280
351
 
281
352
  if not predicted_rule:
@@ -568,7 +639,17 @@ def cmd_trace(raw_input: str, api: str, secret: str, config: dict = None):
568
639
  resolved_ips = _section_dns(domain, api, secret, fakeip_active, corp_dns)
569
640
  collector["stages"]["dns"] = {"resolved": list(resolved_ips), "skipped": False}
570
641
 
571
- predicted_rule, predicted_proxy = _section_rules(domain, resolved_ips, api, secret)
642
+ # no-resolve 的 IP 规则只在连接带真实目的 IP 时才会被引擎匹配:
643
+ # - 目标输入就是 IP
644
+ # - redir-host + TUN on(内核按真实 IP 路由)
645
+ # 域名经 HTTP 代理 / fake-ip 进来时尚未解析 → 这些规则被引擎跳过
646
+ noresolve_can_match = _is_ip(domain) or (
647
+ mode["enhanced_mode"] != "fake-ip" and mode["tun_enabled"]
648
+ )
649
+ predicted_rule, predicted_proxy = _section_rules(
650
+ domain, resolved_ips, api, secret,
651
+ noresolve_can_match, _load_noresolve_payloads(),
652
+ )
572
653
  collector["stages"]["rules"] = {
573
654
  "predicted_rule": predicted_rule,
574
655
  "predicted_proxy": predicted_proxy,
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes