proxyctl 0.1.3__tar.gz → 0.1.5__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.1.3 → proxyctl-0.1.5}/PKG-INFO +1 -1
- {proxyctl-0.1.3 → proxyctl-0.1.5}/pyproject.toml +1 -1
- {proxyctl-0.1.3 → proxyctl-0.1.5}/src/proxyctl/check.py +24 -13
- {proxyctl-0.1.3 → proxyctl-0.1.5}/src/proxyctl/cli.py +15 -3
- {proxyctl-0.1.3 → proxyctl-0.1.5}/src/proxyctl/status.py +22 -5
- {proxyctl-0.1.3 → proxyctl-0.1.5}/.gitignore +0 -0
- {proxyctl-0.1.3 → proxyctl-0.1.5}/LICENSE +0 -0
- {proxyctl-0.1.3 → proxyctl-0.1.5}/README.md +0 -0
- {proxyctl-0.1.3 → proxyctl-0.1.5}/src/proxyctl/__init__.py +0 -0
- {proxyctl-0.1.3 → proxyctl-0.1.5}/src/proxyctl/audit.py +0 -0
- {proxyctl-0.1.3 → proxyctl-0.1.5}/src/proxyctl/builtin_plugins/__init__.py +0 -0
- {proxyctl-0.1.3 → proxyctl-0.1.5}/src/proxyctl/builtin_plugins/connectivity_basic.py +0 -0
- {proxyctl-0.1.3 → proxyctl-0.1.5}/src/proxyctl/builtin_plugins/corp_network.py +0 -0
- {proxyctl-0.1.3 → proxyctl-0.1.5}/src/proxyctl/core/__init__.py +0 -0
- {proxyctl-0.1.3 → proxyctl-0.1.5}/src/proxyctl/core/plugin.py +0 -0
- {proxyctl-0.1.3 → proxyctl-0.1.5}/src/proxyctl/engine/__init__.py +0 -0
- {proxyctl-0.1.3 → proxyctl-0.1.5}/src/proxyctl/engine/base.py +0 -0
- {proxyctl-0.1.3 → proxyctl-0.1.5}/src/proxyctl/engine/mihomo.py +0 -0
- {proxyctl-0.1.3 → proxyctl-0.1.5}/src/proxyctl/engine/singbox.py +0 -0
- {proxyctl-0.1.3 → proxyctl-0.1.5}/src/proxyctl/trace.py +0 -0
|
@@ -36,10 +36,11 @@ def _port_listening(port: int) -> bool:
|
|
|
36
36
|
return False
|
|
37
37
|
|
|
38
38
|
|
|
39
|
-
def _test_url(url: str, desc: str, mode: str = "proxy", timeout: int = 8
|
|
39
|
+
def _test_url(url: str, desc: str, mode: str = "proxy", timeout: int = 8,
|
|
40
|
+
proxy_port: int = 7890) -> tuple:
|
|
40
41
|
"""
|
|
41
42
|
测试 URL 可达性。
|
|
42
|
-
mode=proxy: 走 socks5h://127.0.0.1:
|
|
43
|
+
mode=proxy: 走 socks5h://127.0.0.1:{proxy_port}(需配置 mixed-port 或 socks-port)
|
|
43
44
|
mode=direct: 绕过所有代理 (--noproxy '*')
|
|
44
45
|
返回 (ok: bool, line: str),调用方负责 print。
|
|
45
46
|
"""
|
|
@@ -49,7 +50,7 @@ def _test_url(url: str, desc: str, mode: str = "proxy", timeout: int = 8) -> tup
|
|
|
49
50
|
cmd = ["curl", "-s", "-o", "/dev/null", "-w", "%{http_code}",
|
|
50
51
|
"--max-time", str(timeout)]
|
|
51
52
|
if mode == "proxy":
|
|
52
|
-
cmd += ["--proxy", "socks5h://127.0.0.1:
|
|
53
|
+
cmd += ["--proxy", f"socks5h://127.0.0.1:{proxy_port}"]
|
|
53
54
|
else:
|
|
54
55
|
cmd += ["--noproxy", "*"]
|
|
55
56
|
cmd.append(url)
|
|
@@ -281,7 +282,8 @@ def _proxy_groups_section(api_base: str, api_secret: str,
|
|
|
281
282
|
return True
|
|
282
283
|
|
|
283
284
|
|
|
284
|
-
def _ipgeo(ip: str, cache_file: str, api_secret: str
|
|
285
|
+
def _ipgeo(ip: str, cache_file: str, api_secret: str,
|
|
286
|
+
proxy_port: int = 7890) -> str:
|
|
285
287
|
"""查询 IP 归属地,带文件缓存。返回 'city,country|org' 格式。"""
|
|
286
288
|
if not ip:
|
|
287
289
|
return ""
|
|
@@ -293,7 +295,8 @@ def _ipgeo(ip: str, cache_file: str, api_secret: str) -> str:
|
|
|
293
295
|
if k not in ("http_proxy", "https_proxy", "all_proxy",
|
|
294
296
|
"HTTP_PROXY", "HTTPS_PROXY", "ALL_PROXY")}
|
|
295
297
|
r = subprocess.run(
|
|
296
|
-
["curl", "-s", "--max-time", "6",
|
|
298
|
+
["curl", "-s", "--max-time", "6",
|
|
299
|
+
"--proxy", f"socks5h://127.0.0.1:{proxy_port}",
|
|
297
300
|
f"https://ipinfo.io/{ip}/json"],
|
|
298
301
|
capture_output=True, text=True, env=env, timeout=10
|
|
299
302
|
)
|
|
@@ -428,11 +431,11 @@ def cmd_bench(api: str, api_secret: str, groups: list = None,
|
|
|
428
431
|
_proxy_groups_section(api, api_secret, groups=list(group_members.keys()))
|
|
429
432
|
|
|
430
433
|
|
|
431
|
-
def _fetch_probe(probe, env_clean: dict) -> str:
|
|
434
|
+
def _fetch_probe(probe, env_clean: dict, proxy_port: int = 7890) -> str:
|
|
432
435
|
"""根据 OutboundProbe 配置发起一次 IP 查询,返回提取后的 IP。"""
|
|
433
436
|
cmd = ["curl", "-s", "--max-time", str(probe.timeout)]
|
|
434
437
|
if probe.mode == "proxy":
|
|
435
|
-
cmd += ["--proxy", "socks5h://127.0.0.1:
|
|
438
|
+
cmd += ["--proxy", f"socks5h://127.0.0.1:{proxy_port}"]
|
|
436
439
|
else:
|
|
437
440
|
cmd += ["--noproxy", "*"]
|
|
438
441
|
cmd.append(probe.url)
|
|
@@ -478,9 +481,13 @@ def cmd_check(engine, api: str, api_secret: str,
|
|
|
478
481
|
probes = registry.collect("check_outbound_probes", ctx={})
|
|
479
482
|
probe_ips: dict[str, str] = {p.name: "" for p in probes}
|
|
480
483
|
|
|
484
|
+
# 临时常量:probes 在端口解析之前启动,所以这里用 config 直接拿
|
|
485
|
+
_probe_proxy_port = int(config.get("proxy_port", 7890))
|
|
486
|
+
|
|
481
487
|
def _make_fetcher(probe):
|
|
482
488
|
def _run():
|
|
483
|
-
probe_ips[probe.name] = _fetch_probe(probe, env_clean
|
|
489
|
+
probe_ips[probe.name] = _fetch_probe(probe, env_clean,
|
|
490
|
+
proxy_port=_probe_proxy_port)
|
|
484
491
|
return _run
|
|
485
492
|
|
|
486
493
|
ip_threads = [threading.Thread(target=_make_fetcher(p)) for p in probes]
|
|
@@ -521,10 +528,13 @@ def cmd_check(engine, api: str, api_secret: str,
|
|
|
521
528
|
print(f" {RED}✗{NC} daemon not running — 执行 proxyctl start")
|
|
522
529
|
return
|
|
523
530
|
|
|
524
|
-
# 端口检测(proxy 模式不检查 53
|
|
531
|
+
# 端口检测(proxy 模式不检查 53)— 端口来自 config + api_base
|
|
532
|
+
from urllib.parse import urlparse
|
|
533
|
+
proxy_port = int(config.get("proxy_port", 7890))
|
|
534
|
+
api_port = urlparse(api).port or 9090
|
|
525
535
|
dns_hijack = mode in ("tun", "mixed")
|
|
526
|
-
check_ports = [(53, "dns"), (
|
|
527
|
-
else [(
|
|
536
|
+
check_ports = [(53, "dns"), (proxy_port, "proxy"), (api_port, "api")] if dns_hijack \
|
|
537
|
+
else [(proxy_port, "proxy"), (api_port, "api")]
|
|
528
538
|
ok_ports, fail_ports = [], []
|
|
529
539
|
for port, desc in check_ports:
|
|
530
540
|
if _port_listening(port):
|
|
@@ -710,7 +720,8 @@ def cmd_check(engine, api: str, api_secret: str,
|
|
|
710
720
|
ok, line = _test_tcp(parts[0], int(parts[1]), target.name)
|
|
711
721
|
else:
|
|
712
722
|
ok, line = _test_url(target.url, target.name,
|
|
713
|
-
target.mode, target.timeout
|
|
723
|
+
target.mode, target.timeout,
|
|
724
|
+
proxy_port=proxy_port)
|
|
714
725
|
except Exception as e:
|
|
715
726
|
ok, line = False, f" {RED}✗{NC} {target.name} error: {e}"
|
|
716
727
|
results[idx] = (line, ok)
|
|
@@ -737,7 +748,7 @@ def cmd_check(engine, api: str, api_secret: str,
|
|
|
737
748
|
print(f" {YELLOW}—{NC} 无出口探测项")
|
|
738
749
|
for probe in probes:
|
|
739
750
|
ip = probe_ips.get(probe.name, "")
|
|
740
|
-
geo = _ipgeo(ip, cache_file, api_secret)
|
|
751
|
+
geo = _ipgeo(ip, cache_file, api_secret, proxy_port=proxy_port)
|
|
741
752
|
print(f" {probe.name:<7s}{_fmt_ip(ip, geo)}")
|
|
742
753
|
|
|
743
754
|
# 分流校验:当同时有 proxy 出口和 direct 出口时比较
|
|
@@ -43,6 +43,11 @@ DEFAULTS = {
|
|
|
43
43
|
"api_secret": "", # 必填:Clash API Bearer token
|
|
44
44
|
"config_dir": os.path.join(HOME, ".config"),
|
|
45
45
|
"dns_lock_label": "com.proxyctl.dns-lock",
|
|
46
|
+
# 引擎对外暴露的 HTTP/SOCKS mixed-port(应与 mihomo config 的 port/mixed-port 一致)
|
|
47
|
+
"proxy_port": 7890,
|
|
48
|
+
# 个人附加的 NO_PROXY 项(追加到默认 localhost/私网集合之后)
|
|
49
|
+
# 例: ["corp.example.com", "intranet.local"] 或 "corp.example.com,intranet.local"
|
|
50
|
+
"no_proxy_extra": [],
|
|
46
51
|
}
|
|
47
52
|
|
|
48
53
|
SCRIPTS_DIR = os.path.dirname(os.path.realpath(__file__))
|
|
@@ -514,7 +519,8 @@ def _wait_ready(backend: Backend):
|
|
|
514
519
|
"""
|
|
515
520
|
if IS_MACOS:
|
|
516
521
|
wait_port(53, timeout=10)
|
|
517
|
-
wait_port(
|
|
522
|
+
wait_port(int(backend.config.get("proxy_port", DEFAULTS["proxy_port"])),
|
|
523
|
+
timeout=10)
|
|
518
524
|
time.sleep(3)
|
|
519
525
|
|
|
520
526
|
|
|
@@ -1200,10 +1206,16 @@ def cmd_env(config: dict, unset: bool = False):
|
|
|
1200
1206
|
print(f"unset {var};")
|
|
1201
1207
|
return
|
|
1202
1208
|
|
|
1203
|
-
port =
|
|
1209
|
+
port = int(config.get("proxy_port", DEFAULTS["proxy_port"])) # mixed-port
|
|
1204
1210
|
proxy_http = f"http://127.0.0.1:{port}"
|
|
1205
1211
|
proxy_socks = f"socks5://127.0.0.1:{port}"
|
|
1206
1212
|
no_proxy = "localhost,127.0.0.1,::1,10.0.0.0/8,172.16.0.0/12,192.168.0.0/16"
|
|
1213
|
+
# 用户附加的 NO_PROXY 项(个人域名等);接受 list[str] 或逗号分隔 str
|
|
1214
|
+
extra = config.get("no_proxy_extra") or []
|
|
1215
|
+
if isinstance(extra, str):
|
|
1216
|
+
extra = [s.strip() for s in extra.split(",") if s.strip()]
|
|
1217
|
+
if extra:
|
|
1218
|
+
no_proxy = no_proxy + "," + ",".join(extra)
|
|
1207
1219
|
|
|
1208
1220
|
for var in ("http_proxy", "HTTP_PROXY"):
|
|
1209
1221
|
print(f"export {var}={proxy_http};")
|
|
@@ -1217,7 +1229,7 @@ def cmd_env(config: dict, unset: bool = False):
|
|
|
1217
1229
|
|
|
1218
1230
|
# ── 帮助 ──────────────────────────────────────────────────────────────────────
|
|
1219
1231
|
|
|
1220
|
-
VERSION = "0.1.
|
|
1232
|
+
VERSION = "0.1.5"
|
|
1221
1233
|
|
|
1222
1234
|
def cmd_help(verbose: bool = False):
|
|
1223
1235
|
"""打印帮助信息
|
|
@@ -94,10 +94,17 @@ def _gather_engine(engine) -> dict:
|
|
|
94
94
|
return {"pid": pid, "runs": runs, "daemon_up": daemon_up, "etime": etime}
|
|
95
95
|
|
|
96
96
|
|
|
97
|
-
def _gather_ports(claude_proxy_label: str
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
97
|
+
def _gather_ports(claude_proxy_label: str,
|
|
98
|
+
port_list: list[tuple[int, str]] | None = None) -> dict:
|
|
99
|
+
"""采集端口监听状态。
|
|
100
|
+
|
|
101
|
+
Args:
|
|
102
|
+
port_list: [(port, desc), ...],由调用方根据 config/api_base 解析后传入。
|
|
103
|
+
None 时回退到 [(7890,"proxy"), (9090,"api")] 以兼容旧调用。
|
|
104
|
+
"""
|
|
105
|
+
if port_list is None:
|
|
106
|
+
port_list = [(7890, "proxy"), (9090, "api")]
|
|
107
|
+
ports = [(p, desc, _port_listening(p)) for p, desc in port_list]
|
|
101
108
|
cp_running = False
|
|
102
109
|
cp_pid = ""
|
|
103
110
|
cp_port = False
|
|
@@ -475,10 +482,20 @@ def cmd_status(engine, api: str, api_secret: str,
|
|
|
475
482
|
dns_lock_label = config.get("dns_lock_label", "com.proxyctl.dns-lock")
|
|
476
483
|
claude_proxy_label = config.get("claude_proxy_label", "com.proxyctl.claude-proxy")
|
|
477
484
|
|
|
485
|
+
# 端口列表:proxy 取 config.proxy_port,api 从 api_base URL 解析
|
|
486
|
+
proxy_port = int(config.get("proxy_port", 7890))
|
|
487
|
+
api_port = 9090
|
|
488
|
+
try:
|
|
489
|
+
from urllib.parse import urlparse
|
|
490
|
+
api_port = urlparse(api).port or 9090
|
|
491
|
+
except Exception:
|
|
492
|
+
pass
|
|
493
|
+
port_list = [(proxy_port, "proxy"), (api_port, "api")]
|
|
494
|
+
|
|
478
495
|
# 全部 section 并发采集,拿到一个打印一个
|
|
479
496
|
with ThreadPoolExecutor(max_workers=8) as pool:
|
|
480
497
|
f_engine = pool.submit(_gather_engine, engine)
|
|
481
|
-
f_ports = pool.submit(_gather_ports, claude_proxy_label)
|
|
498
|
+
f_ports = pool.submit(_gather_ports, claude_proxy_label, port_list)
|
|
482
499
|
f_tun = pool.submit(_gather_tun, engine, True)
|
|
483
500
|
f_proxy = pool.submit(_gather_proxy_settings)
|
|
484
501
|
f_dns = pool.submit(_gather_dns, dns_lock_label)
|
|
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
|