proxyctl 0.5.10__tar.gz → 0.5.12__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.10 → proxyctl-0.5.12}/PKG-INFO +1 -1
- {proxyctl-0.5.10 → proxyctl-0.5.12}/pyproject.toml +1 -1
- {proxyctl-0.5.10 → proxyctl-0.5.12}/src/proxyctl/cli.py +26 -3
- {proxyctl-0.5.10 → proxyctl-0.5.12}/src/proxyctl/explain.py +4 -1
- {proxyctl-0.5.10 → proxyctl-0.5.12}/src/proxyctl/trace.py +152 -51
- {proxyctl-0.5.10 → proxyctl-0.5.12}/.gitignore +0 -0
- {proxyctl-0.5.10 → proxyctl-0.5.12}/LICENSE +0 -0
- {proxyctl-0.5.10 → proxyctl-0.5.12}/README.md +0 -0
- {proxyctl-0.5.10 → proxyctl-0.5.12}/man/proxyctl.1 +0 -0
- {proxyctl-0.5.10 → proxyctl-0.5.12}/src/proxyctl/__init__.py +0 -0
- {proxyctl-0.5.10 → proxyctl-0.5.12}/src/proxyctl/_io.py +0 -0
- {proxyctl-0.5.10 → proxyctl-0.5.12}/src/proxyctl/audit.py +0 -0
- {proxyctl-0.5.10 → proxyctl-0.5.12}/src/proxyctl/autostart.py +0 -0
- {proxyctl-0.5.10 → proxyctl-0.5.12}/src/proxyctl/builtin_plugins/__init__.py +0 -0
- {proxyctl-0.5.10 → proxyctl-0.5.12}/src/proxyctl/builtin_plugins/connectivity_basic.py +0 -0
- {proxyctl-0.5.10 → proxyctl-0.5.12}/src/proxyctl/builtin_plugins/corp_network.py +0 -0
- {proxyctl-0.5.10 → proxyctl-0.5.12}/src/proxyctl/check.py +0 -0
- {proxyctl-0.5.10 → proxyctl-0.5.12}/src/proxyctl/completion.py +0 -0
- {proxyctl-0.5.10 → proxyctl-0.5.12}/src/proxyctl/connections.py +0 -0
- {proxyctl-0.5.10 → proxyctl-0.5.12}/src/proxyctl/connections_filters.py +0 -0
- {proxyctl-0.5.10 → proxyctl-0.5.12}/src/proxyctl/connections_human.py +0 -0
- {proxyctl-0.5.10 → proxyctl-0.5.12}/src/proxyctl/core/__init__.py +0 -0
- {proxyctl-0.5.10 → proxyctl-0.5.12}/src/proxyctl/core/plugin.py +0 -0
- {proxyctl-0.5.10 → proxyctl-0.5.12}/src/proxyctl/engine/__init__.py +0 -0
- {proxyctl-0.5.10 → proxyctl-0.5.12}/src/proxyctl/engine/base.py +0 -0
- {proxyctl-0.5.10 → proxyctl-0.5.12}/src/proxyctl/engine/mihomo.py +0 -0
- {proxyctl-0.5.10 → proxyctl-0.5.12}/src/proxyctl/engine/singbox.py +0 -0
- {proxyctl-0.5.10 → proxyctl-0.5.12}/src/proxyctl/status.py +0 -0
- {proxyctl-0.5.10 → proxyctl-0.5.12}/src/proxyctl/subscription.py +0 -0
- {proxyctl-0.5.10 → proxyctl-0.5.12}/src/proxyctl/suggest.py +0 -0
- {proxyctl-0.5.10 → proxyctl-0.5.12}/src/proxyctl/suggest_rules.py +0 -0
- {proxyctl-0.5.10 → proxyctl-0.5.12}/src/proxyctl/traffic.py +0 -0
- {proxyctl-0.5.10 → proxyctl-0.5.12}/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.
|
|
3
|
+
Version: 0.5.12
|
|
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
|
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
"""
|
|
8
8
|
|
|
9
9
|
import functools
|
|
10
|
+
import ipaddress
|
|
10
11
|
import json
|
|
11
12
|
import os
|
|
12
13
|
import platform
|
|
@@ -1327,6 +1328,30 @@ def cmd_dns_unlock(config: dict):
|
|
|
1327
1328
|
|
|
1328
1329
|
# ── 命令:env ────────────────────────────────────────────────────────────────
|
|
1329
1330
|
|
|
1331
|
+
def _normalize_no_proxy_extra(extra) -> list[str]:
|
|
1332
|
+
"""Return NO_PROXY extras compatible with Python/httpx environment parsing."""
|
|
1333
|
+
if not extra:
|
|
1334
|
+
return []
|
|
1335
|
+
if isinstance(extra, str):
|
|
1336
|
+
items = [s.strip() for s in extra.split(",")]
|
|
1337
|
+
else:
|
|
1338
|
+
items = [str(s).strip() for s in extra]
|
|
1339
|
+
|
|
1340
|
+
out: list[str] = []
|
|
1341
|
+
for item in items:
|
|
1342
|
+
if not item:
|
|
1343
|
+
continue
|
|
1344
|
+
try:
|
|
1345
|
+
network = ipaddress.ip_network(item, strict=False)
|
|
1346
|
+
except ValueError:
|
|
1347
|
+
out.append(item)
|
|
1348
|
+
continue
|
|
1349
|
+
if network.version == 6 and "/" in item:
|
|
1350
|
+
continue
|
|
1351
|
+
out.append(item)
|
|
1352
|
+
return out
|
|
1353
|
+
|
|
1354
|
+
|
|
1330
1355
|
def cmd_env(config: dict, unset: bool = False):
|
|
1331
1356
|
"""输出设置/清除代理环境变量的 shell 语句。
|
|
1332
1357
|
|
|
@@ -1350,9 +1375,7 @@ def cmd_env(config: dict, unset: bool = False):
|
|
|
1350
1375
|
proxy_socks = f"socks5://127.0.0.1:{port}"
|
|
1351
1376
|
no_proxy = "localhost,127.0.0.1,::1,10.0.0.0/8,172.16.0.0/12,192.168.0.0/16"
|
|
1352
1377
|
# 用户附加的 NO_PROXY 项(个人域名等);接受 list[str] 或逗号分隔 str
|
|
1353
|
-
extra = config.get("no_proxy_extra")
|
|
1354
|
-
if isinstance(extra, str):
|
|
1355
|
-
extra = [s.strip() for s in extra.split(",") if s.strip()]
|
|
1378
|
+
extra = _normalize_no_proxy_extra(config.get("no_proxy_extra"))
|
|
1356
1379
|
if extra:
|
|
1357
1380
|
no_proxy = no_proxy + "," + ",".join(extra)
|
|
1358
1381
|
|
|
@@ -217,7 +217,10 @@ def _t_env(backend, config) -> TopicCard:
|
|
|
217
217
|
f"`proxyctl env` 输出 export 行,可 eval 进当前 shell(端口 {port})。"
|
|
218
218
|
),
|
|
219
219
|
"file": _io_proxyctl_config_path() + " [no_proxy_extra: 字段]",
|
|
220
|
-
"edit":
|
|
220
|
+
"edit": (
|
|
221
|
+
" # no_proxy_extra: 追加内网域名 / IPv4 CIDR / 企业 host\n"
|
|
222
|
+
" # 裸 IPv6 CIDR 会被跳过,避免 Python/httpx 误解析 NO_PROXY"
|
|
223
|
+
),
|
|
221
224
|
"verify": "eval \"$(proxyctl env)\" && env | grep -i proxy",
|
|
222
225
|
"next_commands": ["env", "env --unset"],
|
|
223
226
|
}
|
|
@@ -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
|
|
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:
|
|
@@ -435,20 +506,30 @@ def _section_connections(domain: str, resolved_ips: list,
|
|
|
435
506
|
"""[4/4] 实际连接验证 — 从 Clash API 活跃连接 + 日志历史中取路由信息。"""
|
|
436
507
|
print(f"\n{BOLD}[4/4] 实际连接{NC}")
|
|
437
508
|
|
|
438
|
-
|
|
509
|
+
host_conns = []
|
|
510
|
+
ip_conns = []
|
|
439
511
|
for _ in range(3):
|
|
440
512
|
data = _api_get(api, "/connections", secret)
|
|
441
513
|
if data and isinstance(data, dict):
|
|
442
514
|
for c in (data.get("connections") or []):
|
|
443
515
|
m = c.get("metadata", {})
|
|
444
|
-
h = m.get("host", "")
|
|
445
516
|
di = m.get("destinationIP", "")
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
517
|
+
# sniffer/TUN 模式下 host 可能是目的 IP 或为空,真实域名落在
|
|
518
|
+
# sniffHost(嗅探到的 SNI / HTTP Host)。两字段独立判断、不互相
|
|
519
|
+
# 遮蔽:任一匹配即纳入;只有两者都缺失(纯 IP)才按目的 IP 回退
|
|
520
|
+
hosts = [x for x in (m.get("host", ""), m.get("sniffHost", "")) if x]
|
|
521
|
+
if any(x == domain or x.endswith("." + domain) for x in hosts):
|
|
522
|
+
host_conns.append(c)
|
|
523
|
+
elif not hosts and di and di in resolved_ips:
|
|
524
|
+
# host/sniffHost 都没有的纯 IP 连接才回退;任一字段非空但
|
|
525
|
+
# 指向别的域名的连接,即使同 IP 也不算进来——否则共享 IP 下
|
|
526
|
+
# 会把同 IP 别站的活跃连接误算成本域名
|
|
527
|
+
ip_conns.append(c)
|
|
528
|
+
if host_conns or ip_conns:
|
|
450
529
|
break
|
|
451
530
|
time.sleep(0.3)
|
|
531
|
+
# host 精确匹配优先;只有全是无 host 的连接时才回退到按 IP 匹配的那批
|
|
532
|
+
domain_conns = host_conns if host_conns else ip_conns
|
|
452
533
|
|
|
453
534
|
if not domain_conns:
|
|
454
535
|
# 没有活跃连接,从引擎日志中捞历史记录
|
|
@@ -465,54 +546,64 @@ def _section_connections(domain: str, resolved_ips: list,
|
|
|
465
546
|
print(f" {DIM}基于规则预测,该域名应走: {BOLD}{predicted_proxy}{NC}")
|
|
466
547
|
return
|
|
467
548
|
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
549
|
+
def fmt_bytes(b: int) -> str:
|
|
550
|
+
if b < 1024: return f"{b}B"
|
|
551
|
+
if b < 1024 * 1024: return f"{b/1024:.1f}K"
|
|
552
|
+
return f"{b/1024/1024:.1f}M"
|
|
553
|
+
|
|
554
|
+
# 按完整链路聚合:相同链路的连接合并计数、流量累加,不再逐条平铺
|
|
555
|
+
groups: dict = {}
|
|
471
556
|
for c in domain_conns:
|
|
472
|
-
rule = c.get("rule", "?")
|
|
473
|
-
rp = c.get("rulePayload", "")
|
|
474
557
|
chains = c.get("chains", [])
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
558
|
+
chain_str = " → ".join(reversed(chains)) if chains else "?"
|
|
559
|
+
g = groups.setdefault(chain_str, {"chains": chains, "count": 0,
|
|
560
|
+
"up": 0, "down": 0})
|
|
561
|
+
g["count"] += 1
|
|
562
|
+
g["up"] += c.get("upload", 0)
|
|
563
|
+
g["down"] += c.get("download", 0)
|
|
564
|
+
|
|
565
|
+
# 是否符合预测:预测的组名出现在链路任意一跳即算命中。
|
|
566
|
+
# proxy 链 chains=[节点, proxy-tuic, proxy] → 含 'proxy' → 符合
|
|
567
|
+
# DIRECT 链 chains=['DIRECT'] → 不含 → 不符合
|
|
568
|
+
# (不再纠结 chains[0] 是末端节点还是 chains[-1] 是入口组——用集合判定消除歧义)
|
|
569
|
+
def _matches(chains: list):
|
|
570
|
+
if not predicted_proxy:
|
|
571
|
+
return None
|
|
572
|
+
return any(ch.lower() == predicted_proxy.lower() for ch in chains)
|
|
573
|
+
|
|
574
|
+
total = len(domain_conns)
|
|
575
|
+
print(f" {DIM}(mihomo 当前活跃连接 {total} 条){NC}")
|
|
490
576
|
if predicted_proxy:
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
577
|
+
print(f" 应走(规则预测): {BOLD}{predicted_proxy}{NC}")
|
|
578
|
+
print(" 实走:")
|
|
579
|
+
|
|
580
|
+
items = [(_matches(g["chains"]), s, g) for s, g in groups.items()]
|
|
581
|
+
items.sort(key=lambda x: (x[0] is not True, -x[2]["count"])) # 符合在前,多的在前
|
|
582
|
+
|
|
583
|
+
match_n = mismatch_n = 0
|
|
584
|
+
for ok, chain_str, g in items:
|
|
585
|
+
if ok is True:
|
|
586
|
+
mark = f"{GREEN}✓{NC}"; match_n += g["count"]
|
|
587
|
+
elif ok is False:
|
|
588
|
+
mark = f"{YELLOW}✗{NC}"; mismatch_n += g["count"]
|
|
495
589
|
else:
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
print(f" {DIM}---{NC}")
|
|
500
|
-
domain_conns.sort(key=lambda x: x.get("start", ""), reverse=True)
|
|
501
|
-
for c in domain_conns[:5]:
|
|
502
|
-
m = c.get("metadata", {})
|
|
503
|
-
host = m.get("host", "?")
|
|
504
|
-
dport = m.get("destinationPort", "?")
|
|
505
|
-
up = c.get("upload", 0)
|
|
506
|
-
down = c.get("download", 0)
|
|
507
|
-
chains = c.get("chains", [])
|
|
508
|
-
outbound = chains[0] if chains else "?"
|
|
590
|
+
mark = " "
|
|
591
|
+
flow = f"↑{fmt_bytes(g['up'])} ↓{fmt_bytes(g['down'])}"
|
|
592
|
+
print(f" {mark} {chain_str} {DIM}·{NC} {g['count']} 条 {DIM}{flow}{NC}")
|
|
509
593
|
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
print(f" {
|
|
594
|
+
if not predicted_proxy:
|
|
595
|
+
return
|
|
596
|
+
if mismatch_n == 0:
|
|
597
|
+
print(f" {GREEN}✓ 结论: 全部 {total} 条都走 {predicted_proxy},与规则一致{NC}")
|
|
598
|
+
elif match_n == 0:
|
|
599
|
+
print(f" {YELLOW}✗ 结论: {total} 条都没走 {predicted_proxy};"
|
|
600
|
+
f"规则可能未生效,或被前置规则截胡{NC}")
|
|
601
|
+
else:
|
|
602
|
+
print(f" {YELLOW}⚠ 结论: {match_n} 条新连接已走 {predicted_proxy},"
|
|
603
|
+
f"另 {mismatch_n} 条仍走旧链路{NC}")
|
|
604
|
+
print(f" {DIM} 旧链路是改规则前建立的连接(建连时已定死,不随规则切换),"
|
|
605
|
+
f"会随超时自然断开;{NC}")
|
|
606
|
+
print(f" {DIM} 刷新页面后即全部走 {predicted_proxy}。{NC}")
|
|
516
607
|
|
|
517
608
|
|
|
518
609
|
def cmd_trace(raw_input: str, api: str, secret: str, config: dict = None):
|
|
@@ -568,7 +659,17 @@ def cmd_trace(raw_input: str, api: str, secret: str, config: dict = None):
|
|
|
568
659
|
resolved_ips = _section_dns(domain, api, secret, fakeip_active, corp_dns)
|
|
569
660
|
collector["stages"]["dns"] = {"resolved": list(resolved_ips), "skipped": False}
|
|
570
661
|
|
|
571
|
-
|
|
662
|
+
# no-resolve 的 IP 规则只在连接带真实目的 IP 时才会被引擎匹配:
|
|
663
|
+
# - 目标输入就是 IP
|
|
664
|
+
# - redir-host + TUN on(内核按真实 IP 路由)
|
|
665
|
+
# 域名经 HTTP 代理 / fake-ip 进来时尚未解析 → 这些规则被引擎跳过
|
|
666
|
+
noresolve_can_match = _is_ip(domain) or (
|
|
667
|
+
mode["enhanced_mode"] != "fake-ip" and mode["tun_enabled"]
|
|
668
|
+
)
|
|
669
|
+
predicted_rule, predicted_proxy = _section_rules(
|
|
670
|
+
domain, resolved_ips, api, secret,
|
|
671
|
+
noresolve_can_match, _load_noresolve_payloads(),
|
|
672
|
+
)
|
|
572
673
|
collector["stages"]["rules"] = {
|
|
573
674
|
"predicted_rule": predicted_rule,
|
|
574
675
|
"predicted_proxy": predicted_proxy,
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|