修复:PAC 规则解析与关键域名分流

This commit is contained in:
RoyirinRosmontis
2026-09-16 17:46:54 +08:00
parent 110a526fc6
commit 9376e35025
+66 -11
View File
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
import sys, os, subprocess, time, ctypes, threading, functools, base64, bisect import sys, os, subprocess, time, ctypes, threading, functools, base64, bisect, re
from pathlib import Path from pathlib import Path
from PyQt5.QtWidgets import ( from PyQt5.QtWidgets import (
@@ -832,8 +832,17 @@ def _is_private_ip(host: str) -> bool:
_CN_RANGES = None # 合并排序后的 (start, end) 整数段 _CN_RANGES = None # 合并排序后的 (start, end) 整数段
_CN_RANGES_LOADED = False _CN_RANGES_LOADED = False
_FORCE_PROXY_SUFFIXES = None _FORCE_PROXY_SUFFIXES = None
_FORCE_PROXY_EXACT = None
_FORCE_PROXY_REGEXES = None
_FORCE_PROXY_LOADED = False _FORCE_PROXY_LOADED = False
# PAC 数据由 CDN 独立下发;此处保留登录/AI 服务的关键后缀兜底。
# 即使远端表暂时未更新或不可读,这些域名也不能因解析到中国 CDN IP 而直连。
_CORE_FORCE_PROXY_SUFFIXES = frozenset((
"anthropic.com", "chatgpt.com", "claude.ai", "googlecdn.com",
"oaistatic.com", "oaiusercontent.com", "openai.com",
))
def _data_path(name: str): def _data_path(name: str):
"""数据文件路径:只取打包内置目录(config.json 随 exe 内置)。""" """数据文件路径:只取打包内置目录(config.json 随 exe 内置)。"""
p = Path(BASE_DIR) / name p = Path(BASE_DIR) / name
@@ -1318,8 +1327,13 @@ def _load_cn_ranges():
return _CN_RANGES return _CN_RANGES
def _load_force_proxy_suffixes(): def _load_force_proxy_suffixes():
"""加载 pac_domains.json:命中这些域名后缀的连接强制走隧道(优先于 IP 判定)。""" """加载 pac_domains.json 的后缀、精确和正则规则。
global _FORCE_PROXY_SUFFIXES, _FORCE_PROXY_LOADED
domain-list-community 的三种规则语义:普通域名匹配自身及子域;
``full:`` 仅匹配该主机;``regexp:`` 按正则匹配。旧逻辑把后两类
当成带冒号的普通后缀,导致它们永远不命中。
"""
global _FORCE_PROXY_SUFFIXES, _FORCE_PROXY_EXACT, _FORCE_PROXY_REGEXES, _FORCE_PROXY_LOADED
if _FORCE_PROXY_LOADED: if _FORCE_PROXY_LOADED:
return _FORCE_PROXY_SUFFIXES return _FORCE_PROXY_SUFFIXES
_FORCE_PROXY_LOADED = True _FORCE_PROXY_LOADED = True
@@ -1327,14 +1341,51 @@ def _load_force_proxy_suffixes():
import json import json
path = _user_path("pac_domains.json") path = _user_path("pac_domains.json")
if path is None: if path is None:
return _FORCE_PROXY_SUFFIXES # 可选文件,缺失不影响 IP 分流 _FORCE_PROXY_SUFFIXES = sorted(_CORE_FORCE_PROXY_SUFFIXES)
_FORCE_PROXY_EXACT = frozenset()
_FORCE_PROXY_REGEXES = ()
log(f"未找到 pac_domains.json,使用 {len(_FORCE_PROXY_SUFFIXES)} 条关键域名兜底")
return _FORCE_PROXY_SUFFIXES
data = json.loads(Path(path).read_text("utf-8")) data = json.loads(Path(path).read_text("utf-8"))
suffixes = sorted({str(d).strip().lower().lstrip("*.") suffixes = set(_CORE_FORCE_PROXY_SUFFIXES)
for d in (data.get("domains") or []) if str(d).strip()}) exact = set()
regexes = []
invalid_regexes = 0
for raw in data.get("domains") or []:
raw_rule = str(raw).strip()
rule = raw_rule.lower()
if not raw_rule:
continue
if rule.startswith("full:"):
host = rule[len("full:"):].rstrip(".")
if host:
exact.add(host)
continue
if rule.startswith("regexp:"):
# 正则本身可能含大写字符类或大小写敏感字面量;只忽略匹配时的
# 域名大小写,不能在编译前改写 pattern。
pattern = raw_rule[len("regexp:"):]
if not pattern:
invalid_regexes += 1
continue
try:
regexes.append(re.compile(pattern, re.IGNORECASE))
except re.error:
invalid_regexes += 1
continue
suffixes.add(rule.lstrip("*.").rstrip("."))
suffixes.discard("")
_FORCE_PROXY_SUFFIXES = suffixes _FORCE_PROXY_SUFFIXES = suffixes
log(f"已加载强制代理域名: {len(suffixes)} 条 (pac_domains.json)") _FORCE_PROXY_EXACT = frozenset(exact)
_FORCE_PROXY_REGEXES = tuple(regexes)
log(f"已加载强制代理规则: 后缀 {len(suffixes)} 条、精确 {len(exact)} 条、"
f"正则 {len(regexes)} 条 (pac_domains.json)" +
(f",跳过 {invalid_regexes} 条非法正则" if invalid_regexes else ""))
except Exception as e: except Exception as e:
log_red(f"pac_domains.json 加载失败: {e}") log_red(f"pac_domains.json 加载失败: {e}")
_FORCE_PROXY_SUFFIXES = sorted(_CORE_FORCE_PROXY_SUFFIXES)
_FORCE_PROXY_EXACT = frozenset()
_FORCE_PROXY_REGEXES = ()
return _FORCE_PROXY_SUFFIXES return _FORCE_PROXY_SUFFIXES
def _is_private_int(n: int) -> bool: def _is_private_int(n: int) -> bool:
@@ -1386,12 +1437,16 @@ def _is_china_ip(host: str):
@functools.lru_cache(maxsize=8192) @functools.lru_cache(maxsize=8192)
def _is_force_proxy(host: str) -> bool: def _is_force_proxy(host: str) -> bool:
"""pac_domains.json 命中的域名强制走隧道(如 claude.ai:国内可连但显示拒绝页)。""" """PAC 强制规则命中的域名走隧道(后缀 / full / regexp)。"""
suffixes = _load_force_proxy_suffixes() suffixes = _load_force_proxy_suffixes()
if not suffixes: exact = _FORCE_PROXY_EXACT or ()
regexes = _FORCE_PROXY_REGEXES or ()
if not suffixes and not exact and not regexes:
return False return False
h = host.rstrip(".").lower() h = host.rstrip(".").lower()
return any(h == s or h.endswith("." + s) for s in suffixes) return (any(h == s or h.endswith("." + s) for s in suffixes) or
h in exact or
any(pattern.search(h) is not None for pattern in regexes))
def _resolve_host_ips(host: str): def _resolve_host_ips(host: str):
"""解析域名的 IPv4/IPv6 地址集合(字面量 IP 返回自身);解析失败返回空集合。""" """解析域名的 IPv4/IPv6 地址集合(字面量 IP 返回自身);解析失败返回空集合。"""
@@ -3092,4 +3147,4 @@ def main():
os._exit(ret) os._exit(ret)
if __name__ == "__main__": if __name__ == "__main__":
main() main()