diff --git a/core/client.yaml.tmpl b/core/client.yaml.tmpl index 54b4c5b..0bf88e8 100644 --- a/core/client.yaml.tmpl +++ b/core/client.yaml.tmpl @@ -308,69 +308,19 @@ rules: # 国内 DNS 与国内业务使用同一策略组;默认直连,无需等待 CN 地址库命中。 - IP-CIDR,223.5.5.5/32,🇨🇳 国内流量,no-resolve - IP-CIDR,120.53.53.53/32,🇨🇳 国内流量,no-resolve +{APP_RULES} {STUN_PROTOCOL_RULE} # --- [P0] 规则更新 / GitHub raw 走代理,避免大陆网络下规则集刷新失败 --- - DOMAIN-SUFFIX,raw.githubusercontent.com,🤖 AI 隐私出口 - # --- [P1] Claude / Anthropic 及其依赖固定到同一个 IPv4 Xray 出口 --- - # Stash 不接受 Mihomo 的数组型 inline rule-provider payload;静态锚点直接放在 rules 中。 - # 这些依赖规则必须早于动态 AI、广告拦截和公网直连规则。 + # AI 核心域名由 sensitive-services.json 维护,并用于汇总 DNS 策略。 +{SENSITIVE_RULES} - DOMAIN-KEYWORD,stun,🤖 AI 隐私出口 - - DOMAIN-SUFFIX,anthropic.com,🤖 AI 隐私出口 - - DOMAIN-SUFFIX,clau.de,🤖 AI 隐私出口 - - DOMAIN-SUFFIX,claude.ai,🤖 AI 隐私出口 - - DOMAIN-SUFFIX,claude.com,🤖 AI 隐私出口 - - DOMAIN-SUFFIX,claudemcpclient.com,🤖 AI 隐私出口 - - DOMAIN-SUFFIX,claudemcpcontent.com,🤖 AI 隐私出口 - - DOMAIN-SUFFIX,claudeusercontent.com,🤖 AI 隐私出口 - - DOMAIN,servd-anthropic-website.b-cdn.net,🤖 AI 隐私出口 - - DOMAIN,anthropic.com.cdn.cloudflare.net,🤖 AI 隐私出口 - - DOMAIN,anthropic.auth0.com,🤖 AI 隐私出口 - - DOMAIN,anthropic-com.ghost.io,🤖 AI 隐私出口 - - DOMAIN-SUFFIX,sentry.io,🤖 AI 隐私出口 - - DOMAIN-SUFFIX,statsigapi.net,🤖 AI 隐私出口 - - DOMAIN,browser-intake-us5-datadoghq.com,🤖 AI 隐私出口 - - DOMAIN-KEYWORD,datadog,🤖 AI 隐私出口 - - DOMAIN-KEYWORD,sentry,🤖 AI 隐私出口 - - DOMAIN-KEYWORD,sift,🤖 AI 隐私出口 - - DOMAIN-SUFFIX,intercom.io,🤖 AI 隐私出口 - - DOMAIN-SUFFIX,intercomcdn.com,🤖 AI 隐私出口 - - DOMAIN,cdn.usefathom.com,🤖 AI 隐私出口 - IP-CIDR,160.79.104.0/21,🤖 AI 隐私出口,no-resolve - IP-CIDR6,2607:6bc0::/32,🤖 AI 隐私出口,no-resolve - IP-ASN,399358,🤖 AI 隐私出口,no-resolve - GEOSITE,category-ntp,🤖 AI 隐私出口 - DST-PORT,123,🤖 AI 隐私出口 - - DOMAIN-SUFFIX,openai.com,🤖 AI 隐私出口 - - DOMAIN-SUFFIX,chatgpt.com,🤖 AI 隐私出口 - - DOMAIN-SUFFIX,oaistatic.com,🤖 AI 隐私出口 - - DOMAIN-SUFFIX,oaiusercontent.com,🤖 AI 隐私出口 - - DOMAIN-SUFFIX,gemini.google.com,🤖 AI 隐私出口 - - DOMAIN-SUFFIX,aistudio.google.com,🤖 AI 隐私出口 - - DOMAIN-SUFFIX,generativelanguage.googleapis.com,🤖 AI 隐私出口 - - DOMAIN-SUFFIX,notebooklm.google.com,🤖 AI 隐私出口 - - DOMAIN-SUFFIX,perplexity.ai,🤖 AI 隐私出口 - - DOMAIN-SUFFIX,cursor.com,🤖 AI 隐私出口 - - DOMAIN-SUFFIX,cursor.sh,🤖 AI 隐私出口 - - DOMAIN-SUFFIX,chat.com,🤖 AI 隐私出口 - - DOMAIN,challenges.cloudflare.com,🤖 AI 隐私出口 - - DOMAIN,accounts.google.com,🤖 AI 隐私出口 - - DOMAIN,oauth2.googleapis.com,🤖 AI 隐私出口 - # OpenAI 官方网络白名单(2026-09-05):认证、功能开关、上传、支付及 Apple 集成。 - # https://help.openai.com/en/articles/9247338-network-recommendations-for-chatgpt-errors-on-web - - DOMAIN-SUFFIX,ct.sendgrid.net,🤖 AI 隐私出口 - - DOMAIN-SUFFIX,featuregates.org,🤖 AI 隐私出口 - - DOMAIN-SUFFIX,statsig.com,🤖 AI 隐私出口 - - DOMAIN,featureassets.org,🤖 AI 隐私出口 - - DOMAIN,prodregistryv2.org,🤖 AI 隐私出口 - - DOMAIN,cdn.openaimerge.com,🤖 AI 隐私出口 - - DOMAIN,cdn.workos.com,🤖 AI 隐私出口 - - DOMAIN,forwarder.workos.com,🤖 AI 隐私出口 - - DOMAIN,setup.workos.com,🤖 AI 隐私出口 - - DOMAIN,images.workoscdn.com,🤖 AI 隐私出口 - - DOMAIN,workos.imgix.net,🤖 AI 隐私出口 - - DOMAIN,humb.apple.com,🤖 AI 隐私出口 - - DOMAIN,js.stripe.com,🤖 AI 隐私出口 - RULE-SET,ai,🤖 AI 隐私出口 - RULE-SET,google,🌐 代理流量 diff --git a/core/client_aggregate.py b/core/client_aggregate.py index 7f01ffd..521a026 100644 --- a/core/client_aggregate.py +++ b/core/client_aggregate.py @@ -15,6 +15,7 @@ from client_output import write_outputs from client_policy import adapt_config +from sensitive_policy import domain_rules, app_rules from profile_lock import profile_lock from settings import DEVICE, NAME, load_settings, validate @@ -200,9 +201,13 @@ def dns_policy(rules): def compose(profile, device, target, nodes, sensitive): template = (CORE / 'client.yaml.tmpl').read_text() values = {name: '' for name in re.findall(r'\{([A-Z0-9_]+)\}', template)} - revision = hashlib.sha256(template.encode() + Path(__file__).read_bytes()).hexdigest()[:12] + revision = hashlib.sha256(template.encode() + Path(__file__).read_bytes() + + (CORE / 'sensitive_policy.py').read_bytes() + + (CORE / 'sensitive-services.json').read_bytes() + + (CORE / 'client_policy.py').read_bytes()).hexdigest()[:12] values.update(DEVICE=device, PROFILE_OWNER=profile, TARGET_LABEL=target, STRICT_LABEL='false', TEMPLATE_REVISION=revision, + SENSITIVE_RULES=domain_rules(AI), APP_RULES=app_rules(target, device, AI), SERVER_LABEL='Aggregate; per-server credentials remain in source profiles', DNS_FOLLOW_RULE=' follow-rule: true' if target == 'stash' else ' respect-rules: true', STUN_PROTOCOL_RULE=f' - PROTOCOL,STUN,{AI}' if target == 'stash' else '') diff --git a/core/client_policy.py b/core/client_policy.py index 846bf80..75f0157 100644 --- a/core/client_policy.py +++ b/core/client_policy.py @@ -10,6 +10,10 @@ def remove_section(config, start, end): def adapt_config(config, target, strict, ai_nodes): if target == "stash": + # Stash accepts only IP literals for the bootstrap default-nameserver. + config = re.sub(r'(^ default-nameserver:\n)(.*?)(?=^ \S)', + lambda m: m[1] + re.sub(r'https://([0-9.]+)/dns-query', r'\1', m[2]), + config, flags=re.S | re.M) # Stash manages its own TUN and protocol sniffing. config = remove_section(config, "geodata-mode: true\n", "skip-proxy:\n") config = remove_section(config, "tun:\n", "dns:\n") diff --git a/core/gen-clash.py b/core/gen-clash.py index 4284574..82c3fad 100755 --- a/core/gen-clash.py +++ b/core/gen-clash.py @@ -18,6 +18,7 @@ from settings import load_settings, validate from client_output import write_outputs from client_policy import adapt_config +from sensitive_policy import domain_rules, app_rules parser = argparse.ArgumentParser(description=__doc__) parser.add_argument("--check", action="store_true", help="只检查输出是否与当前配置一致,不写文件") @@ -257,6 +258,8 @@ def direct_proxy_blocks(dev_uuid, hy2_password): pathlib.Path(__file__).read_bytes() + TEMPLATE_PATH.read_bytes() + pathlib.Path(__file__).with_name("settings.py").read_bytes() + pathlib.Path(__file__).with_name("client_policy.py").read_bytes() + + pathlib.Path(__file__).with_name("sensitive_policy.py").read_bytes() + + pathlib.Path(__file__).with_name("sensitive-services.json").read_bytes() ).hexdigest()[:12] for dev in devices: uuid = env.get(f"REALITY_UUID_{dev}") @@ -293,6 +296,8 @@ def direct_proxy_blocks(dev_uuid, hy2_password): TARGET_LABEL=CLIENT_TARGET, STRICT_LABEL=str(AI_STRICT_MODE).lower(), TEMPLATE_REVISION=template_revision, + SENSITIVE_RULES=domain_rules("🤖 AI 隐私出口"), + APP_RULES=app_rules(CLIENT_TARGET, dev, "🤖 AI 隐私出口"), DNS_FOLLOW_RULE=" follow-rule: true" if CLIENT_TARGET == "stash" else " respect-rules: true", STUN_PROTOCOL_RULE=" - PROTOCOL,STUN,🤖 AI 隐私出口" if CLIENT_TARGET == "stash" else "", SERVER_LABEL=( diff --git a/core/sensitive-services.json b/core/sensitive-services.json new file mode 100644 index 0000000..83b9bc7 --- /dev/null +++ b/core/sensitive-services.json @@ -0,0 +1,146 @@ +{ + "existing_ai": { + "suffixes": [ + "anthropic.com", + "clau.de", + "claude.ai", + "claude.com", + "claudemcpclient.com", + "claudemcpcontent.com", + "claudeusercontent.com", + "statsigapi.net", + "intercom.io", + "intercomcdn.com", + "openai.com", + "chatgpt.com", + "oaistatic.com", + "oaiusercontent.com", + "gemini.google.com", + "aistudio.google.com", + "generativelanguage.googleapis.com", + "notebooklm.google.com", + "perplexity.ai", + "cursor.com", + "cursor.sh", + "chat.com", + "ct.sendgrid.net", + "featuregates.org", + "statsig.com" + ], + "domains": [ + "servd-anthropic-website.b-cdn.net", + "anthropic.com.cdn.cloudflare.net", + "anthropic.auth0.com", + "anthropic-com.ghost.io", + "browser-intake-us5-datadoghq.com", + "cdn.usefathom.com", + "challenges.cloudflare.com", + "accounts.google.com", + "oauth2.googleapis.com", + "featureassets.org", + "prodregistryv2.org", + "cdn.openaimerge.com", + "cdn.workos.com", + "forwarder.workos.com", + "setup.workos.com", + "images.workoscdn.com", + "workos.imgix.net", + "humb.apple.com", + "js.stripe.com" + ] + }, + "openai_dependencies": { + "suffixes": [], + "domains": [ + "rum.browser-intake-datadoghq.com", + "o207216.ingest.sentry.io", + "o33249.ingest.sentry.io", + "events.statsigapi.net" + ] + }, + "antigravity": { + "suffixes": [ + "antigravity.google", + "antigravity-unleash.goog" + ], + "domains": [ + "antigravity-pa.googleapis.com", + "antigravity.googleapis.com", + "cloudcode-pa.googleapis.com", + "daily-cloudcode-pa.googleapis.com", + "aicode.googleapis.com", + "businessaicode.googleapis.com", + "www.googleapis.com", + "aiplatform.googleapis.com", + "alkalimakersuiteapplets.pa.googleapis.com" + ] + }, + "additional_ai": { + "suffixes": [ + "runwayml.com", + "replicate.com", + "replicate.delivery", + "fal.ai", + "fal.run", + "v0.app", + "windsurf.com", + "windsurf.build", + "codeium.com", + "codeiumdata.com", + "githubcopilot.com", + "grok.com", + "x.ai", + "openrouter.ai", + "huggingface.co" + ], + "domains": [ + "notebooklm-pa.googleapis.com" + ] + }, + "vertex_ai": { + "suffixes": [], + "domains": [ + "global-aiplatform.googleapis.com", + "us-aiplatform.googleapis.com", + "eu-aiplatform.googleapis.com", + "africa-south1-aiplatform.googleapis.com", + "asia-east1-aiplatform.googleapis.com", + "asia-east2-aiplatform.googleapis.com", + "asia-northeast1-aiplatform.googleapis.com", + "asia-northeast2-aiplatform.googleapis.com", + "asia-northeast3-aiplatform.googleapis.com", + "asia-south1-aiplatform.googleapis.com", + "asia-southeast1-aiplatform.googleapis.com", + "asia-southeast2-aiplatform.googleapis.com", + "australia-southeast1-aiplatform.googleapis.com", + "australia-southeast2-aiplatform.googleapis.com", + "europe-central2-aiplatform.googleapis.com", + "europe-north1-aiplatform.googleapis.com", + "europe-southwest1-aiplatform.googleapis.com", + "europe-west1-aiplatform.googleapis.com", + "europe-west2-aiplatform.googleapis.com", + "europe-west3-aiplatform.googleapis.com", + "europe-west4-aiplatform.googleapis.com", + "europe-west6-aiplatform.googleapis.com", + "europe-west8-aiplatform.googleapis.com", + "europe-west9-aiplatform.googleapis.com", + "europe-west12-aiplatform.googleapis.com", + "me-central1-aiplatform.googleapis.com", + "me-central2-aiplatform.googleapis.com", + "me-west1-aiplatform.googleapis.com", + "northamerica-northeast1-aiplatform.googleapis.com", + "northamerica-northeast2-aiplatform.googleapis.com", + "southamerica-east1-aiplatform.googleapis.com", + "southamerica-west1-aiplatform.googleapis.com", + "us-central1-aiplatform.googleapis.com", + "us-east1-aiplatform.googleapis.com", + "us-east4-aiplatform.googleapis.com", + "us-east5-aiplatform.googleapis.com", + "us-south1-aiplatform.googleapis.com", + "us-west1-aiplatform.googleapis.com", + "us-west2-aiplatform.googleapis.com", + "us-west3-aiplatform.googleapis.com", + "us-west4-aiplatform.googleapis.com" + ] + } +} diff --git a/core/sensitive_policy.py b/core/sensitive_policy.py new file mode 100644 index 0000000..bb9d6f1 --- /dev/null +++ b/core/sensitive_policy.py @@ -0,0 +1,33 @@ +"""Curated AI domains shared by single-profile and aggregate routing.""" +import json +from pathlib import Path +import re + +DATA = Path(__file__).with_name('sensitive-services.json') + + +def domain_rules(group): + services = json.loads(DATA.read_text()) + seen = set() + lines = [] + for service in services.values(): + for field, kind in (('domains', 'DOMAIN'), ('suffixes', 'DOMAIN-SUFFIX')): + for domain in service[field]: + if not re.fullmatch(r'[a-z0-9]+(?:[a-z0-9.-]*[a-z0-9])?', domain): + raise ValueError('敏感服务域名格式无效') + if (kind, domain) in seen: + raise ValueError('敏感服务域名重复') + seen.add((kind, domain)) + lines.append(f' - {kind},{domain},{group}') + return '\n'.join(lines) + + +def app_rules(target, device, group): + # App bundle prefix matching is Stash-specific; do not export it to Mihomo. + # iOS ignores process rules, so its configuration relies on domain rules. + if device != 'mac': + return '' + if target == 'stash': + return '\n'.join(f' - PROCESS-NAME,/Applications/{app}.app/,{group}' + for app in ('Antigravity', 'Antigravity IDE')) + return f' - PROCESS-PATH-REGEX,^/Applications/Antigravity( IDE)?[.]app/.*,{group}' diff --git a/docs/aggregate-routing.md b/docs/aggregate-routing.md index 53d5608..13c5bdc 100644 --- a/docs/aggregate-routing.md +++ b/docs/aggregate-routing.md @@ -6,7 +6,7 @@ | 分流入口 | 默认策略 | 可手动调整 | |---|---|---| -| AI / Meta 服务(包括 Facebook、Instagram、WhatsApp 等) | 敏感服务主备 | 手动选择 | +| AI / Meta 服务(包括 Antigravity 应用、Facebook、Instagram、WhatsApp 等) | 敏感服务主备 | 手动选择 | | 海外流量,包括未匹配请求 | 自动测速 | 手动选择 | | 国内流量 | DIRECT | 自动测速、手动选择 | | Apple 基础服务 | DIRECT;Siri 等既有海外规则仍进入海外流量 | 自动测速、手动选择 | @@ -49,7 +49,7 @@ AI 和 Meta 的明确域名/域名集共用敏感服务的加密解析器地址 DNS 分组覆盖静态精确/后缀规则及对应 AI、Meta geosite 集合。域名规则无法推断未知第三方依赖或请求归属;原有 DOMAIN-KEYWORD、IP、协议规则与 DNS 策略也不是逐条等价。新增网站依赖时仍需补精确规则;通配 DNS 规则可能优先于 geosite,见 [Stash DNS 文档](https://stash.wiki/features/dns-server)。 -Meta 的静态域名和远程域名集优先于广告拦截、国内分流和普通兜底;AI 认证、上传、监控等已知依赖继续复用共享模板。汇总仅增补策略,不复制维护另一份完整业务规则。 +Meta 的静态域名和远程域名集优先于广告拦截、国内分流和普通兜底;AI 认证、上传、监控等已知依赖由共享敏感服务清单生成。汇总仅增补策略,不复制维护另一份完整业务规则。 ## 验证 @@ -61,3 +61,5 @@ python3 -m unittest discover -s tests -p 'test_*.py' ``` 真实连通性、Stash 导入后的组选择和设备网络差异,需要在各设备上验证。规则策略类型依据 [Stash 策略组文档](https://stash.wiki/proxy-protocols/proxy-groups)。 + +Antigravity 应用规则、AI 清单来源、Stash 版本要求及已知覆盖限制见 [AI 分流覆盖说明](ai-routing-coverage.md)。 diff --git a/docs/ai-routing-coverage.md b/docs/ai-routing-coverage.md new file mode 100644 index 0000000..6083ac2 --- /dev/null +++ b/docs/ai-routing-coverage.md @@ -0,0 +1,43 @@ +# AI 与 Antigravity 的覆盖边界 + +核对日期:2026-09-07。目标是让已知海外 AI 服务及其必要登录依赖使用敏感线路,同时为 Antigravity 增加应用级分流;不把所有 Google Cloud、AWS 或 Azure 流量都当成 AI。 + +## Antigravity:应用优先,域名补充 + +汇总 Mac 配置在静态局域网规则之后、普通业务和广告规则之前,将以下应用包内的可执行文件送到「AI / Meta 服务」: + +- `/Applications/Antigravity.app/`:本机核对版本 2.12.2,含主程序、Electron 辅助程序与 `Contents/Resources/bin/language_server`。 +- `/Applications/Antigravity IDE.app/`:当前未安装;这是 2.12.2 Hub 安装代码内明确的独立 IDE 安装目标。 + +Stash 使用 `PROCESS-NAME` 的包路径前缀模式,末尾 `/` 必须保留;Mihomo 使用单独生成的 `PROCESS-PATH-REGEX`,不能混用两种语义。默认经过敏感服务主备,排查时把「AI / Meta 服务」切到「手动选择」即可。没有增加外显分组。单服务器配置复用同一批域名与应用规则,但出口仍是其既有「AI 隐私出口」组;本文的手动切换步骤指汇总配置。 + +[Stash Mac 4.2.0 更新记录](https://stash.wiki/en/release-notes/macos)明确新增了末尾 `/` 的进程路径前缀匹配。本机 Stash 为 4.2.0,满足这项规则的版本要求。应用规则依赖客户端能识别连接的本机进程;浏览器登录、应用启动的外部 `curl` / Python / Node、远端 SSH 执行和不在这些包内的工具不继承应用身份。应用移到其他目录后应修改路径。iOS 不支持进程规则,因此只在设备名 `mac` 的配置中输出,其他设备仍依靠域名规则。参见 [Stash 规则类型](https://stash.wiki/rules/rule-types)。 + +应用规则覆盖包内程序发出的公网业务、更新、插件与遥测请求;局域网优先直连。不能由此声称所有 DNS 查询也带有应用身份:应用未知域名仍可能使用普通海外解析器,所以核心 API 和登录域名需要明确 DNS 策略。 + +## 同源的域名与 DNS + +`core/sensitive-services.json` 是人工维护的必要域名清单,供单服务器生成器与汇总生成器共同使用。汇总 DNS 策略从生成后的精确域名和后缀规则推导,避免两张人工列表漂移。例如 `rum.browser-intake-datadoghq.com` 同时进入 AI 业务与敏感 DNS,修复原先只有宽泛关键词命中的缺口。 + +| 服务 | 本轮重点锚点 | 核对来源 | +|---|---|---| +| OpenAI / ChatGPT | API、网页、WebSocket、登录、上传、功能开关和指定 Sentry/Datadog 依赖 | [OpenAI 网络建议](https://help.openai.com/en/articles/9247338-network-recommendations-for-chatgpt-errors-on-web) | +| Claude / Anthropic | API、网页、MCP、下载与内容域名 | [Claude Code 网络配置](https://code.claude.com/docs/en/network-config)及原有静态清单 | +| Antigravity / Google AI | 主站、实验开关、Cloud Code、AI Code、生成接口、Google OAuth、userinfo、Vertex AI | 本机 2.12.2 的 `app.asar`、`language_server`;[Antigravity 企业文档](https://antigravity.google/docs/enterprise/) | +| Vertex AI | 根端点及明确地区的 `*-aiplatform.googleapis.com` 主机 | [Google API 参考](https://cloud.google.com/vertex-ai/docs/reference/rest);新增区域仍需维护 | +| Runway / Replicate / fal | `api.dev.runwayml.com`、`api.replicate.com`、`fal.run` 等 | [Runway API](https://docs.dev.runwayml.com/api.md)、[Replicate HTTP API](https://replicate.com/docs/reference/http/)与[输出文件域名](https://replicate.com/docs/topics/predictions/output-files)、[fal 模型端点](https://docs.fal.ai/model-apis/model-endpoints) | +| Cursor、Windsurf、Copilot、Grok、OpenRouter、Hugging Face、v0 | 产品域名静态锚点与动态 AI 集合 | [MetaCubeX AI 集合快照](https://github.com/MetaCubeX/meta-rules-dat/blob/8964c30fc18a52dfc6761d30c664faaac0c219b1/geo/geosite/category-ai-%21cn.list),并补充 [v0 当前产品域名](https://v0.app/) | + +Antigravity 包内明确包含 `daily-cloudcode-pa.googleapis.com`、`aicode.googleapis.com`、`generativelanguage.googleapis.com`、`oauth2.googleapis.com`、`www.googleapis.com/oauth2/v2/userinfo`、`antigravity-unleash.goog` 等。企业 API、Vertex AI 与导出端点在包内存在,不表示当前个人账号每次都使用。动态集合中的 Antigravity 端点也提升为静态锚点,避免依赖首次规则下载。 + +HTTPS 规则只能按主机分流,不能区分加密 URL 路径。因此 `www.googleapis.com` 的非 AI API、Google 账号登录,以及既有 `challenges.cloudflare.com`、Stripe/Intercom 等共享依赖,也会进入敏感线路。这是保障登录链路的明确取舍;没有将整个 `googleapis.com`、`googleusercontent.com`、`storage.googleapis.com` 或 `www.gstatic.com` 静态归入敏感服务。 + +移除 `datadog`、`sentry`、`sift` 的全局关键词和 `sentry.io` 整站静态规则,改用已确认的具体租户/主机。既有 STUN 与 NTP 隐私规则保留,它们是协议/端口策略,不代表对应域名全部属于 AI。Meta 仍合并在同一个服务入口;Spotify 继续显式 DIRECT,国内 AI 未统一强制出海。 + +## 仍然存在的限制 + +动态 AI MRS 集合与 DNS geosite 分别更新,可能短期不同步;静态清单保证已列出的必要域名,不承诺覆盖所有 AI、第三方登录、插件、自建 API、新增地区或临时 CDN。Azure OpenAI 与 AWS Bedrock 的所有租户/地区尚未穷举,避免为补 AI 而接管整个公有云。出现具体问题时用连接记录核对域名、进程、规则和实际出口。 + +Stash 的 `default-nameserver` 只填写 IP,业务 DNS 保持 DoH。[独立节点域名解析](https://stash.wiki/features/dns-server)的 `proxy-server-nameserver` 官方要求 macOS 4.3 / iOS 3.6 及以上;本机 4.2.0 尚不满足这项已有配置要求,不能宣称独立 bootstrap 已在本机验证。应用分流的 4.2.0 支持与这项 DNS 能力是两件事。 + +自动主备只检查测速地址,不识别 AI 服务的地区限制、账号风控或业务 403。应用规则测试、域名/DNS 回归与 Mihomo 配置检查不能替代实际 Stash 的连接记录验收。 diff --git a/tests/test_aggregate.py b/tests/test_aggregate.py index f6739ac..6c54750 100644 --- a/tests/test_aggregate.py +++ b/tests/test_aggregate.py @@ -2,6 +2,8 @@ import copy import json import os +import ipaddress +import re from pathlib import Path import sys import subprocess @@ -59,6 +61,77 @@ def generate(self, **kwargs): def output(self, device='mac'): return self.root / 'clash-configs' / f'routing-{device}.yaml' + @unittest.skipUnless(yaml, 'PyYAML required') + def test_ai_core_domains_have_matching_sensitive_dns(self): + self.generate() + data = yaml.safe_load(self.output().read_text()) + rules = [r.split(',') for r in data['rules']] + def sensitive(host): + for fields in rules: + if len(fields) < 3 or fields[2] != aggregate.AI: + continue + kind, value = fields[:2] + if ((kind == 'DOMAIN' and host == value) or + (kind == 'DOMAIN-SUFFIX' and (host == value or host.endswith('.' + value)))): + return True + return False + # Product acceptance examples, independent of the generated data file. + required = ('api.openai.com', 'ws.chatgpt.com', 'api.anthropic.com', + 'rum.browser-intake-datadoghq.com', 'o207216.ingest.sentry.io', + 'antigravity.google', 'antigravity-pa.googleapis.com', + 'daily-cloudcode-pa.googleapis.com', 'aicode.googleapis.com', + 'businessaicode.googleapis.com', 'generativelanguage.googleapis.com', + 'accounts.google.com', 'oauth2.googleapis.com', 'www.googleapis.com', + 'antigravity-unleash.goog', 'us-central1-aiplatform.googleapis.com', + 'global-aiplatform.googleapis.com', 'api.dev.runwayml.com', + 'api.replicate.com', 'fal.run', 'v0.app', 'api.githubcopilot.com') + dns = data['dns']['nameserver-policy'] + for host in required: + with self.subTest(host=host): + self.assertTrue(sensitive(host)) + candidates = [(key, value) for key, value in dns.items() if key == host or + (key.startswith('+.') and (host == key[2:] or host.endswith(key[1:])))] + self.assertTrue(candidates, 'missing sensitive DNS policy') + for key, value in candidates: + self.assertEqual(value, ['https://1.1.1.1/dns-query', 'https://1.0.0.1/dns-query']) + for host in ('storage.googleapis.com', 'maps.googleapis.com', 'www.gstatic.com', + 'unrelated.sentry.io', 'datadog-unrelated.example', 'sift-example.com', + 'api.deepseek.com', 'doubao.com', 'api.spotify.com'): + self.assertFalse(sensitive(host), host) + for keyword in ('datadog', 'sentry', 'sift'): + self.assertFalse(any(r[:2] == ['DOMAIN-KEYWORD', keyword] for r in rules)) + + @unittest.skipUnless(yaml, 'PyYAML required') + def test_antigravity_bundle_routes_are_mac_only_and_client_specific(self): + paths = ('/Applications/Antigravity.app/Contents/MacOS/Antigravity', + '/Applications/Antigravity.app/Contents/Resources/bin/language_server', + '/Applications/Antigravity IDE.app/Contents/Frameworks/Helper.app/Contents/MacOS/Helper') + for target in ('stash', 'mihomo'): + self.generate(target=target) + data = yaml.safe_load(self.output().read_text()) + rules = data['rules'] + processes = [r for r in rules if r.startswith('PROCESS-')] + self.assertTrue(processes) + def matches(path): + return any(path.startswith(r.split(',')[1]) if target == 'stash' else + re.fullmatch(r.split(',')[1], path) for r in processes) + for path in paths: + self.assertTrue(matches(path), path) + for path in ('/usr/bin/curl', '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome', + '/Applications/Antigravity.app.backup/Contents/MacOS/Antigravity'): + self.assertFalse(matches(path), path) + for rule in processes: + self.assertEqual(rule.split(',')[2], aggregate.AI) + self.assertLess(rules.index('IP-CIDR,192.168.0.0/16,DIRECT,no-resolve'), rules.index(rule)) + self.assertLess(rules.index(rule), rules.index(f'RULE-SET,ads-lite,{aggregate.ADS}')) + self.assertLess(rules.index(rule), rules.index(f'RULE-SET,cn,{aggregate.CN}')) + iphone = yaml.safe_load(self.output('iphone').read_text()) + self.assertFalse(any(r.startswith('PROCESS-') for r in iphone['rules'])) + if target == 'stash': + for address in data['dns']['default-nameserver']: + ipaddress.ip_address(address) + self.assertFalse(any(r.startswith('PROCESS-PATH-REGEX,') for r in processes)) + def test_sources_are_fresh_devices_isolated_and_outputs_owned(self): self.assertEqual(self.generate(), 0) text = self.output().read_text() diff --git a/tests/test_gen_clash.py b/tests/test_gen_clash.py index fe2b7b6..47e95d0 100644 --- a/tests/test_gen_clash.py +++ b/tests/test_gen_clash.py @@ -116,7 +116,6 @@ def test_generates_one_config_per_device_from_shared_core(self): "claudemcpclient.com", "claudemcpcontent.com", "claudeusercontent.com", - "sentry.io", "statsigapi.net", "intercom.io", "intercomcdn.com", @@ -142,9 +141,9 @@ def test_generates_one_config_per_device_from_shared_core(self): " - DOMAIN,anthropic-com.ghost.io,🤖 AI 隐私出口", " - DOMAIN,browser-intake-us5-datadoghq.com,🤖 AI 隐私出口", " - DOMAIN,cdn.usefathom.com,🤖 AI 隐私出口", - " - DOMAIN-KEYWORD,datadog,🤖 AI 隐私出口", - " - DOMAIN-KEYWORD,sentry,🤖 AI 隐私出口", - " - DOMAIN-KEYWORD,sift,🤖 AI 隐私出口", + " - DOMAIN,rum.browser-intake-datadoghq.com,🤖 AI 隐私出口", + " - DOMAIN,o207216.ingest.sentry.io,🤖 AI 隐私出口", + " - DOMAIN,o33249.ingest.sentry.io,🤖 AI 隐私出口", " - IP-CIDR,160.79.104.0/21,🤖 AI 隐私出口,no-resolve", " - IP-CIDR6,2607:6bc0::/32,🤖 AI 隐私出口,no-resolve", " - IP-ASN,399358,🤖 AI 隐私出口,no-resolve", @@ -166,7 +165,7 @@ def test_generates_one_config_per_device_from_shared_core(self): boundary_index = rules.index(boundary) for rule in ( " - DOMAIN,anthropic.auth0.com,🤖 AI 隐私出口", - " - DOMAIN-KEYWORD,datadog,🤖 AI 隐私出口", + " - DOMAIN,rum.browser-intake-datadoghq.com,🤖 AI 隐私出口", " - IP-CIDR,160.79.104.0/21,🤖 AI 隐私出口,no-resolve", " - GEOSITE,category-ntp,🤖 AI 隐私出口", " - DST-PORT,123,🤖 AI 隐私出口", diff --git a/tests/test_safety_regressions.py b/tests/test_safety_regressions.py index cc981cf..22625ef 100644 --- a/tests/test_safety_regressions.py +++ b/tests/test_safety_regressions.py @@ -105,7 +105,7 @@ def test_default_routes_domestic_direct_without_weakening_ai_priority(self): rules = config.split("\nrules:\n", 1)[1] for boundary in ("RULE-SET,ads-lite,", "DOMAIN-SUFFIX,cn,", "RULE-SET,cn,", "RULE-SET,cn-ip,"): for dependency in ("DOMAIN-SUFFIX,openai.com,", "DOMAIN,anthropic.auth0.com,", - "DOMAIN,cdn.workos.com,", "DOMAIN-KEYWORD,datadog,", "RULE-SET,ai,"): + "DOMAIN,cdn.workos.com,", "DOMAIN,rum.browser-intake-datadoghq.com,", "RULE-SET,ai,"): self.assertLess(rules.index(dependency), rules.index(boundary)) for resolver in ("223.5.5.5", "120.53.53.53"): route = f"IP-CIDR,{resolver}/32,🇨🇳 国内流量,no-resolve" @@ -133,7 +133,7 @@ def test_ai_dependencies_are_preserved_before_ad_blocking(self): self.generate() rules = (self.output / "alpha-mac.yaml").read_text().split("\nrules:\n")[1] ads = rules.index("RULE-SET,ads-lite,") - for dependency in ("DOMAIN-KEYWORD,datadog,", "DOMAIN-KEYWORD,sentry,", "DOMAIN-KEYWORD,sift,", + for dependency in ("DOMAIN,rum.browser-intake-datadoghq.com,", "DOMAIN,o207216.ingest.sentry.io,", "DOMAIN,o33249.ingest.sentry.io,", "DOMAIN,cdn.workos.com,", "DOMAIN,humb.apple.com,", "DOMAIN,js.stripe.com,"): self.assertLess(rules.index(dependency), ads)