Compare commits

...
Author SHA1 Message Date
RoyirinRosmontis 9376e35025 修复:PAC 规则解析与关键域名分流 2026-09-16 17:46:54 +08:00
RRRosmontis 110a526fc6 server_config: 同步伪装页为线上实际版本(md5 4eb73450)
仓库内 fake-site/index.html 为旧版(md5 e0f5cbcb),与四个生产节点实际
服务的版本不一致。已用线上(us 节点)实际抓取版本替换,恢复所有节点
2026-09-11 12:53:56 +08:00
RoyirinRosmontis 9b7b817cb7 更新 README.md 2026-09-11 02:06:47 +08:00
RoyirinRosmontis a39647d46f 更新 README.md 2026-09-11 02:06:21 +08:00
3 changed files with 71 additions and 14 deletions
+3 -1
View File
@@ -1,9 +1,11 @@
# Royirin Rosmontis Private Proxy
Supported by Global Rosmanka Cloud Nexus Inc. —— https://rosmanka.rrrosmontis.icu
## 警告 ## 警告
- 包含 WireGuard 的 common 版本转发器永久不再维护,推荐使用 Lite 版本 - 包含 WireGuard 的 common 版本转发器永久不再维护,推荐使用 Lite 版本
> common 版本现在有许多未修复的 bug (如部分 UI 的显示问题) 、令人难以忍受的代码逻辑 (如 PAC 规则直接写在代码里)和安全性问题 (认证凭据的暴露与证书忽略),而 Lite 版本修复了这些问题,并会收到持续的更新 > common 版本现在有许多未修复的 bug (如部分 UI 的显示问题) 、令人难以忍受的代码逻辑 (如 PAC 规则直接写在代码里)和安全性问题 (认证凭据的暴露与证书忽略),而 Lite 版本修复了这些问题,并会收到持续的更新
- 本程序完全没有 Linux 与 CLI 功能兼容,默认构建为带有图形界面的 Windows .exe - 本程序完全没有 Linux 与 CLI 功能兼容,默认构建为带有图形界面的 Windows .exe
## 如何区分 common 版本与 Lite 版本 ### 如何区分 common 版本与 Lite 版本
- Lite 版本的版本号前四位固定为 9999,后续为从月份开始的构建时间戳,而 common 版本的版本号为包含年份的完整的构建时间戳 - Lite 版本的版本号前四位固定为 9999,后续为从月份开始的构建时间戳,而 common 版本的版本号为包含年份的完整的构建时间戳
## 服务器侧配置手册 (部分内容过时) ## 服务器侧配置手册 (部分内容过时)
+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()
+2 -2
View File
@@ -26,7 +26,7 @@
<span class="code-label">Error code 200</span> <span class="code-label">Error code 200</span>
</h1> </h1>
<div> <div>
Visit <a href="https://example.com" target="_blank" rel="noopener noreferrer">example.com</a> for more information. Visit <a href="https://rrrosmontis.icu" target="_blank" rel="noopener noreferrer">rrrosmontis.icu</a> for more information.
</div> </div>
<div class="mt-3">2026-06-28 05:00:19 UTC</div> <div class="mt-3">2026-06-28 05:00:19 UTC</div>
</header> </header>
@@ -90,7 +90,7 @@
<span class="cf-footer-separator sm:hidden">&bull;</span> <span class="cf-footer-separator sm:hidden">&bull;</span>
</span> </span>
<span class="cf-footer-item sm:block sm:mb-1"><span>Performance &amp; security by</span> <a rel="noopener noreferrer" href="https://example.com" id="brand_link" target="_blank">Example Site</a></span> <span class="cf-footer-item sm:block sm:mb-1"><span>Performance &amp; security by</span> <a rel="noopener noreferrer" href="https://rrrosmontis.icu" id="brand_link" target="_blank">Royirin Rosmontis</a></span>
</p> </p>
</div><!-- /.error-footer --> </div><!-- /.error-footer -->
</div> </div>