Files
ACL4SSR/generate_github_rules.py
ZLX 70fdf5d1f6 fix(clash): 补回 DOMAIN-KEYWORD,github,收拢漏到海外访问的域名
30 条 DOMAIN-SUFFIX 覆盖不到的含 github 域名(githubmemory.com、
github.global.ssl.fastly.net、各类 github.* 镜像站…)会一路穿到
ProxyGFWlist 的 DOMAIN-KEYWORD,github,落进 🌍 海外访问,造成 GitHub
流量被劈成两个组。

GitHub.list 补回 DOMAIN-KEYWORD,github 兜底;它误伤的两条在 ini 里于
🐙 Github ruleset 之前提前豁免:

  kgithub.com             -> DIRECT  (国内镜像,原属 ChinaOnly.list)
  collector.githubapp.com -> 💩 广告 (遥测,原属 BanProgramAD.list,
                                       建组时被 githubapp.com 抢走,此处修回)

本地按 ini 顺序重放 19837 条规则校验:全部 GitHub 域名首个命中均为
🐙 Github,两条例外保持原分组,AI 域名不受影响。

via [HAPI](https://hapi.run)

Co-Authored-By: HAPI <noreply@hapi.run>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GNghjXGrjt64G32W7t6Bhu
2026-09-18 07:53:20 +08:00

152 lines
5.1 KiB
Python

#!/usr/bin/env python3
"""
从 GitHub 官方 meta API (https://api.github.com/meta) 拉取 IP 段,
生成/更新 Rules/GitHub.list 中的 IP-CIDR 规则块。
域名规则由人工维护在标记块之外, 本脚本只重写标记块内的 IP 规则,
不会覆盖手工补充的域名条目。
"""
import ipaddress
import json
import urllib.request
from pathlib import Path
META_URL = "https://api.github.com/meta"
OUTPUT_FILE = Path("Rules/GitHub.list")
BEGIN_MARK = "# === BEGIN AUTO-GENERATED IP RULES (generate_github_rules.py) ==="
END_MARK = "# === END AUTO-GENERATED IP RULES ==="
# 只取这几个服务的网段。
# 刻意排除:
# actions / actions_macos / codespaces —— 是 Azure 大段(actions 近 7000 条),
# 会把大量非 GitHub 流量误判进代理组
# copilot —— Copilot 域名已归本组(见 ini 规则顺序), 但其 IP 段与 AI 服务商重叠,
# 按 IP 兜底易误伤, 域名规则已足够
# hooks / importer / github_enterprise_importer —— 入站或企业迁移用, 客户端不需要
INCLUDED_KEYS = ["git", "web", "api", "packages", "pages"]
# 域名骨架, 仅在 Rules/GitHub.list 不存在时写入
DEFAULT_DOMAIN_SECTION = """\
# GitHub 全站规则
# 域名部分基于 https://github.com/blackmatrix7/ios_rule_script rule/Clash/GitHub/GitHub.list
# 本地改动:
# - 保留 DOMAIN-KEYWORD,github —— 兜住下面 30 条 suffix 漏掉的一切含 github 的域名
# 被它误伤的两条已在 ini 里于本 ruleset 之前提前豁免:
# kgithub.com -> DIRECT / collector.githubapp.com -> 广告
# - githubcopilot.com / githubnext.com 归本组: ruleset 已排在所有 AI ruleset 之前
# IP 部分由 generate_github_rules.py 从 https://api.github.com/meta 生成, 见文件末尾标记块
DOMAIN-KEYWORD,github
DOMAIN-SUFFIX,atom.io
DOMAIN-SUFFIX,dependabot.com
DOMAIN-SUFFIX,ghcr.io
DOMAIN-SUFFIX,git.io
DOMAIN-SUFFIX,github-atom-io-herokuapp-com.freetls.fastly.net
DOMAIN-SUFFIX,github-avatars.oss-cn-hongkong.aliyuncs.com
DOMAIN-SUFFIX,github-cloud.s3.amazonaws.com
DOMAIN-SUFFIX,github.blog
DOMAIN-SUFFIX,github.com
DOMAIN-SUFFIX,github.community
DOMAIN-SUFFIX,github.dev
DOMAIN-SUFFIX,github.io
DOMAIN-SUFFIX,githubapp.com
DOMAIN-SUFFIX,githubassets.com
DOMAIN-SUFFIX,githubcopilot.com
DOMAIN-SUFFIX,githubhackathon.com
DOMAIN-SUFFIX,githubnext.com
DOMAIN-SUFFIX,githubpreview.dev
DOMAIN-SUFFIX,githubstatus.com
DOMAIN-SUFFIX,githubuniverse.com
DOMAIN-SUFFIX,githubusercontent.com
DOMAIN-SUFFIX,myoctocat.com
DOMAIN-SUFFIX,npm.community
DOMAIN-SUFFIX,npmjs.com
DOMAIN-SUFFIX,npmjs.org
DOMAIN-SUFFIX,opensource.guide
DOMAIN-SUFFIX,rawgit.com
DOMAIN-SUFFIX,rawgithub.com
DOMAIN-SUFFIX,repo.new
DOMAIN-SUFFIX,thegithubshop.com
"""
def fetch_meta():
"""拉取 GitHub meta API"""
req = urllib.request.Request(META_URL, headers={"User-Agent": "curl/8"})
with urllib.request.urlopen(req, timeout=30) as resp:
return json.load(resp)
def extract_networks(meta):
"""按 INCLUDED_KEYS 取网段并去重, IPv4 / IPv6 分开返回"""
v4, v6 = set(), set()
for key in INCLUDED_KEYS:
for cidr in meta.get(key, []):
try:
net = ipaddress.ip_network(cidr, strict=False)
except ValueError:
print(f"跳过无法解析的网段: {key} -> {cidr}")
continue
(v4 if net.version == 4 else v6).add(net)
# collapse_addresses 会合并相邻/包含关系的网段, git/web/api 高度重叠
return (
sorted(ipaddress.collapse_addresses(v4)),
sorted(ipaddress.collapse_addresses(v6)),
)
def build_ip_block(v4, v6):
"""生成标记块内容。no-resolve 必须保留: 没有它, 每条 IP 规则都会触发一次
DNS 解析, 拖慢整条规则链, 且这些规则的用途只是兜住已经是 IP 的连接
(git@github.com 的 SSH 推拉在 redir-host 模式下嗅探不到域名)。"""
lines = [
BEGIN_MARK,
f"# 来源: {META_URL}{' / '.join(INCLUDED_KEYS)} 字段",
"# 请勿手工编辑本块, 运行 python3 generate_github_rules.py 重新生成",
"",
]
lines += [f"IP-CIDR,{net},no-resolve" for net in v4]
if v6:
lines.append("")
lines += [f"IP-CIDR6,{net},no-resolve" for net in v6]
lines += ["", END_MARK]
return "\n".join(lines) + "\n"
def write_rules(ip_block, output_file):
"""保留域名部分, 只替换标记块"""
if output_file.exists():
text = output_file.read_text(encoding="utf-8")
else:
text = DEFAULT_DOMAIN_SECTION
if BEGIN_MARK in text and END_MARK in text:
head = text.split(BEGIN_MARK)[0]
tail = text.split(END_MARK, 1)[1]
text = head + ip_block + tail.lstrip("\n")
else:
text = text.rstrip("\n") + "\n\n" + ip_block
output_file.parent.mkdir(exist_ok=True)
output_file.write_text(text, encoding="utf-8")
def main():
meta = fetch_meta()
v4, v6 = extract_networks(meta)
print(f"合并后 IPv4 网段 {len(v4)} 条, IPv6 网段 {len(v6)}")
write_rules(build_ip_block(v4, v6), OUTPUT_FILE)
print(f"已更新 {OUTPUT_FILE}")
if __name__ == "__main__":
main()