修复:
- IPv6 专用节点识别不到:服务端 subscription?method=rrpp_client 吐的是旧扁平格式,ipv6_only 字段被后端丢掉、只在 remark 里留 "(IPv6)"。判定改为三级:ipv6_only 字段 -> remark 含 IPv6 -> 域名解析兜底(解析结果只有 IPv6 无 IPv4) - 可达性探测改用该节点自己的 HTTP 端口(原来固定按 443 探测),IPv6 地址直接取自解析结果,避免端口不对造成误判 - 本机无全局 IPv6 地址时判定不可达(菜单置灰,提示「不可用:本机无 IPv6」),有 IPv6 时只标注不禁用 - 节点可达性判定不再打印日志
This commit is contained in:
+73
-28
@@ -147,10 +147,13 @@ def _save_current_node(domain):
|
|||||||
_settings_save(data)
|
_settings_save(data)
|
||||||
|
|
||||||
# ── IPv6-only 节点可达性(客户端标注用)─────────────────────
|
# ── IPv6-only 节点可达性(客户端标注用)─────────────────────
|
||||||
# nodes.json 里 ipv6_only: true 的节点域名只有 AAAA 记录(无 A 记录),
|
# IPv6-only 节点的域名只有 AAAA 记录(无 A 记录),本机没有 IPv6 出口时连不上。
|
||||||
# 本机没有 IPv6 出口时连不上。判定结果按域名缓存,避免每次开菜单都探测。
|
# 判定三级:ipv6_only 字段 → 服务端 remark 里的 "(IPv6)" 标记 → 域名解析兜底。
|
||||||
|
# 服务端 subscription?method=rrpp_client 只吐旧扁平格式,ipv6_only 字段被丢掉,
|
||||||
|
# 只留 remark 里的 "(IPv6)"(见后端 countries_to_rrpp_client),故第 2 级是现网主路径。
|
||||||
|
|
||||||
_IPV6_OK = {} # domain -> 本机能否连上该节点的 IPv6 地址
|
_IPV6_OK = {} # domain -> 本机能否连上该节点的 IPv6 地址
|
||||||
|
_IPV6_ONLY = {} # domain -> 该域名是否只有 IPv6 地址(解析兜底)
|
||||||
_IPV6_PROBE_TIMEOUT = 4 # 单次 TCP 探测超时(秒)
|
_IPV6_PROBE_TIMEOUT = 4 # 单次 TCP 探测超时(秒)
|
||||||
|
|
||||||
def _local_has_ipv6():
|
def _local_has_ipv6():
|
||||||
@@ -172,50 +175,92 @@ def _local_has_ipv6():
|
|||||||
return False
|
return False
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
def _domain_ipv6_only(domain: str) -> bool:
|
||||||
|
"""域名是否「只有 IPv6 地址」:解析结果里有 IPv6 且没有 IPv4 才算。
|
||||||
|
解析失败按 False(当普通节点处理,不误伤)。结果缓存。"""
|
||||||
|
if domain in _IPV6_ONLY:
|
||||||
|
return _IPV6_ONLY[domain]
|
||||||
|
import ipaddress
|
||||||
|
only = False
|
||||||
|
try:
|
||||||
|
addrs = _resolve_host_ips(domain)
|
||||||
|
if addrs:
|
||||||
|
has6 = any(ip.version == 6 for ip in addrs)
|
||||||
|
has4 = any(ip.version == 4 for ip in addrs)
|
||||||
|
only = has6 and not has4
|
||||||
|
except Exception:
|
||||||
|
only = False
|
||||||
|
_IPV6_ONLY[domain] = only
|
||||||
|
if only:
|
||||||
|
log(f"节点 {domain} 解析结果只有 IPv6 地址,按 IPv6 专用节点处理")
|
||||||
|
return only
|
||||||
|
|
||||||
|
def _node_ipv6_only(node) -> bool:
|
||||||
|
"""节点是否 IPv6 专用。
|
||||||
|
|
||||||
|
三级判定:
|
||||||
|
1. ipv6_only 字段(服务端新格式;rrpp_client 输出里没有)
|
||||||
|
2. remark 里的 "(IPv6)" 标记(rrpp_client 现网唯一的权威线索)
|
||||||
|
3. 域名解析兜底:解析结果只有 IPv6 地址、没有 IPv4
|
||||||
|
"""
|
||||||
|
if not isinstance(node, dict):
|
||||||
|
return False
|
||||||
|
if node.get("ipv6_only"):
|
||||||
|
return True
|
||||||
|
if "ipv6" in str(node.get("remark") or "").lower():
|
||||||
|
return True
|
||||||
|
return _domain_ipv6_only(str(node.get("domain") or "").strip())
|
||||||
|
|
||||||
|
def _node_http_port(node) -> int:
|
||||||
|
"""节点的 HTTP 代理端口(不依赖当前选中节点,供可达性探测用)。"""
|
||||||
|
m = (node.get("methods") or {}).get("http") or {}
|
||||||
|
for v in (m.get("port"), node.get("port")):
|
||||||
|
if v not in (None, ""):
|
||||||
|
try:
|
||||||
|
return int(v)
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
pass
|
||||||
|
return 443
|
||||||
|
|
||||||
def _node_reachable(node):
|
def _node_reachable(node):
|
||||||
"""节点在本机是否可达:普通节点恒为 True;ipv6_only 节点需真的能建立 IPv6 连接。
|
"""节点在本机是否可达:普通节点恒为 True;IPv6 专用节点需真的能建立 IPv6 连接
|
||||||
结果按域名缓存(探测一次)。"""
|
(按该节点自己的 HTTP 端口探测)。结果按域名缓存(探测一次)。"""
|
||||||
if not isinstance(node, dict) or not node.get("ipv6_only"):
|
if not isinstance(node, dict):
|
||||||
return True
|
return True
|
||||||
domain = str(node.get("domain") or "").strip()
|
domain = str(node.get("domain") or "").strip()
|
||||||
|
if not _node_ipv6_only(node):
|
||||||
|
return True
|
||||||
if not domain:
|
if not domain:
|
||||||
return False
|
return False
|
||||||
if domain in _IPV6_OK:
|
if domain in _IPV6_OK:
|
||||||
return _IPV6_OK[domain]
|
return _IPV6_OK[domain]
|
||||||
|
port = _node_http_port(node)
|
||||||
ok = False
|
ok = False
|
||||||
if _local_has_ipv6():
|
addrs = sorted((ip for ip in _resolve_host_ips(domain) if ip.version == 6), key=str)
|
||||||
|
if addrs and _local_has_ipv6():
|
||||||
import socket
|
import socket
|
||||||
try:
|
for ip in addrs:
|
||||||
infos = socket.getaddrinfo(domain, None, socket.AF_INET6, socket.SOCK_STREAM)
|
try:
|
||||||
for info in infos:
|
s = socket.socket(socket.AF_INET6, socket.SOCK_STREAM)
|
||||||
|
s.settimeout(_IPV6_PROBE_TIMEOUT)
|
||||||
|
s.connect((str(ip), port))
|
||||||
|
s.close()
|
||||||
|
ok = True
|
||||||
|
break
|
||||||
|
except Exception:
|
||||||
try:
|
try:
|
||||||
s = socket.socket(socket.AF_INET6, socket.SOCK_STREAM)
|
|
||||||
s.settimeout(_IPV6_PROBE_TIMEOUT)
|
|
||||||
s.connect(info[4][:2])
|
|
||||||
s.close()
|
s.close()
|
||||||
ok = True
|
|
||||||
break
|
|
||||||
except Exception:
|
except Exception:
|
||||||
try:
|
pass
|
||||||
s.close()
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
except Exception:
|
|
||||||
ok = False
|
|
||||||
_IPV6_OK[domain] = ok
|
_IPV6_OK[domain] = ok
|
||||||
log(f"节点 {domain} 为 IPv6 专用:本机 IPv6 "
|
|
||||||
f"{'可用' if ok else '不可用'},{'可正常使用' if ok else '已标注为不可用'}")
|
|
||||||
return ok
|
return ok
|
||||||
|
|
||||||
def _node_ipv6_only(node):
|
|
||||||
"""节点是否标记为 IPv6 专用(可选字段,缺省视为 False)。"""
|
|
||||||
return bool(isinstance(node, dict) and node.get("ipv6_only"))
|
|
||||||
|
|
||||||
def _reload_nodes():
|
def _reload_nodes():
|
||||||
"""强制重新读取 nodes.json(菜单打开时调用,外部改动即时生效)。"""
|
"""强制重新读取 nodes.json(菜单打开时调用,外部改动即时生效)。"""
|
||||||
global _NODES
|
global _NODES
|
||||||
_NODES = None
|
_NODES = None
|
||||||
_IPV6_OK.clear() # 节点列表变了,IPv6 可达性缓存作废
|
_IPV6_OK.clear() # 节点列表变了,IPv6 可达性/专用判定缓存作废
|
||||||
|
_IPV6_ONLY.clear()
|
||||||
return _load_nodes()
|
return _load_nodes()
|
||||||
|
|
||||||
def _node_val(key, default=""):
|
def _node_val(key, default=""):
|
||||||
@@ -2496,7 +2541,7 @@ class MainWindow(QWidget):
|
|||||||
if not reachable:
|
if not reachable:
|
||||||
# 本机无 IPv6 出口:置灰不可选,避免选中后连不上
|
# 本机无 IPv6 出口:置灰不可选,避免选中后连不上
|
||||||
act.setEnabled(False)
|
act.setEnabled(False)
|
||||||
act.setToolTip("该节点仅提供 IPv6 地址,当前网络没有 IPv6 出口,无法连接")
|
act.setToolTip("不可用:本机无 IPv6")
|
||||||
else:
|
else:
|
||||||
if ipv6_only:
|
if ipv6_only:
|
||||||
act.setToolTip("该节点仅提供 IPv6 地址(本机 IPv6 可用)")
|
act.setToolTip("该节点仅提供 IPv6 地址(本机 IPv6 可用)")
|
||||||
|
|||||||
Reference in New Issue
Block a user