Files
Private-Proxy/lite/test/probe_nodes.py
T

152 lines
5.9 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# -*- coding: utf-8 -*-
"""连通性探测:用 test/nodes.json 的真实节点,走 _tls_start 转发逻辑实测。
仅需标准库;不启动 GUI(main 的 GUI 在 __main__ 保护下)。
用法: python3 test/probe_nodes.py [JP US ...] 不带参数测全部
"""
import os, sys, time, json, ssl, socket, urllib.request, urllib.error
from pathlib import Path
HERE = Path(__file__).resolve().parent
LITE = HERE.parent
sys.path.insert(0, str(HERE)) # PyQt5 桩
sys.path.insert(0, str(LITE))
for k in ("http_proxy", "https_proxy", "HTTP_PROXY", "HTTPS_PROXY", "all_proxy", "ALL_PROXY"):
os.environ.pop(k, None)
# 不能设 NO_PROXY=*:urllib 会把 127.0.0.1 也当直连,显式 ProxyHandler 被绕过
os.environ["NO_PROXY"] = ""
import main as pp
NODES = json.loads((HERE / "nodes.json").read_text("utf-8"))
want = [a.upper() for a in sys.argv[1:]]
if want:
NODES = [n for n in NODES if str(n.get("country", "")).upper() in want]
TIMEOUT = 12
def free_port():
s = socket.socket(); s.bind(("127.0.0.1", 0))
p = s.getsockname()[1]; s.close(); return p
def http_method(node):
return (node.get("methods") or {}).get("http") or {}
def as_flat_node(node):
"""把 nodes.json 的 methods.http 结构摊平成代码 _node_val() 期望的形状。
仅用于探测:暴露「代码读不到真实节点字段」这一问题。"""
m = http_method(node)
return dict(node, port=int(m.get("port")), username=m.get("username", ""),
password=m.get("password", ""))
def probe_connect(host, port, sni=None, connect_timeout=8):
"""直连探测:DNS -> TCP -> TLS -> 发 CONNECT 127.0.0.1:9 看上游代理响应。"""
res = {"dns": None, "tcp": None, "tls": None, "proxy": None, "err": None}
t0 = time.time()
try:
infos = socket.getaddrinfo(host, port, socket.AF_UNSPEC, socket.SOCK_STREAM)
res["dns"] = sorted({i[4][0] for i in infos})
except Exception as e:
res["err"] = f"DNS: {e}"
return res
addr = res["dns"][0]
af = socket.AF_INET6 if ":" in addr else socket.AF_INET
try:
s = socket.socket(af, socket.SOCK_STREAM)
s.settimeout(connect_timeout)
s.connect((addr, port))
res["tcp"] = round(time.time() - t0, 2)
except Exception as e:
res["err"] = f"TCP: {e}"
return res
try:
ctx = ssl.create_default_context()
s = ctx.wrap_socket(s, server_hostname=sni or host)
res["tls"] = s.version()
except Exception as e:
res["err"] = f"TLS: {e}"
try: s.close()
except Exception: pass
return res
try:
s.sendall(b"CONNECT 127.0.0.1:9 HTTP/1.1\r\nHost: 127.0.0.1:9\r\n\r\n")
res["proxy"] = s.recv(200).split(b"\r\n", 1)[0].decode("latin-1", "replace")
except Exception as e:
res["err"] = f"CONNECT: {e}"
try: s.close()
except Exception: pass
return res
def probe_via_forwarder(node):
"""真实转发器链路:_tls_start -> 经 127.0.0.1 代理请求,取出口 IP 与归属地。"""
m = http_method(node)
if not m:
return "(该节点无 http 方法,跳过)"
old_node, old_port = pp._current_node, pp._TLS_LOCAL_PORT
port = free_port()
try:
# _proxy_auth_basic() 内部走 _current_node(),固定成本次待测节点
pp._current_node = lambda: node
pp._TLS_LOCAL_PORT = port
pp._tls_stop()
pp._tls_start(node["domain"], int(m["port"]), use_tls=True,
bypass_lan=False, smart=False, quiet=True)
ph = urllib.request.ProxyHandler({"http": f"127.0.0.1:{port}",
"https": f"127.0.0.1:{port}"})
op = urllib.request.build_opener(ph)
t0 = time.time()
# 明文 HTTP 代理(走 GET 绝对 URI)与 CONNECT 隧道各测一条
with op.open("http://ip-api.com/json/?fields=query,country,regionName,isp",
timeout=TIMEOUT) as r:
info = json.loads(r.read(512).decode("utf-8", "replace"))
with op.open("https://ifconfig.me/ip", timeout=TIMEOUT) as r:
https_ip = r.read(64).decode().strip()
dt = round(time.time() - t0, 2)
return (f"OK {dt}s 出口IP={info.get('query')} "
f"[{info.get('country')} {info.get('regionName')} / {info.get('isp')}] "
f"HTTPS隧道出口={https_ip}")
except urllib.error.HTTPError as e:
return f"HTTP {e.code}: {e.read(120).decode('utf-8', 'replace').strip()[:100]}"
except Exception as e:
return f"失败: {type(e).__name__}: {e}"
finally:
pp._tls_stop()
pp._current_node, pp._TLS_LOCAL_PORT = old_node, old_port
print(f"节点数: {len(NODES)}")
n0 = NODES[0]
print(f"代码 _endpoint_domain()={pp._endpoint_domain()!r} "
f"_endpoint_port()={pp._endpoint_port()!r} _endpoint_country()={pp._endpoint_country()!r}")
print(f"代码 _proxy_auth_basic()={pp._proxy_auth_basic()!r}")
print(f"nodes.json[0] 顶层键 = {list(n0.keys())}")
print(f"nodes.json[0] domain={n0.get('domain')!r} methods.http.port="
f"{http_method(n0).get('port')!r}")
print()
for n in NODES:
c = n.get("country", "?")
m = http_method(n)
print("=" * 78)
print(f"[{c}] {n.get('domain')}:{m.get('port')} provider={n.get('provider')} "
f"traffic={n.get('traffic')} level={n.get('available_level')}")
if not m:
print(" 该节点无 http 方法,跳过")
continue
r = probe_connect(n["domain"], int(m["port"]))
print(f" DNS : {r['dns']}")
print(f" TCP : {r['tcp']}s" if r["tcp"] is not None else " TCP : -")
print(f" TLS : {r['tls']}" if r.get("tls") else " TLS : -")
print(f" CONNECT: {r['proxy']}" if r.get("proxy") else " CONNECT: -")
if r.get("err"):
print(f" 错误 : {r['err'].splitlines()[0][:200]}")
print(f" 经转发器: {probe_via_forwarder(n)}")
print(f" 经转发器(摊平节点): {probe_via_forwarder(as_flat_node(n))}")
print()