Files
Su YijunandClaude Opus 5.5 2585d15d53 feat: 发布 0.6.2,通知渠道照搬 EasySub、使用场景改为全局设置、识别顺序调整(debug12)
- 通知:14 种渠道与 EasySub 字段 / 实现一致(Telegram 代理与 getMe/getUpdates、飞书应用机器人、
  QQ 机器人、企业微信自建应用 + API 代理 + 检查应用、Webhook 超时与重试),旧配置自动迁移
- 场景:家庭 / 企业,企业称“员工 / 部门”;在 系统设置 → 场景与网络 全局切换,本地缓存避免菜单闪错
- 识别:国内域名列表先于官方 IP 段;IP 段重叠按最长前缀匹配;自动更新开关覆盖 ASN 与补充库

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-24 01:21:45 +08:00

109 lines
5.3 KiB
Python

"""debug12:补充识别库(国内域名列表 + 官方 IP 段)——只做兜底,不让任何已有识别变差。"""
import json
import pytest
from app.classify import Classifier
from app.classify.sources import sources as src
@pytest.fixture
def loaded_sources(tmp_path, monkeypatch):
"""用小份样本数据驱动 sources(不访问网络):国内域名 + Google/GitHub/国内 IP 段。"""
(tmp_path / "china-domains.conf").write_text(
"# 样本\nserver=/example-test.cn/114.114.114.114\nserver=/gov-example.gov.cn/114\nbad line\n", encoding="utf-8")
(tmp_path / "ip-google.json").write_text(json.dumps(
{"prefixes": [{"ipv4Prefix": "192.0.2.0/24"}, {"ipv6Prefix": "2001:db8::/32"}]}), encoding="utf-8")
(tmp_path / "ip-github.json").write_text(json.dumps(
{"git": ["203.0.113.0/25"], "web": ["203.0.113.128/25"]}), encoding="utf-8")
(tmp_path / "ip-cn.txt").write_text("198.51.100.0/24\n# comment\n", encoding="utf-8")
monkeypatch.setenv("DATA_DIR", str(tmp_path))
monkeypatch.setattr("app.classify.sources._enabled", lambda sid: True)
src.reset()
src.load()
yield src
src.reset()
def test_china_list_fallback(loaded_sources):
"""域名规则没认出、但在国内域名列表里 → 国内网站;子域名同样命中;列表外仍是通用 HTTPS。"""
c = Classifier()
assert c.classify("93.184.216.34", 6, 443, "www.example-test.cn", None) == "cn_web"
assert c.classify("93.184.216.34", 6, 443, "deep.sub.example-test.cn", None) == "cn_web"
assert c.classify("93.184.216.34", 6, 443, "not-in-list.example.org", None) == "https"
# 已有规则的应用不被兜底覆盖
assert c.classify("93.184.216.34", 6, 443, "www.bilibili.com", None) == "bilibili"
def test_official_ip_fallback(loaded_sources):
"""没有域名的流量按官方 IP 段认出归属(测试里 ASN 库是关的,正好只验证这一层)。"""
c = Classifier()
assert c.classify("192.0.2.1", 6, 443, None, None) == "google"
assert c.classify("203.0.113.10", 6, 443, None, None) == "github"
assert c.classify("198.51.100.7", 6, 443, None, None) == "cn_net"
assert c.classify("198.51.100.7", 17, 443, None, None) == "cn_net"
# 官方 IP 段认出的也在库外:测试段外的 IP 仍走通用端口
assert c.classify("45.155.99.10", 6, 443, None, None) == "https"
# 域名优先于 IP:IP 在 Google 段里但域名是别的应用
assert c.classify("192.0.2.1", 6, 443, "store.steampowered.com", None) == "steam"
# Apple 静态段(IANA 注册)
assert c.classify("17.0.0.1", 6, 443, None, None) == "apple"
def test_sources_counted_as_identified(loaded_sources):
"""国内网站 / 国内网络不计入「未识别」,识别率计算把它们算已识别。"""
from app.classify import UNIDENTIFIED_CATEGORIES
from app import stats
c = Classifier()
for app_id, ip, qname in (("cn_web", "93.184.216.34", "www.example-test.cn"), ("cn_net", "198.51.100.7", None)):
hit = c.classify(ip, 6, 443, qname, None)
assert c.app_info(hit).category not in UNIDENTIFIED_CATEGORIES
assert c.app_info(hit).name_en
apps = [{**stats.app_meta(a), "total": 1} for a in ("cn_web", "cn_net")]
assert stats.identified_ratio(apps) == 1.0
def test_sources_rows_for_library_panel(loaded_sources):
rows = {r["id"]: r for r in src.rows()}
assert rows["cnlist"]["domains"] == 2
assert rows["google"]["ranges"] == 2
assert rows["github"]["ranges"] == 2
assert rows["cn"]["ranges"] == 1
assert all(r["license"] and r["url"] for r in rows.values())
def test_domain_fallback_before_ip_fallback(loaded_sources):
"""有域名时先按域名兜底:国内网站托管在 Google/国内段的 IP 上,仍归「国内网站」而不是 IP 所属的 CDN。"""
c = Classifier()
assert c.classify("192.0.2.1", 6, 443, "www.example-test.cn", None) == "cn_web"
assert c.classify("198.51.100.7", 6, 443, "www.example-test.cn", None) == "cn_web"
# 域名不在任何库里时才按 IP
assert c.classify("192.0.2.1", 6, 443, "not-in-list.example.org", None) == "google"
def test_overlapping_ranges_longest_prefix():
"""IP 段重叠时按最长前缀:小段赢;小段之外仍落在大段里(旧实现在这里会漏查返回 None)。"""
from app.classify.sources import _Ranges
r = _Ranges()
r.add("10.0.0.0/8", "cn_net") # 先加入的大段
r.add("10.1.0.0/16", "aws") # 后加入、更具体
r.add("10.1.2.0/24", "github") # 再嵌套一层
r.add("10.200.0.0/16", "cn_net") # 同归属的嵌套,合并后不影响
r.add("2001:db8::/32", "google")
r.add("2001:db8:1::/48", "cloudflare")
r.build()
assert r.lookup("10.1.2.3") == "github"
assert r.lookup("10.1.3.1") == "aws"
assert r.lookup("10.2.0.1") == "cn_net" # 在小段后面、仍属于大段
assert r.lookup("10.255.255.255") == "cn_net"
assert r.lookup("10.0.0.0") == "cn_net"
assert r.lookup("11.0.0.1") is None
assert r.lookup("2001:db8:1::5") == "cloudflare"
assert r.lookup("2001:db8:2::5") == "google"
# 一样大时先加入的赢
r2 = _Ranges()
r2.add("192.0.2.0/24", "google")
r2.add("192.0.2.0/24", "aws")
r2.build()
assert r2.lookup("192.0.2.9") == "google"