ca5d8ee84a
这份清单存在的唯一原因是 GCP 把 CDN 出站单独计价(Carrier Peering,$0.08/GiB 从 第一字节起算、无 200 GiB 免费额度),而消费它的 VPC 拒绝规则 cdn-egress-deny 本身 也是 GCP 资源。所以生成器归 gc-toolkit 更合理。 - 删掉本仓库的 tools/refresh-cdn-lists.sh(功能已合并进 gc-toolkit,避免两份分叉) - 脚本头部与 docs/cdn-list-audit.md 改成指路 gc-toolkit - 清单内容本次无变化:v4 271 条 / v6 193 条,与 gc-toolkit 生成的 canonical 文件一致 迁移后 gc-toolkit 那边多了两件事:生成 canonical 的 cdn-cidrs-v4/v6.txt, 以及 --vpsetup 参数(一次运行同时同步本仓库的内嵌清单与 v6 文件)。
999 lines
40 KiB
Bash
999 lines
40 KiB
Bash
#!/usr/bin/env bash
|
||
#
|
||
# setup_cdn_bypass_tproxy.sh — CDN 绕行透明代理(为 Google Cloud 等对 Cloudflare/Fastly/Akamai
|
||
# 出口被封锁或计费的 VPS 设计)
|
||
#
|
||
# 原理:本机发往 CDN 网段的流量在 nft mangle OUTPUT 被打 mark,策略路由把它送进 lo,
|
||
# 由 xray 的 dokodemo-door (tproxy) 入站接管,经一个"不限流"上游节点出站。
|
||
#
|
||
# 脚本只做四件事(均为幂等、可重复执行):
|
||
# 1. 解析粘贴的 vless:// 节点链接 → 生成【独立完整】的 xray 配置 cdn-bypass.json,
|
||
# 并用专用 systemd unit(xray-cdn-bypass.service)作为【第二个 xray 进程】运行它。
|
||
# —— 不再给主 xray.service 加 -confdir:xray 的 confdir 合并语义有两个坑
|
||
# (① 片段的 outbounds 会被【前插】成默认出站,普通客户端流量被劫走;
|
||
# ② 片段里的 routing 整体【替换】主配置的 routing,所有路由规则与 domainStrategy 被抹掉),
|
||
# 单进程 -confdir 无法表达“只把这一个入站路由到新出站”。
|
||
# 独立实例与主实例互不影响,**不改动用户原有 config.json 一个字节,也不改主 unit**
|
||
# 2. 生成 /etc/nftables/cdn-bypass.nft(打标表 + tproxy 表),并向 nftables.conf 追加 include
|
||
# —— 与 setup_caddy_xray_hysteria.sh 同一约定:运行时只 nft -f 自己的文件,
|
||
# 绝不执行整份 nftables.conf(避免其中的 flush ruleset 清掉主机防火墙)
|
||
# 3. 安装 cdn-bypass-route.service(oneshot):开机补 ip rule/ip route 策略路由
|
||
# —— ip 命令是纯内核运行时状态,必须由 service 持久化
|
||
# 4. 全部装完后实测 https://www.cloudflare.com/cdn-cgi/trace 验证绕行生效
|
||
#
|
||
# 防回环:注入的 vless 出站带 sockopt.mark=255,nft 第一条规则豁免 mark 255 ——
|
||
# 即使上游节点 IP 落在 CDN 段内也不会被重新劫回 xray 自身,因此 nft 里无需再豁免上游 IP。
|
||
#
|
||
# 端口策略:nft 只对 TCP/UDP 53,80,443 打标,其他端口直接直连不抓(省上游流量)。
|
||
# xray 侧无需再过滤 —— nft 已按端口预筛。tproxy 不到的包永远不会出现在 dokodemo。
|
||
# 如需全端口代理,在 nft OUTPUT 链把那条 `"ip daddr @cdn_ranges meta l4proto ... mark set"` 的端口语删掉。
|
||
#
|
||
# 客户端入站流量同样受益:客户端 → 本机主 xray 入站 → direct 出站(本机发出)→ 一样过
|
||
# nft OUTPUT 被筛选打标 → tproxy → bypass 实例 → 上游。即客户端访问 CDN 域名会自动走绕行,
|
||
# 无需在主 config.json 里加任何 routing 规则。前提:主配置的 direct 出站【不得】带 sockopt.mark=255
|
||
# (setup_caddy_xray_hysteria.sh 模板不带;若你手写的 direct 带了 255,客户端 CDN 流量会
|
||
# 被豁免直连——在 GCP 这类机器上就是"本机 curl 通、客户端不通"的典型症状)。
|
||
#
|
||
# 多上游 + 健康检查:支持 N 个上游节点(VLESS_LINKS,换行/空白分隔)。出站 tag 依次为
|
||
# ${UPSTREAM_TAG_PREFIX}1、2、…,xray 用 balancer cdn-pool(random 策略,selector=前缀)在它们之间分流,
|
||
# 并由 observatory 每 30s 探测 https://www.gstatic.com/generate_204,只在健康节点间随机。
|
||
# 注意 fallbackTag 必须非空(固定为第 1 个节点):Xray 的 random/roundRobin 策略只有在 fallbackTag
|
||
# 非空时才会去查 observatory,否则会盲目轮转(见 render_bypass_config 注释)。
|
||
#
|
||
# IPv6:同时生成镜像的 `table ip6`(cdn_bypass_mark / cdn_bypass_tproxy),v6 CDN 段取自数据文件
|
||
# cdn-cidrs-v6.txt(CDN_CIDRS_V6_FILE 可改路径),bypass 实例加监听 [::1],route unit 补 v6 策略路由。
|
||
# 本机没有/禁用 IPv6 时 ENABLE_IPV6=auto 自动退化为只做 IPv4。
|
||
#
|
||
# 用法:
|
||
# sudo VLESS_LINKS='vless://uuid1@host1:443?security=reality&pbk=...&sid=...&sni=...&fp=chrome&flow=xtls-rprx-vision#n1
|
||
# vless://uuid2@host2:443?...#n2' \
|
||
# bash setup_cdn_bypass_tproxy.sh
|
||
# 向后兼容:只设旧变量 VLESS_LINK(单条)等价于只有一个元素的 VLESS_LINKS
|
||
# 两者都不设时会从 /dev/tty 交互读取(每行一条,空行结束;wget|bash 管道场景也能粘贴)
|
||
# sudo ACTION=uninstall bash setup_cdn_bypass_tproxy.sh # 全部卸载
|
||
# DRY_RUN=1 bash setup_cdn_bypass_tproxy.sh # 只渲染到 ./dry-run-output/,不动系统
|
||
#
|
||
# 可选环境变量:TPROXY_PORT(54443) ROUTE_TABLE(100) MARK(1) XRAY_SERVICE(xray/v2ray,默认自动探测)
|
||
# UPSTREAM_TAG_PREFIX(oracle-,上游出站 tag 前缀)
|
||
# ENABLE_IPV6(auto|1|0) CDN_CIDRS_V6_FILE(默认脚本同目录 cdn-cidrs-v6.txt)
|
||
# BYPASS_SERVICE(xray-cdn-bypass,独立实例的 unit 名)
|
||
# EXTRA_EXEMPT_CIDRS("8.8.8.8 119.29.29.29" 这类额外不打标的段,空格分隔)
|
||
#
|
||
# 已知边界:v6 CDN 段目前只收 Cloudflare / Fastly(官方公布);Akamai 的 v6 段尚未收录(见
|
||
# cdn-cidrs-v6.txt 里的 TODO),所以 Akamai 的 IPv6 流量暂不经过本代理。
|
||
|
||
set -euo pipefail
|
||
|
||
# ---------- 可配项 ----------
|
||
ACTION="${ACTION:-install}"
|
||
TPROXY_PORT="${TPROXY_PORT:-54443}"
|
||
ROUTE_TABLE="${ROUTE_TABLE:-100}"
|
||
MARK="${MARK:-1}"
|
||
XRAY_SERVICE="${XRAY_SERVICE:-}"
|
||
BYPASS_SERVICE="${BYPASS_SERVICE:-xray-cdn-bypass}"
|
||
UPSTREAM_TAG_PREFIX="${UPSTREAM_TAG_PREFIX:-oracle-}"
|
||
BALANCER_TAG="cdn-pool"
|
||
ENABLE_IPV6="${ENABLE_IPV6:-auto}"
|
||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||
CDN_CIDRS_V6_FILE="${CDN_CIDRS_V6_FILE:-$SCRIPT_DIR/cdn-cidrs-v6.txt}"
|
||
XRAY_CONFDIR="${XRAY_CONFDIR:-/etc/xray-cdn-bypass}"
|
||
NFT_DIR="${NFT_DIR:-/etc/nftables}"
|
||
NFT_FILE="${NFT_FILE:-$NFT_DIR/cdn-bypass.nft}"
|
||
NFTABLES_CONF="${NFTABLES_CONF:-/etc/nftables.conf}"
|
||
ROUTE_UNIT="${ROUTE_UNIT:-cdn-bypass-route.service}"
|
||
NETWORKD_DROPIN="${NETWORKD_DROPIN:-/etc/systemd/networkd.conf.d/90-cdn-bypass.conf}"
|
||
EXTRA_EXEMPT_CIDRS="${EXTRA_EXEMPT_CIDRS:-}"
|
||
DRY_RUN="${DRY_RUN:-}"
|
||
|
||
# ---------- 输出 ----------
|
||
info() { printf '\033[32m[+]\033[0m %s\n' "$*"; }
|
||
warn() { printf '\033[33m[!]\033[0m %s\n' "$*" >&2; }
|
||
die() { printf '\033[31m[x]\033[0m %s\n' "$*" >&2; exit 1; }
|
||
|
||
[ -n "$DRY_RUN" ] && { DRY_RUN_DIR="./dry-run-output"; mkdir -p "$DRY_RUN_DIR"; info "DRY_RUN:只渲染到 $DRY_RUN_DIR"; }
|
||
[ -z "$DRY_RUN" ] && [ "$(id -u)" -ne 0 ] && die "需要 root 运行"
|
||
|
||
# tag 前缀会被原样写进 JSON 和 balancer selector:只允许安全字符
|
||
[[ "$UPSTREAM_TAG_PREFIX" =~ ^[A-Za-z0-9_.-]+$ ]] || die "UPSTREAM_TAG_PREFIX 只能含字母数字 _ . -,收到:$UPSTREAM_TAG_PREFIX"
|
||
|
||
# 解析 ENABLE_IPV6:auto = DRY_RUN 恒为 1(便于渲染/测试);真实安装时看本机 lo 上 IPv6 是否启用
|
||
resolve_ipv6() {
|
||
case "$ENABLE_IPV6" in
|
||
1|0) ;;
|
||
auto)
|
||
if [ -n "$DRY_RUN" ]; then ENABLE_IPV6=1
|
||
elif [ "$(cat /proc/sys/net/ipv6/conf/lo/disable_ipv6 2>/dev/null || echo 1)" = 0 ]; then ENABLE_IPV6=1
|
||
else ENABLE_IPV6=0; warn "本机 IPv6 未启用,ENABLE_IPV6=auto → 0(只处理 IPv4)"; fi;;
|
||
*) die "ENABLE_IPV6 只支持 auto/1/0";;
|
||
esac
|
||
}
|
||
|
||
# ---------- CDN 网段清单(269 条;抓取日期见 CDN_LIST_AUDIT_DATE) ----------
|
||
#
|
||
# 【怎么更新这份清单】—— 不要手动改,用 gc-toolkit 的 cdn-list 子工具:
|
||
# cd ../gc-toolkit
|
||
# ./cdn-list/refresh.sh --apply --vpsetup ../vpsetup
|
||
# 它会拉三个上游、自己 collapse、交叉校验(把 Akamai 原始 9462 行自己合一遍,
|
||
# 与仓库给的合并版逐条比对,不一致就拒绝落盘),然后同步这里的清单体和 CDN_LIST_AUDIT_DATE。
|
||
# 不放心可以先不加 --apply 只报告差异。
|
||
#
|
||
# 为什么生成器在 gc-toolkit 而不在这里:这份清单存在的唯一原因是 GCP 的 CDN 计费科目,
|
||
# 消费它的 VPC 拒绝规则(cdn-egress-deny)本身就是 GCP 资源。
|
||
#
|
||
# 三个上游:Cloudflare ips-v4 / Fastly public-ip-list / Akamai
|
||
# (SecOps-Institute/Akamai-ASN-and-IPs-List,按 40 个 ASN 拉 BGP,每小时更新)。
|
||
# 注意:仓库的 akamai_ip_list.lst 是【按注册原样】的 9462 行、互相重叠,
|
||
# 不要拿它自己合——同一仓库的 akamai_ip_cidr_blocks.lst 就是它的正确合并结果。
|
||
#
|
||
# 【依据是什么】GCP 判定"是否 CDN"看的是目的地属于哪个网络(CDN Interconnect 对已接入的
|
||
# CDN 厂商自动生效),不公开 IP 清单;Akamai 也不提供公开的边缘网段清单(官方 TechDocs 的
|
||
# CIDR 列表是按客户合同定制的,官方 GitHub 207 个仓库里也没有网段仓库)。所以业界唯一可行的
|
||
# 依据就是按 ASN 查 BGP 宣告(AS20940/AS16625 等),上面那个仓库做的就是这件事,每小时更新。
|
||
# 它【不是官方承诺】,会随时间漂移,所以必须定期刷新——2026-11-08 复核时发现旧快照已过时
|
||
# 1~2 年,有 20 万地址 Akamai 已不再宣告、1.3 万新地址漏绕。
|
||
#
|
||
# 【Linode】ASN 清单里的 AS63949 是 Akamai Connected Cloud(Linode)的客户虚拟机,严格说不是
|
||
# CDN 边缘。2026-11-08 评估过精确挖掉:368 → 381 条(减法切出更多碎片),且需同步改 GCP 拒
|
||
# 规则否则那些站点会被 VPC 直接丢包——净负收益,故保留。若将来 Linode 误绕不可接受再处理。
|
||
#
|
||
# 【与 GCP 侧的关系】VPC 的 egress 拒绝规则(cf-no-cdn / fastly-no-cdn / no-akamai-0 /
|
||
# no-akamai-1,新项目里是 cdn-egress-deny)必须用同一份网段。不一致会导致"漏绕(按 CDN 计费)"
|
||
# 或"误丢包(拒绝规则挡住本应经代理绕出去的流量)"。
|
||
#
|
||
# 免在线获取:被封锁的机器在代理起来之前根本访问不了 cloudflare.com,所以必须内嵌。
|
||
CDN_LIST_AUDIT_DATE=2026-11-08
|
||
read -r -d '' CDN_RANGES <<'RANGES_EOF' || true
|
||
2.16.0.0/13
|
||
23.0.0.0/12
|
||
23.32.0.0/11
|
||
23.64.0.0/14
|
||
23.72.0.0/13
|
||
23.92.16.0/20
|
||
23.192.0.0/11
|
||
23.235.32.0/20
|
||
23.239.0.0/19
|
||
43.249.72.0/22
|
||
43.254.120.0/22
|
||
45.33.0.0/17
|
||
45.56.64.0/18
|
||
45.79.0.0/18
|
||
45.79.64.0/19
|
||
45.79.96.0/20
|
||
45.79.112.0/22
|
||
45.79.116.0/23
|
||
45.79.118.0/24
|
||
45.79.120.0/21
|
||
45.79.128.0/18
|
||
45.79.192.0/19
|
||
45.79.224.0/21
|
||
45.79.234.0/24
|
||
45.79.236.0/22
|
||
45.79.240.0/20
|
||
45.118.132.0/22
|
||
49.231.112.0/24
|
||
49.231.116.0/23
|
||
50.116.0.0/18
|
||
59.151.128.0/18
|
||
60.254.128.0/18
|
||
61.19.12.0/24
|
||
64.86.217.0/24
|
||
64.89.224.0/20
|
||
66.175.208.0/20
|
||
66.198.8.0/24
|
||
66.228.32.0/19
|
||
66.246.75.0/24
|
||
69.22.150.0/23
|
||
69.31.112.0/23
|
||
69.31.118.0/24
|
||
69.31.122.0/24
|
||
69.164.192.0/19
|
||
69.192.0.0/16
|
||
72.14.176.0/20
|
||
72.200.254.0/23
|
||
72.246.0.0/15
|
||
74.121.124.0/22
|
||
74.207.224.0/19
|
||
80.67.64.0/19
|
||
80.85.84.0/22
|
||
81.200.66.0/23
|
||
81.200.69.0/24
|
||
84.53.128.0/18
|
||
85.90.244.0/22
|
||
85.159.208.0/21
|
||
88.80.184.0/21
|
||
88.221.0.0/16
|
||
90.98.0.0/17
|
||
90.98.128.0/18
|
||
92.122.0.0/15
|
||
93.191.168.0/24
|
||
93.191.170.0/24
|
||
95.100.0.0/15
|
||
96.6.0.0/15
|
||
96.16.0.0/15
|
||
96.126.96.0/19
|
||
97.107.128.0/20
|
||
103.3.60.0/22
|
||
103.21.244.0/22
|
||
103.22.200.0/22
|
||
103.29.68.0/22
|
||
103.31.4.0/22
|
||
103.95.84.0/22
|
||
103.238.148.0/22
|
||
103.244.50.0/24
|
||
103.245.222.0/23
|
||
103.245.224.0/24
|
||
104.16.0.0/13
|
||
104.24.0.0/14
|
||
104.64.0.0/10
|
||
104.156.80.0/20
|
||
104.200.16.0/20
|
||
104.237.128.0/19
|
||
108.162.192.0/18
|
||
109.74.192.0/20
|
||
109.207.109.0/24
|
||
109.230.116.0/22
|
||
109.230.127.0/24
|
||
109.237.24.0/22
|
||
115.69.232.0/22
|
||
118.214.0.0/16
|
||
118.215.0.0/17
|
||
118.215.128.0/18
|
||
122.2.208.0/24
|
||
122.252.32.0/19
|
||
122.252.128.0/20
|
||
124.40.52.208/28
|
||
124.106.8.0/24
|
||
124.106.175.0/24
|
||
125.56.191.0/24
|
||
125.56.192.0/18
|
||
125.252.192.0/18
|
||
131.0.72.0/22
|
||
139.144.0.0/18
|
||
139.144.64.0/20
|
||
139.144.96.0/19
|
||
139.144.128.0/21
|
||
139.144.144.0/20
|
||
139.144.160.0/19
|
||
139.144.192.0/18
|
||
139.162.0.0/16
|
||
139.177.176.0/20
|
||
139.177.192.0/20
|
||
140.248.64.0/18
|
||
140.248.128.0/17
|
||
141.101.64.0/18
|
||
143.42.0.0/18
|
||
143.42.64.0/20
|
||
143.42.96.0/19
|
||
143.42.128.0/17
|
||
146.75.0.0/17
|
||
151.101.0.0/16
|
||
151.236.216.0/21
|
||
157.52.64.0/18
|
||
162.158.0.0/15
|
||
162.216.16.0/22
|
||
165.254.0.0/24
|
||
165.254.2.0/24
|
||
165.254.26.128/26
|
||
165.254.40.0/23
|
||
165.254.44.0/23
|
||
165.254.52.0/24
|
||
165.254.58.0/25
|
||
165.254.81.0/24
|
||
165.254.110.0/23
|
||
165.254.119.128/25
|
||
165.254.123.0/24
|
||
165.254.136.0/24
|
||
165.254.142.0/24
|
||
165.254.145.64/26
|
||
165.254.156.0/23
|
||
165.254.203.192/26
|
||
165.254.237.128/25
|
||
165.254.238.128/25
|
||
165.254.247.128/25
|
||
167.82.0.0/17
|
||
167.82.128.0/20
|
||
167.82.160.0/20
|
||
167.82.224.0/20
|
||
168.143.242.0/23
|
||
168.143.254.0/23
|
||
170.187.128.0/23
|
||
170.187.131.0/24
|
||
170.187.132.0/24
|
||
170.187.134.0/23
|
||
170.187.136.0/21
|
||
170.187.144.0/20
|
||
170.187.160.0/19
|
||
170.187.192.0/18
|
||
172.64.0.0/13
|
||
172.104.4.0/22
|
||
172.104.8.0/21
|
||
172.104.16.0/20
|
||
172.104.32.0/19
|
||
172.104.64.0/18
|
||
172.104.128.0/18
|
||
172.104.192.0/21
|
||
172.104.202.0/23
|
||
172.104.205.0/24
|
||
172.104.206.0/23
|
||
172.104.208.0/20
|
||
172.104.224.0/19
|
||
172.105.0.0/18
|
||
172.105.64.0/19
|
||
172.105.112.0/20
|
||
172.105.128.0/21
|
||
172.105.136.0/22
|
||
172.105.140.0/23
|
||
172.105.146.0/23
|
||
172.105.148.0/22
|
||
172.105.152.0/21
|
||
172.105.160.0/19
|
||
172.105.192.0/18
|
||
172.111.64.0/18
|
||
172.224.0.0/12
|
||
173.205.69.0/24
|
||
173.205.76.0/23
|
||
173.222.0.0/15
|
||
173.230.128.0/19
|
||
173.245.48.0/20
|
||
173.255.192.0/18
|
||
176.58.96.0/19
|
||
178.79.128.0/18
|
||
184.24.0.0/13
|
||
184.50.0.0/15
|
||
184.84.0.0/14
|
||
185.3.92.0/22
|
||
185.31.16.0/22
|
||
185.225.250.0/24
|
||
187.210.208.0/24
|
||
188.114.96.0/20
|
||
189.247.167.0/24
|
||
189.247.204.0/22
|
||
189.247.210.0/24
|
||
189.247.212.0/24
|
||
189.247.214.0/24
|
||
189.247.216.0/24
|
||
189.247.220.0/24
|
||
190.93.240.0/20
|
||
190.94.188.0/24
|
||
190.98.153.0/24
|
||
190.98.160.0/24
|
||
192.46.208.0/20
|
||
192.46.224.0/20
|
||
192.53.112.0/20
|
||
192.53.160.0/20
|
||
192.80.13.0/24
|
||
192.81.128.0/21
|
||
192.155.80.0/20
|
||
192.204.5.0/24
|
||
193.108.88.0/21
|
||
193.108.152.0/22
|
||
194.195.112.0/20
|
||
194.195.208.0/20
|
||
194.195.240.0/20
|
||
194.233.160.0/20
|
||
195.245.124.0/22
|
||
197.234.240.0/22
|
||
198.41.128.0/17
|
||
198.58.96.0/19
|
||
198.74.48.0/20
|
||
198.87.182.0/25
|
||
198.173.160.0/23
|
||
199.27.72.0/21
|
||
199.101.28.0/22
|
||
199.119.220.0/22
|
||
199.232.0.0/16
|
||
200.60.190.0/24
|
||
202.138.164.0/22
|
||
202.138.183.0/24
|
||
203.69.138.0/24
|
||
203.69.141.0/24
|
||
203.74.95.0/24
|
||
203.144.1.0/24
|
||
203.198.20.0/24
|
||
204.1.141.0/24
|
||
204.2.139.0/24
|
||
204.2.145.0/24
|
||
204.2.158.0/23
|
||
204.2.162.0/24
|
||
204.2.164.0/23
|
||
204.2.196.0/24
|
||
204.8.48.0/22
|
||
204.10.28.0/22
|
||
204.93.48.0/24
|
||
204.201.160.0/24
|
||
204.237.186.0/23
|
||
204.237.188.0/24
|
||
210.61.248.0/23
|
||
212.71.232.0/21
|
||
212.71.244.0/22
|
||
212.71.248.0/21
|
||
212.111.40.0/22
|
||
213.52.128.0/23
|
||
213.52.130.0/24
|
||
213.168.248.0/22
|
||
213.219.36.0/22
|
||
218.231.251.0/24
|
||
219.76.11.0/24
|
||
RANGES_EOF
|
||
|
||
# ---------- 解析 vless:// 链接(多节点:结果写入按下标的数组,各节点互相独立) ----------
|
||
# 用法:parse_vless_link <link> <下标>。每个节点各自拥有 uuid/host/reality 公钥等,
|
||
# 不假设多条链接共享凭据。
|
||
V_UUID=(); V_HOST=(); V_PORT=(); V_SECURITY=(); V_PBK=(); V_SID=(); V_SNI=()
|
||
V_FP=(); V_FLOW=(); V_NETWORK=(); V_PATH=(); V_HOSTHDR=()
|
||
parse_vless_link() {
|
||
local link="$1" idx="$2"
|
||
[[ "$link" == vless://* ]] || die "只支持 vless:// 链接(reality/tls/tcp/ws),收到:${link:0:20}..."
|
||
link="${link#vless://}"
|
||
V_UUID[idx]="${link%%@*}"; link="${link#*@}"
|
||
local hostport="${link%%\?*}"
|
||
hostport="${hostport%%#*}" # 无 query 时 #name 片段会粘在 hostport 后面
|
||
V_HOST[idx]="${hostport%%:*}"
|
||
V_PORT[idx]="${hostport##*:}"
|
||
local query=""
|
||
[[ "$link" == *\?* ]] && { query="${link#*\?}"; query="${query%%#*}"; }
|
||
V_SECURITY[idx]=""; V_PBK[idx]=""; V_SID[idx]=""; V_SNI[idx]=""; V_FP[idx]="chrome"
|
||
V_FLOW[idx]=""; V_NETWORK[idx]="tcp"; V_PATH[idx]=""; V_HOSTHDR[idx]=""
|
||
local kv k v
|
||
IFS='&' read -ra _pairs <<< "$query"
|
||
for kv in "${_pairs[@]}"; do
|
||
k="${kv%%=*}"; v="${kv#*=}"
|
||
v="${v//%2F//}"; v="${v//%3A/:}"; v="${v//%40/@}"; v="${v//%23/#}" # 常见 URL 转义
|
||
case "$k" in
|
||
security) V_SECURITY[idx]="$v";;
|
||
pbk|publicKey) V_PBK[idx]="$v";;
|
||
sid|shortId) V_SID[idx]="$v";;
|
||
sni) V_SNI[idx]="$v";;
|
||
fp) V_FP[idx]="$v";;
|
||
flow) V_FLOW[idx]="$v";;
|
||
type) V_NETWORK[idx]="$v";;
|
||
path) V_PATH[idx]="$v";;
|
||
host) V_HOSTHDR[idx]="$v";;
|
||
esac
|
||
done
|
||
if [ -z "${V_UUID[idx]}" ] || [ -z "${V_HOST[idx]}" ] || [ -z "${V_PORT[idx]}" ]; then
|
||
die "链接 #$((idx + 1)) 解析失败(缺 uuid/host/port)"
|
||
fi
|
||
if [ "${V_SECURITY[idx]}" = "reality" ] && [ -z "${V_PBK[idx]}" ]; then
|
||
die "链接 #$((idx + 1)) 是 reality 但缺 pbk(publicKey)参数"
|
||
fi
|
||
[ -z "${V_SNI[idx]}" ] && V_SNI[idx]="${V_HOST[idx]}"
|
||
info "节点 ${UPSTREAM_TAG_PREFIX}$((idx + 1)):${V_HOST[idx]}:${V_PORT[idx]} security=${V_SECURITY[idx]:-none} network=${V_NETWORK[idx]} flow=${V_FLOW[idx]:-无}"
|
||
}
|
||
|
||
# 收集链接:VLESS_LINKS(换行/空白分隔,多条)优先;未设则回退旧变量 VLESS_LINK(单条);
|
||
# 都没有则从 /dev/tty 交互读取(每行一条,空行结束)。结果放进数组 LINKS。
|
||
LINKS=()
|
||
collect_links() {
|
||
local raw="${VLESS_LINKS:-}"
|
||
[ -n "$raw" ] || raw="${VLESS_LINK:-}"
|
||
if [ -z "$raw" ] && { : < /dev/tty; } 2>/dev/null; then
|
||
printf '请粘贴 vless:// 节点链接(可多条,每行一条,空行结束):\n' > /dev/tty
|
||
local line
|
||
while IFS= read -r line < /dev/tty; do
|
||
[ -n "$line" ] || break
|
||
raw+="$line"$'\n'
|
||
done
|
||
fi
|
||
# 按任意空白拆成一行一条;不用 word-splitting,避免链接里的 ? * 被 glob 展开
|
||
mapfile -t LINKS < <(printf '%s\n' "$raw" | tr -s '[:space:]' '\n' | awk 'NF')
|
||
[ "${#LINKS[@]}" -ge 1 ] || die "至少需要 1 条节点链接(VLESS_LINKS / VLESS_LINK 环境变量,或交互粘贴)"
|
||
[ "${#LINKS[@]}" -ge 2 ] || warn "只有 1 个上游节点:balancer 合法但没有冗余(建议至少 2 条,健康检查才有节点可切换)"
|
||
}
|
||
|
||
# ---------- 生成单个上游 vless 出站(tag = ${UPSTREAM_TAG_PREFIX}<序号>) ----------
|
||
render_outbound() {
|
||
local i="$1" stream
|
||
case "${V_SECURITY[i]}" in
|
||
reality)
|
||
stream=$(cat <<EOS
|
||
"network": "${V_NETWORK[i]}",
|
||
"security": "reality",
|
||
"realitySettings": {
|
||
"serverName": "${V_SNI[i]}",
|
||
"fingerprint": "${V_FP[i]}",
|
||
"publicKey": "${V_PBK[i]}",
|
||
"shortId": "${V_SID[i]}"
|
||
},
|
||
"sockopt": { "mark": 255 }
|
||
EOS
|
||
);;
|
||
tls)
|
||
stream=$(cat <<EOS
|
||
"network": "${V_NETWORK[i]}",
|
||
"security": "tls",
|
||
"tlsSettings": { "serverName": "${V_SNI[i]}", "fingerprint": "${V_FP[i]}" },
|
||
"sockopt": { "mark": 255 }
|
||
EOS
|
||
);;
|
||
*)
|
||
stream=$(cat <<EOS
|
||
"network": "${V_NETWORK[i]}",
|
||
"sockopt": { "mark": 255 }
|
||
EOS
|
||
);;
|
||
esac
|
||
local flow_field=""
|
||
[ -n "${V_FLOW[i]}" ] && flow_field=",
|
||
\"flow\": \"${V_FLOW[i]}\""
|
||
local ws_field=""
|
||
if [ "${V_NETWORK[i]}" = "ws" ]; then
|
||
ws_field=",
|
||
\"wsSettings\": { \"path\": \"${V_PATH[i]:-/}\", \"headers\": { \"Host\": \"${V_HOSTHDR[i]:-${V_HOST[i]}}\" } }"
|
||
fi
|
||
cat <<EOS
|
||
{
|
||
"tag": "${UPSTREAM_TAG_PREFIX}$((i + 1))",
|
||
"protocol": "vless",
|
||
"settings": {
|
||
"vnext": [
|
||
{
|
||
"address": "${V_HOST[i]}",
|
||
"port": ${V_PORT[i]},
|
||
"users": [
|
||
{
|
||
"id": "${V_UUID[i]}",
|
||
"encryption": "none"$flow_field
|
||
}
|
||
]
|
||
}
|
||
]
|
||
},
|
||
"streamSettings": {
|
||
$stream$ws_field
|
||
}
|
||
}
|
||
EOS
|
||
}
|
||
|
||
# ---------- 生成 bypass 实例的【完整独立】xray 配置 ----------
|
||
# 由独立 unit 以 `xray run -config` 加载(不使用 -confdir,不与主配置合并)。
|
||
#
|
||
# balancer / observatory 的两条硬约束(均已对照 Xray 源码核实):
|
||
# * fallbackTag 必须非空(这里固定为第一个节点)。app/router/strategy_random.go 与 balancing.go 里
|
||
# RandomStrategy / RoundRobinStrategy 的 InjectContext 只在 `len(s.FallbackTag) > 0` 时才注入
|
||
# observatory;不设 fallbackTag 则策略【根本不查询健康状态】,会盲目轮转到已宕的节点。
|
||
# fallbackTag 同时是"全部节点都不健康"时的兜底出站。
|
||
# * selector 用【前缀】${UPSTREAM_TAG_PREFIX}:以后新增的节点只要 tag 以该前缀开头就自动入池;
|
||
# observatory.subjectSelector 用同一前缀,保证入池的节点都被探测。
|
||
render_bypass_config() {
|
||
local n="${#LINKS[@]}" i outbounds="" ob
|
||
for ((i = 0; i < n; i++)); do
|
||
ob="$(render_outbound "$i")"
|
||
outbounds+="${outbounds:+,
|
||
}$ob"
|
||
done
|
||
# 监听地址:127.0.0.1 与 ::1 各一个入站(仅回环,不暴露公网)。
|
||
# 2026-11 在 gcoregon(内核 6.8、xray 26.10.31、bindv6only=0)实测:单个 listen "::" 双栈也能同时
|
||
# 接住 v4/v6 的 TCP+UDP tproxy,但会把 tproxy 端口暴露到所有网卡;127.0.0.1 + ::1 两个入站同样
|
||
# 全部通过且只开回环,故选后者。只有 v4 入站时,v6 打标流量会被 RST(实测 connection refused)。
|
||
local inbound_v6="" intags='"cdn-bypass-in"'
|
||
if [ "$ENABLE_IPV6" = 1 ]; then
|
||
inbound_v6=",
|
||
{
|
||
\"tag\": \"cdn-bypass-in6\",
|
||
\"listen\": \"::1\",
|
||
\"port\": $TPROXY_PORT,
|
||
\"protocol\": \"dokodemo-door\",
|
||
\"settings\": { \"network\": \"tcp,udp\", \"followRedirect\": true },
|
||
\"streamSettings\": { \"sockopt\": { \"tproxy\": \"tproxy\" } }
|
||
}"
|
||
intags='"cdn-bypass-in", "cdn-bypass-in6"'
|
||
fi
|
||
cat <<EOF
|
||
{
|
||
"//": "cdn-bypass: 由 setup_cdn_bypass_tproxy.sh 生成的独立 xray 配置(xray-cdn-bypass.service 以 -config 加载),重复运行会整体覆盖本文件,请勿手改",
|
||
"inbounds": [
|
||
{
|
||
"tag": "cdn-bypass-in",
|
||
"listen": "127.0.0.1",
|
||
"port": $TPROXY_PORT,
|
||
"protocol": "dokodemo-door",
|
||
"settings": { "network": "tcp,udp", "followRedirect": true },
|
||
"streamSettings": { "sockopt": { "tproxy": "tproxy" } }
|
||
}$inbound_v6
|
||
],
|
||
"outbounds": [
|
||
$outbounds
|
||
],
|
||
"observatory": {
|
||
"subjectSelector": ["$UPSTREAM_TAG_PREFIX"],
|
||
"probeURL": "https://www.gstatic.com/generate_204",
|
||
"probeInterval": "30s",
|
||
"enableConcurrency": true
|
||
},
|
||
"routing": {
|
||
"balancers": [
|
||
{
|
||
"tag": "$BALANCER_TAG",
|
||
"selector": ["$UPSTREAM_TAG_PREFIX"],
|
||
"strategy": { "type": "random" },
|
||
"fallbackTag": "${UPSTREAM_TAG_PREFIX}1"
|
||
}
|
||
],
|
||
"rules": [
|
||
{
|
||
"type": "field",
|
||
"inboundTag": [$intags],
|
||
"balancerTag": "$BALANCER_TAG"
|
||
}
|
||
]
|
||
}
|
||
}
|
||
EOF
|
||
}
|
||
|
||
# ---------- 读取 IPv6 CDN 网段清单(数据文件,一行一个 CIDR,# 注释) ----------
|
||
# 缺文件直接报错(不静默降级成"只代理 v4");不想处理 v6 可显式设 ENABLE_IPV6=0。
|
||
load_cdn_v6() {
|
||
[ -f "$CDN_CIDRS_V6_FILE" ] || die "找不到 IPv6 CDN 网段文件:$CDN_CIDRS_V6_FILE(默认放在脚本同目录的 cdn-cidrs-v6.txt;可用 CDN_CIDRS_V6_FILE 指定路径,或设 ENABLE_IPV6=0 跳过 IPv6)"
|
||
local line n=0
|
||
CDN_RANGES_V6=""
|
||
while IFS= read -r line || [ -n "$line" ]; do
|
||
line="${line%%#*}"
|
||
line="${line//[[:space:]]/}"
|
||
[ -n "$line" ] || continue
|
||
[[ "$line" =~ ^[0-9a-fA-F:]+/[0-9]{1,3}$ && "$line" == *:* ]] || die "$CDN_CIDRS_V6_FILE 含非法 IPv6 CIDR:$line"
|
||
CDN_RANGES_V6+="$line"$'\n'
|
||
n=$((n + 1))
|
||
done < "$CDN_CIDRS_V6_FILE"
|
||
[ "$n" -ge 1 ] || die "$CDN_CIDRS_V6_FILE 里没有任何 CIDR(全是注释/空行);不想处理 IPv6 请设 ENABLE_IPV6=0"
|
||
info "已读取 IPv6 CDN 网段 $n 条:$CDN_CIDRS_V6_FILE"
|
||
}
|
||
|
||
# ---------- 生成 nft 文件 ----------
|
||
render_nft() {
|
||
local extra="" extra6=""
|
||
local c
|
||
for c in $EXTRA_EXEMPT_CIDRS; do
|
||
if [[ "$c" == *:* ]]; then extra6+=" ip6 daddr $c accept\n"
|
||
else extra+=" ip daddr $c accept\n"; fi
|
||
done
|
||
# CDN 段组装成 nft 集合(每行 4 段)
|
||
local set_lines
|
||
set_lines=$(echo "$CDN_RANGES" | awk 'NF' | sort -t. -k1,1n -k2,2n -k3,3n -u | \
|
||
awk 'BEGIN{n=0} {a[n++]=$1} END{for(i=0;i<n;i++){printf "%s%s", a[i], (i==n-1?"":((i+1)%4?", ":",\n "))}}')
|
||
cat <<EOF
|
||
# cdn-bypass:CDN 绕行透明代理(由 setup_cdn_bypass_tproxy.sh 生成,重复运行整体覆盖,勿手改)
|
||
table ip cdn_bypass_mark {
|
||
chain output {
|
||
type route hook output priority mangle; policy accept;
|
||
# xray 自身出站(sockopt.mark=255)豁免 —— 防回环的关键
|
||
meta mark 0xff ip protocol { tcp, udp } accept
|
||
# 私网/本机/链路本地豁免
|
||
ip daddr { 127.0.0.0/8, 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16, 169.254.0.0/16, 100.64.0.0/10 } accept
|
||
$(printf '%b' "$extra") # CDN 段 53/80/443 端口打标(DNS、HTTP、HTTPS;QUIC 走 udp/443 自动覆盖)
|
||
ip daddr {
|
||
$set_lines
|
||
} meta l4proto { tcp, udp } th dport { 53, 80, 443 } meta mark set $MARK
|
||
}
|
||
}
|
||
|
||
table ip cdn_bypass_tproxy {
|
||
chain prerouting {
|
||
type filter hook prerouting priority mangle + 1; policy accept;
|
||
meta mark $MARK ip protocol { tcp, udp } tproxy to :$TPROXY_PORT
|
||
}
|
||
}
|
||
EOF
|
||
[ "$ENABLE_IPV6" = 1 ] || return 0
|
||
# IPv6:与 v4 镜像的两张表(独立的 ip6 family 表,不碰任何现有 ip6 filter / rules.v6)
|
||
local set6_lines
|
||
set6_lines=$(echo "$CDN_RANGES_V6" | awk 'NF' | sort -u | \
|
||
awk 'BEGIN{n=0} {a[n++]=$1} END{for(i=0;i<n;i++){printf "%s%s", a[i], (i==n-1?"":((i+1)%4?", ":",\n "))}}')
|
||
cat <<EOF
|
||
|
||
table ip6 cdn_bypass_mark {
|
||
chain output {
|
||
type route hook output priority mangle; policy accept;
|
||
# xray 自身出站(sockopt.mark=255)豁免 —— 防回环
|
||
meta mark 0xff meta l4proto { tcp, udp } accept
|
||
# 本机/链路本地/ULA/组播豁免
|
||
ip6 daddr { ::1/128, fe80::/10, fc00::/7, ff00::/8 } accept
|
||
$(printf '%b' "$extra6") # CDN v6 段 53/80/443 端口打标
|
||
ip6 daddr {
|
||
$set6_lines
|
||
} meta l4proto { tcp, udp } th dport { 53, 80, 443 } meta mark set $MARK
|
||
}
|
||
}
|
||
|
||
table ip6 cdn_bypass_tproxy {
|
||
chain prerouting {
|
||
type filter hook prerouting priority mangle + 1; policy accept;
|
||
# IPv6 写死 [::1]:不带地址的 tproxy to :PORT 内核同样接受,但监听地址由路由推导;
|
||
# 实测 [::1] 才与 inbound 的 listen 确定对应(v4 保持原有不带地址写法不动)。
|
||
meta mark $MARK meta l4proto { tcp, udp } tproxy to [::1]:$TPROXY_PORT
|
||
}
|
||
}
|
||
EOF
|
||
}
|
||
|
||
# ---------- 生成 networkd drop-in ----------
|
||
# 防 systemd-networkd 把本脚本加的 fwmark 策略路由当"外源规则"清掉:
|
||
# networkd 默认 ManageForeignRoutingPolicyRules=yes,它每次重启/重配(如 unattended-upgrade
|
||
# 升级 systemd 触发)都会删掉 ip rule fwmark $MARK —— 而 route unit 是 oneshot,不重启不会补回,
|
||
# 绕行就此静默失效(2026-10-16 gcoregon 实战事故)。文件落盘即对下一次 networkd 重启生效,
|
||
# 安装时无需重启 networkd。
|
||
render_networkd_dropin() {
|
||
cat <<EOF
|
||
[Network]
|
||
# cdn-bypass:不许 networkd 在重启/重配时清掉 $ROUTE_UNIT 加的 fwmark 策略路由
|
||
ManageForeignRoutingPolicyRules=no
|
||
EOF
|
||
}
|
||
|
||
# ---------- 生成策略路由 unit ----------
|
||
render_route_unit() {
|
||
# IPv6 同理需要 fwmark 策略路由 + `local ::/0 dev lo`(2026-11 gcoregon 用 table 199 实测可行)
|
||
local route_v6_start="" route_v6_stop=""
|
||
if [ "$ENABLE_IPV6" = 1 ]; then
|
||
route_v6_start="
|
||
ExecStart=/bin/sh -c 'ip -6 rule add fwmark $MARK lookup $ROUTE_TABLE 2>/dev/null || true; ip -6 route add local ::/0 dev lo table $ROUTE_TABLE 2>/dev/null || true'"
|
||
route_v6_stop="
|
||
ExecStop=/bin/sh -c 'ip -6 rule del fwmark $MARK lookup $ROUTE_TABLE 2>/dev/null || true; ip -6 route del local ::/0 dev lo table $ROUTE_TABLE 2>/dev/null || true'"
|
||
fi
|
||
cat <<EOF
|
||
[Unit]
|
||
Description=CDN bypass tproxy policy routing (fwmark $MARK -> table $ROUTE_TABLE)
|
||
# ip rule/ip route 是纯内核运行时状态,必须由本 unit 在每次开机时补回
|
||
After=network-pre.target
|
||
Wants=network-pre.target
|
||
|
||
[Service]
|
||
Type=oneshot
|
||
RemainAfterExit=yes
|
||
ExecStart=/bin/sh -c 'ip rule add fwmark $MARK lookup $ROUTE_TABLE 2>/dev/null || true; ip route add local 0.0.0.0/0 dev lo table $ROUTE_TABLE 2>/dev/null || true'$route_v6_start
|
||
ExecStop=/bin/sh -c 'ip rule del fwmark $MARK lookup $ROUTE_TABLE 2>/dev/null || true; ip route del local 0.0.0.0/0 dev lo table $ROUTE_TABLE 2>/dev/null || true'$route_v6_stop
|
||
|
||
[Install]
|
||
WantedBy=multi-user.target
|
||
EOF
|
||
}
|
||
|
||
# ---------- 探测 xray ----------
|
||
detect_xray() {
|
||
if [ -z "$XRAY_SERVICE" ]; then
|
||
if systemctl cat xray.service >/dev/null 2>&1; then XRAY_SERVICE=xray
|
||
elif systemctl cat v2ray.service >/dev/null 2>&1; then XRAY_SERVICE=v2ray
|
||
else die "找不到 xray.service / v2ray.service(本脚本要求已装好 xray;bypass 将作为独立实例运行)"; fi
|
||
fi
|
||
XRAY_EXECSTART="$(systemctl show "$XRAY_SERVICE" -p ExecStart --value)"
|
||
[ -n "$XRAY_EXECSTART" ] || die "读不到 $XRAY_SERVICE 的 ExecStart"
|
||
# 新版 systemd 输出 "{ path=/usr/local/bin/xray ; argv[]=... }",老格式直接是路径
|
||
if [[ "$XRAY_EXECSTART" =~ path=([^[:space:]]+) ]]; then
|
||
XRAY_BIN="${BASH_REMATCH[1]}"
|
||
else
|
||
XRAY_BIN="$(echo "$XRAY_EXECSTART" | awk '{print $1}')"
|
||
fi
|
||
[ -x "$XRAY_BIN" ] || die "从 ExecStart 解析不到 xray 二进制路径:$XRAY_EXECSTART"
|
||
# bypass 实例沿用主服务的运行身份(空 = unit 未显式设置 = root)
|
||
XRAY_USER="$(systemctl show "$XRAY_SERVICE" -p User --value 2>/dev/null || true)"
|
||
XRAY_GROUP="$(systemctl show "$XRAY_SERVICE" -p Group --value 2>/dev/null || true)"
|
||
[ -n "$XRAY_USER" ] || XRAY_USER=root
|
||
# Group= 未显式设置时 systemctl 返回空,此时应取该用户的 primary group
|
||
# (官方 unit 常写 User=nobody 而省略 Group=,回退成 root 会给出 gid=0)
|
||
[ -n "$XRAY_GROUP" ] || XRAY_GROUP="$(id -gn "$XRAY_USER" 2>/dev/null || echo root)"
|
||
info "xray 服务:$XRAY_SERVICE ExecStart:$XRAY_EXECSTART 运行身份:$XRAY_USER:$XRAY_GROUP"
|
||
}
|
||
|
||
# ---------- 生成 bypass 独立实例的 systemd unit ----------
|
||
# DRY_RUN 不调用 detect_xray,XRAY_BIN/XRAY_USER/XRAY_GROUP 取占位默认值。
|
||
render_bypass_unit() {
|
||
local main="${XRAY_SERVICE:-xray}" bin="${XRAY_BIN:-/usr/local/bin/xray}"
|
||
local user="${XRAY_USER:-root}" group="${XRAY_GROUP:-root}"
|
||
cat <<EOF
|
||
[Unit]
|
||
Description=CDN bypass tproxy (xray standalone)
|
||
After=network.target nss-lookup.target $main.service
|
||
Wants=network-online.target
|
||
# PartOf 是有意为之:官方 install-release.sh 升级只重启 xray.service,
|
||
# 借此让 bypass 实例随之重启、换上新二进制;而它是单向的 —— 单独重启 bypass 不会碰主服务。
|
||
PartOf=$main.service
|
||
|
||
[Service]
|
||
Type=simple
|
||
User=$user
|
||
Group=$group
|
||
# CAP_NET_ADMIN:IP_TRANSPARENT(tproxy)与 SO_MARK(出站 mark 255)都需要它
|
||
CapabilityBoundingSet=CAP_NET_ADMIN CAP_NET_BIND_SERVICE CAP_NET_RAW
|
||
AmbientCapabilities=CAP_NET_ADMIN CAP_NET_BIND_SERVICE CAP_NET_RAW
|
||
NoNewPrivileges=yes
|
||
ExecStart=$bin run -config $XRAY_CONFDIR/cdn-bypass.json
|
||
Restart=on-failure
|
||
RestartPreventExitStatus=23
|
||
LimitNOFILE=1000000
|
||
|
||
[Install]
|
||
# 同时挂到主服务:PartOf 只传播 stop/restart,【不传播 start】。
|
||
# 没有这一行,traffic-guard 次月 systemctl start 只拉起主服务,bypass 仍是 dead,
|
||
# 而 nft 仍在打标 —— CDN 流量会全部被 TPROXY 到已关闭的端口(黑洞,连 ACME 续期都会挂)。
|
||
WantedBy=multi-user.target $main.service
|
||
EOF
|
||
}
|
||
|
||
# 清理旧版(-confdir drop-in 方案)残留;有残留时返回 0,否则 1。只删文件,不重启。
|
||
remove_legacy_dropins() {
|
||
local f found=1
|
||
for f in "/etc/systemd/system/$XRAY_SERVICE.service.d/10-cdn-bypass-confdir.conf" \
|
||
"/etc/systemd/system/$XRAY_SERVICE.service.d/20-cdn-bypass-confdir.conf"; do
|
||
if [ -e "$f" ]; then rm -f "$f"; found=0; fi
|
||
done
|
||
rmdir "/etc/systemd/system/$XRAY_SERVICE.service.d" 2>/dev/null || true
|
||
return "$found"
|
||
}
|
||
|
||
# ---------- 安装 ----------
|
||
do_install() {
|
||
# 1. 读链接(N 条)并逐条解析成各自独立的节点
|
||
collect_links
|
||
local li
|
||
for li in "${!LINKS[@]}"; do parse_vless_link "${LINKS[li]}" "$li"; done
|
||
resolve_ipv6
|
||
[ "$ENABLE_IPV6" = 1 ] && load_cdn_v6
|
||
|
||
# 2. 渲染
|
||
local xray_json nft_conf route_unit bypass_unit networkd_dropin
|
||
nft_conf="$(render_nft)"
|
||
route_unit="$(render_route_unit)"
|
||
networkd_dropin="$(render_networkd_dropin)"
|
||
|
||
if [ -n "$DRY_RUN" ]; then
|
||
xray_json="$(render_bypass_config)"
|
||
bypass_unit="$(render_bypass_unit)"
|
||
printf '%s\n' "$xray_json" > "$DRY_RUN_DIR/cdn-bypass.json"
|
||
chmod 600 "$DRY_RUN_DIR/cdn-bypass.json"
|
||
printf '%s\n' "$bypass_unit" > "$DRY_RUN_DIR/$BYPASS_SERVICE.service"
|
||
printf '%s\n' "$nft_conf" > "$DRY_RUN_DIR/cdn-bypass.nft"
|
||
printf '%s\n' "$route_unit" > "$DRY_RUN_DIR/$ROUTE_UNIT"
|
||
printf '%s\n' "$networkd_dropin" > "$DRY_RUN_DIR/90-cdn-bypass-networkd.conf"
|
||
printf 'include "%s"\n' "$NFT_FILE" >> "$DRY_RUN_DIR/nftables.conf"
|
||
info "已渲染:cdn-bypass.json / $BYPASS_SERVICE.service / cdn-bypass.nft / $ROUTE_UNIT / networkd drop-in / nftables.conf(include)"
|
||
info "(真实安装时还会移除旧版 {10,20}-cdn-bypass-confdir.conf drop-in,如存在则重启一次主 ${XRAY_SERVICE:-xray};DRY_RUN 不触碰系统)"
|
||
return 0
|
||
fi
|
||
|
||
detect_xray
|
||
xray_json="$(render_bypass_config)"
|
||
bypass_unit="$(render_bypass_unit)"
|
||
|
||
# 3. 先验证独立配置(不过则直接中止,系统一字节未动)
|
||
local tmp_conf
|
||
# 必须带 .json 后缀:xray 靠扩展名判断配置格式,无后缀会报 "Failed to get format of ..."
|
||
tmp_conf="$(mktemp --suffix=.json)"
|
||
printf '%s\n' "$xray_json" > "$tmp_conf"
|
||
if ! "$XRAY_BIN" run -test -config "$tmp_conf" > /dev/null 2>&1; then
|
||
"$XRAY_BIN" run -test -config "$tmp_conf" 2>&1 | tail -5
|
||
rm -f "$tmp_conf"
|
||
die "独立配置自检失败,未改动系统"
|
||
fi
|
||
rm -f "$tmp_conf"
|
||
info "bypass 独立配置自检通过"
|
||
|
||
# 4. 旧版迁移:先移除 -confdir drop-in 并重启主服务【一次】,让主实例立刻摆脱 -confdir、
|
||
# 恢复完整 routing,同时释放 TPROXY 端口;必须发生在启动新 unit 之前。
|
||
if remove_legacy_dropins; then
|
||
systemctl daemon-reload
|
||
systemctl restart "$XRAY_SERVICE"
|
||
info "已移除旧版 -confdir drop-in 并重启 $XRAY_SERVICE(主实例恢复完整 routing)"
|
||
fi
|
||
|
||
# 5. 写配置 + 独立 unit,启动 bypass 实例
|
||
mkdir -p "$XRAY_CONFDIR"
|
||
# 含 VLESS UUID:600,属主为 bypass 实例的运行身份
|
||
( umask 077; printf '%s\n' "$xray_json" > "$XRAY_CONFDIR/cdn-bypass.json" )
|
||
chown "$XRAY_USER:$XRAY_GROUP" "$XRAY_CONFDIR/cdn-bypass.json"
|
||
chmod 600 "$XRAY_CONFDIR/cdn-bypass.json"
|
||
info "已写入 $XRAY_CONFDIR/cdn-bypass.json(600)"
|
||
printf '%s\n' "$bypass_unit" > "/etc/systemd/system/$BYPASS_SERVICE.service"
|
||
systemctl daemon-reload
|
||
systemctl enable --now "$BYPASS_SERVICE"
|
||
systemctl restart "$BYPASS_SERVICE" # 幂等重跑时让新配置生效
|
||
info "$BYPASS_SERVICE 已启动(独立 xray 实例,主 $XRAY_SERVICE 未被改动)"
|
||
|
||
# 6. nft
|
||
mkdir -p "$NFT_DIR"
|
||
printf '%s\n' "$nft_conf" > "$NFT_FILE"
|
||
chmod 644 "$NFT_FILE"
|
||
touch "$NFTABLES_CONF"
|
||
local include_line="include \"$NFT_FILE\""
|
||
if ! grep -qF "$include_line" "$NFTABLES_CONF"; then
|
||
printf '%s\n' "$include_line" >> "$NFTABLES_CONF"
|
||
info "已向 $NFTABLES_CONF 追加 include 行"
|
||
fi
|
||
# 只 enable,绝不 --now/start:nftables.service 若此前未激活,启动会执行
|
||
# /etc/nftables.conf 首行的 `flush ruleset`,瞬间清空主机防火墙(含他人维护的 ip6 filter)。
|
||
systemctl enable nftables
|
||
# 运行时只加载自己的两张表(先删后加,nft -f 是合并语义),绝不执行整份 nftables.conf
|
||
nft delete table ip cdn_bypass_tproxy 2>/dev/null || true
|
||
nft delete table ip cdn_bypass_mark 2>/dev/null || true
|
||
nft delete table ip6 cdn_bypass_tproxy 2>/dev/null || true
|
||
nft delete table ip6 cdn_bypass_mark 2>/dev/null || true
|
||
nft -f "$NFT_FILE"
|
||
info "nft 打标表 + tproxy 表已加载(IPv6 表:$([ "$ENABLE_IPV6" = 1 ] && echo 已加载 || echo 未启用))"
|
||
|
||
# 7. 策略路由(运行时 + 开机 unit)
|
||
printf '%s\n' "$route_unit" > "/etc/systemd/system/$ROUTE_UNIT"
|
||
systemctl daemon-reload
|
||
systemctl enable "$ROUTE_UNIT"
|
||
# 必须 restart 而不是只 enable/start:oneshot + RemainAfterExit 的 unit 若已处于 active(旧版本装的),
|
||
# 单纯 start 不会重新执行 ExecStart,新写入的 v6 规则就永远不会生效(实测踩过)。
|
||
systemctl restart "$ROUTE_UNIT"
|
||
# 报告实际内核状态,而不是只看变量(避免「显示 v6:是,实际没加」的假绿灯)
|
||
local v4_ok=0 v6_ok=0
|
||
ip rule show | grep -q "fwmark 0x$MARK lookup" && v4_ok=1
|
||
ip -6 rule show 2>/dev/null | grep -q "fwmark 0x$MARK lookup" && v6_ok=1
|
||
info "策略路由:v4=$([ "$v4_ok" = 1 ] && echo 已生效 || echo 缺失) v6=$([ "$v6_ok" = 1 ] && echo 已生效 || echo 缺失)"
|
||
[ "$v4_ok" = 1 ] || warn "v4 fwmark 规则未生效,绕行不会工作"
|
||
[ "$ENABLE_IPV6" = 1 ] && [ "$v6_ok" = 0 ] && warn "v6 fwmark 规则未生效,v6 CDN 流量不会被绕行"
|
||
|
||
# 7b. networkd drop-in:防 networkd 重启清掉上面的 fwmark 规则(落盘即对下次重启生效,无需现在重启 networkd)
|
||
mkdir -p "$(dirname "$NETWORKD_DROPIN")"
|
||
printf '%s\n' "$networkd_dropin" > "$NETWORKD_DROPIN"
|
||
info "已写入 $NETWORKD_DROPIN(ManageForeignRoutingPolicyRules=no,下次 networkd 重启起生效)"
|
||
|
||
# 8. 实测
|
||
info "实测绕行链路(直连必超时,通=必然走了代理)..."
|
||
local code
|
||
code="$(curl -4 -m 12 -sS -o /dev/null -w '%{http_code}' https://www.cloudflare.com/cdn-cgi/trace 2>/dev/null || echo FAIL)"
|
||
if [ "$code" = "200" ]; then
|
||
info "cdn-cgi/trace 200 ✅"
|
||
curl -4 -m 12 -sS https://www.cloudflare.com/cdn-cgi/trace 2>/dev/null | grep -E '^ip=' | sed 's/^/ 出口身份: /'
|
||
else
|
||
warn "cdn-cgi/trace 未通($code)—— 请检查上游节点可用性:curl -4 -m 8 https://www.cloudflare.com/cdn-cgi/trace"
|
||
fi
|
||
info "完成。xray 原 config.json 与主 unit 均未被改动;卸载:sudo ACTION=uninstall bash $0"
|
||
}
|
||
|
||
# ---------- 卸载 ----------
|
||
do_uninstall() {
|
||
if [ -n "$DRY_RUN" ]; then info "DRY_RUN:卸载只列动作"; return 0; fi
|
||
[ -n "$XRAY_SERVICE" ] || { systemctl cat xray.service >/dev/null 2>&1 && XRAY_SERVICE=xray; }
|
||
[ -n "$XRAY_SERVICE" ] || { systemctl cat v2ray.service >/dev/null 2>&1 && XRAY_SERVICE=v2ray; }
|
||
# 顺序很重要:【先】撤 nft 打标与策略路由,【再】停实例。
|
||
# 反过来会让"仍在打标 + 端口已关"同时成立,数秒内 CDN 流量全部被 tproxy 到死端口(RST/黑洞)。
|
||
rm -f "$NFT_FILE"
|
||
[ -f "$NFTABLES_CONF" ] && sed -i "\|include \"$NFT_FILE\"|d" "$NFTABLES_CONF"
|
||
nft delete table ip cdn_bypass_tproxy 2>/dev/null || true
|
||
nft delete table ip cdn_bypass_mark 2>/dev/null || true
|
||
nft delete table ip6 cdn_bypass_tproxy 2>/dev/null || true
|
||
nft delete table ip6 cdn_bypass_mark 2>/dev/null || true
|
||
ip rule del fwmark "$MARK" lookup "$ROUTE_TABLE" 2>/dev/null || true
|
||
ip route del local 0.0.0.0/0 dev lo table "$ROUTE_TABLE" 2>/dev/null || true
|
||
ip -6 rule del fwmark "$MARK" lookup "$ROUTE_TABLE" 2>/dev/null || true
|
||
ip -6 route del local ::/0 dev lo table "$ROUTE_TABLE" 2>/dev/null || true
|
||
systemctl disable --now "$BYPASS_SERVICE" 2>/dev/null || true
|
||
rm -f "/etc/systemd/system/$BYPASS_SERVICE.service"
|
||
rm -f "$XRAY_CONFDIR/cdn-bypass.json"
|
||
rmdir "$XRAY_CONFDIR" 2>/dev/null || true
|
||
# 旧版 drop-in 残留:仅当确实存在时才重启主服务
|
||
local legacy=0
|
||
if [ -n "$XRAY_SERVICE" ] && remove_legacy_dropins; then legacy=1; fi
|
||
systemctl daemon-reload
|
||
if [ "$legacy" = 1 ]; then
|
||
systemctl restart "$XRAY_SERVICE" && info "已移除旧版 -confdir drop-in,$XRAY_SERVICE 已重启"
|
||
fi
|
||
systemctl disable --now "$ROUTE_UNIT" 2>/dev/null || true
|
||
rm -f "/etc/systemd/system/$ROUTE_UNIT"
|
||
rm -f "$NETWORKD_DROPIN"
|
||
systemctl daemon-reload
|
||
info "卸载完成"
|
||
}
|
||
|
||
# 清单过时提醒(Akamai 段来自 BGP,漂移快;只是提醒,不阻塞)
|
||
cdn_list_staleness_hint() {
|
||
local stamp days
|
||
stamp=$(date -u -d "$CDN_LIST_AUDIT_DATE" +%s 2>/dev/null) || return 0
|
||
days=$(( ( $(date -u +%s) - stamp ) / 86400 ))
|
||
if [ "$days" -gt 180 ]; then
|
||
warn "CDN 清单已 $days 天未刷新(审计日 $CDN_LIST_AUDIT_DATE)。Akamai 段来自 ASN/BGP,"
|
||
warn "漂移很快,过时会导致「白绕已不宣告的段」和「漏绕新宣告的段」。刷新方法见脚本头部。"
|
||
fi
|
||
return 0
|
||
}
|
||
|
||
case "$ACTION" in
|
||
install) cdn_list_staleness_hint; do_install;;
|
||
uninstall) do_uninstall;;
|
||
*) die "ACTION 只支持 install/uninstall";;
|
||
esac
|