057a77f8ff
Co-Authored-By: Claude Code <noreply@anthropic.com>
606 lines
20 KiB
Bash
606 lines
20 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 confdir 片段(inbound/outbound/routing)
|
||
# —— 通过 systemd drop-in 给 xray 加 -confdir,**不改动用户原有 config.json 一个字节**
|
||
# 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 → 上游。即客户端访问 CDN 域名会自动走绕行,无需在主
|
||
# config.json 里加任何 routing 规则。前提:主配置的 direct 出站【不得】带 sockopt.mark=255
|
||
# (setup_caddy_xray_hysteria.sh 模板不带;若你手写的 direct 带了 255,客户端 CDN 流量会
|
||
# 被豁免直连——在 GCP 这类机器上就是"本机 curl 通、客户端不通"的典型症状)。
|
||
#
|
||
# 用法:
|
||
# sudo VLESS_LINK='vless://uuid@host:443?security=reality&pbk=...&sid=...&sni=...&fp=chrome&flow=xtls-rprx-vision#name' \
|
||
# bash setup_cdn_bypass_tproxy.sh
|
||
# 不设 VLESS_LINK 时会从 /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,默认自动探测)
|
||
# EXTRA_EXEMPT_CIDRS("8.8.8.8 119.29.29.29" 这类额外不打标的段,空格分隔)
|
||
#
|
||
# 已知边界:只处理 IPv4(table ip)。若 VPS 后续获得全局 IPv6,CDN 的 v6 流量不经过本代理。
|
||
|
||
set -euo pipefail
|
||
|
||
# ---------- 可配项 ----------
|
||
ACTION="${ACTION:-install}"
|
||
TPROXY_PORT="${TPROXY_PORT:-54443}"
|
||
ROUTE_TABLE="${ROUTE_TABLE:-100}"
|
||
MARK="${MARK:-1}"
|
||
XRAY_SERVICE="${XRAY_SERVICE:-}"
|
||
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}"
|
||
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 运行"
|
||
|
||
# ---------- CDN 网段清单(只收三家公司官方段,2026-09-28 来源审计后收敛到 188 条) ----------
|
||
# 来源:CF cloudflare.com/ips-v4(15 条)+ Fastly api.fastly.com/public-ip-list(19 条)
|
||
# + Akamai/Linode WHOIS AS20940/AS16625/AS63949 等(154 条)
|
||
# 2026-09 期间曾因治理 LE 证书续期把 GCP/Linode 回源节点误加为 CDN 段;本清单已剔除所有非官方段。
|
||
# 免在线获取:被封锁的机器在代理起来之前根本访问不了 cloudflare.com,所以必须内嵌。
|
||
read -r -d '' CDN_RANGES <<'RANGES_EOF' || true
|
||
103.12.23.0/24
|
||
103.15.143.0/24
|
||
103.16.77.0/24
|
||
103.21.244.0/22
|
||
103.22.200.0/22
|
||
103.238.148.0/22
|
||
103.243.12.0/22
|
||
103.244.50.0/24
|
||
103.245.222.0/23
|
||
103.245.224.0/24
|
||
103.29.68.0/22
|
||
103.3.60.0/22
|
||
103.31.4.0/22
|
||
103.95.84.0/22
|
||
104.156.80.0/20
|
||
104.16.0.0/13
|
||
104.200.16.0/20
|
||
104.237.128.0/19
|
||
104.24.0.0/14
|
||
104.64.0.0/10
|
||
108.162.192.0/18
|
||
109.237.24.0/22
|
||
109.74.192.0/20
|
||
115.69.232.0/22
|
||
118.214.0.0/15
|
||
122.252.128.0/20
|
||
122.252.32.0/19
|
||
124.158.25.0/24
|
||
125.252.192.0/18
|
||
125.56.128.0/17
|
||
131.0.72.0/22
|
||
139.144.0.0/16
|
||
139.162.0.0/16
|
||
139.177.176.0/20
|
||
139.177.192.0/20
|
||
140.248.128.0/17
|
||
140.248.64.0/18
|
||
141.101.64.0/18
|
||
143.42.0.0/16
|
||
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.2.0/24
|
||
165.254.40.0/23
|
||
165.254.44.0/23
|
||
165.254.52.0/24
|
||
167.82.0.0/17
|
||
167.82.128.0/20
|
||
167.82.160.0/20
|
||
167.82.224.0/20
|
||
168.143.254.0/23
|
||
170.187.128.0/17
|
||
172.104.0.0/15
|
||
172.111.64.0/18
|
||
172.224.0.0/12
|
||
172.64.0.0/13
|
||
173.205.69.0/24
|
||
173.205.76.0/22
|
||
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.225.250.0/23
|
||
185.3.92.0/22
|
||
185.31.16.0/22
|
||
188.114.96.0/20
|
||
189.247.204.0/22
|
||
189.247.216.0/24
|
||
190.93.240.0/20
|
||
190.94.188.0/24
|
||
190.98.153.0/24
|
||
190.98.160.0/23
|
||
192.155.80.0/20
|
||
192.33.24.0/21
|
||
192.46.208.0/20
|
||
192.46.224.0/20
|
||
192.53.112.0/20
|
||
192.53.160.0/20
|
||
192.81.128.0/21
|
||
193.108.152.0/22
|
||
193.108.88.0/23
|
||
193.108.91.0/24
|
||
193.108.92.0/24
|
||
193.108.94.0/23
|
||
194.195.112.0/20
|
||
194.195.208.0/20
|
||
194.195.240.0/20
|
||
194.233.160.0/19
|
||
195.245.124.0/22
|
||
195.95.192.0/22
|
||
197.234.240.0/22
|
||
198.180.186.0/23
|
||
198.41.128.0/17
|
||
198.58.96.0/19
|
||
198.74.48.0/20
|
||
199.101.28.0/22
|
||
199.119.220.0/22
|
||
199.232.0.0/16
|
||
199.27.72.0/21
|
||
2.16.0.0/13
|
||
200.60.136.0/23
|
||
200.60.190.0/24
|
||
202.12.75.0/24
|
||
202.138.164.0/22
|
||
202.138.183.0/24
|
||
202.226.44.0/22
|
||
203.198.20.0/24
|
||
203.27.6.0/23
|
||
203.69.138.0/24
|
||
203.69.141.0/24
|
||
204.10.28.0/22
|
||
204.2.196.0/24
|
||
204.8.48.0/22
|
||
204.93.48.0/24
|
||
210.61.248.0/23
|
||
212.111.40.0/22
|
||
212.71.232.0/21
|
||
212.71.244.0/22
|
||
212.71.248.0/21
|
||
213.168.248.0/22
|
||
213.219.36.0/22
|
||
213.52.128.0/22
|
||
219.76.11.0/24
|
||
23.0.0.0/12
|
||
23.192.0.0/11
|
||
23.235.32.0/20
|
||
23.239.0.0/19
|
||
23.32.0.0/11
|
||
23.64.0.0/14
|
||
23.72.0.0/13
|
||
23.92.16.0/20
|
||
27.116.32.0/23
|
||
43.249.213.0/24
|
||
43.249.72.0/22
|
||
43.254.120.0/22
|
||
45.118.132.0/22
|
||
45.33.0.0/17
|
||
45.56.64.0/18
|
||
45.79.0.0/16
|
||
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/23
|
||
64.89.224.0/20
|
||
66.175.208.0/20
|
||
66.228.32.0/19
|
||
66.246.75.0/24
|
||
69.164.192.0/19
|
||
69.192.0.0/16
|
||
69.22.150.0/23
|
||
69.31.112.0/23
|
||
69.31.118.0/23
|
||
69.31.122.0/24
|
||
70.85.129.0/24
|
||
70.85.16.0/24
|
||
70.85.31.0/24
|
||
70.87.222.0/24
|
||
72.14.176.0/20
|
||
72.200.254.0/23
|
||
72.246.0.0/15
|
||
72.52.0.0/24
|
||
72.52.62.0/24
|
||
74.121.124.0/22
|
||
74.207.224.0/19
|
||
80.67.64.0/19
|
||
80.85.84.0/22
|
||
81.200.64.0/20
|
||
84.53.128.0/18
|
||
85.159.208.0/21
|
||
85.90.244.0/22
|
||
88.221.0.0/16
|
||
88.80.184.0/21
|
||
92.122.0.0/15
|
||
95.100.0.0/15
|
||
96.126.96.0/19
|
||
96.16.0.0/15
|
||
96.6.0.0/15
|
||
97.107.128.0/20
|
||
RANGES_EOF
|
||
|
||
# ---------- 解析 vless:// 链接 ----------
|
||
parse_vless_link() {
|
||
local link="$1"
|
||
[[ "$link" == vless://* ]] || die "只支持 vless:// 链接(reality/tls/tcp/ws),收到:${link:0:20}..."
|
||
link="${link#vless://}"
|
||
V_UUID="${link%%@*}"; link="${link#*@}"
|
||
local hostport="${link%%\?*}"
|
||
V_HOST="${hostport%%:*}"
|
||
V_PORT="${hostport##*:}"
|
||
local query=""
|
||
[[ "$link" == *\?* ]] && { query="${link#*\?}"; query="${query%%#*}"; }
|
||
V_SECURITY=""; V_PBK=""; V_SID=""; V_SNI=""; V_FP="chrome"; V_FLOW=""; V_NETWORK="tcp"; V_PATH=""; V_HOSTHDR=""
|
||
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="$v";;
|
||
pbk|publicKey) V_PBK="$v";;
|
||
sid|shortId) V_SID="$v";;
|
||
sni) V_SNI="$v";;
|
||
fp) V_FP="$v";;
|
||
flow) V_FLOW="$v";;
|
||
type) V_NETWORK="$v";;
|
||
path) V_PATH="$v";;
|
||
host) V_HOSTHDR="$v";;
|
||
esac
|
||
done
|
||
[ -n "$V_UUID" ] && [ -n "$V_HOST" ] && [ -n "$V_PORT" ] || die "链接解析失败(缺 uuid/host/port)"
|
||
[ "$V_SECURITY" = "reality" ] && [ -z "$V_PBK" ] && die "reality 链接缺 pbk(publicKey)参数"
|
||
[ -z "$V_SNI" ] && V_SNI="$V_HOST"
|
||
info "节点:$V_HOST:$V_PORT security=${V_SECURITY:-none} network=$V_NETWORK flow=${V_FLOW:-无}"
|
||
}
|
||
|
||
# ---------- 生成 xray confdir 片段 ----------
|
||
render_xray_fragment() {
|
||
local stream
|
||
case "$V_SECURITY" in
|
||
reality)
|
||
stream=$(cat <<EOF
|
||
"network": "$V_NETWORK",
|
||
"security": "reality",
|
||
"realitySettings": {
|
||
"serverName": "$V_SNI",
|
||
"fingerprint": "$V_FP",
|
||
"publicKey": "$V_PBK",
|
||
"shortId": "$V_SID"
|
||
},
|
||
"sockopt": { "mark": 255 }
|
||
EOF
|
||
);;
|
||
tls)
|
||
stream=$(cat <<EOF
|
||
"network": "$V_NETWORK",
|
||
"security": "tls",
|
||
"tlsSettings": { "serverName": "$V_SNI", "fingerprint": "$V_FP" },
|
||
"sockopt": { "mark": 255 }
|
||
EOF
|
||
);;
|
||
*)
|
||
stream=$(cat <<EOF
|
||
"network": "$V_NETWORK",
|
||
"sockopt": { "mark": 255 }
|
||
EOF
|
||
);;
|
||
esac
|
||
local flow_field=""
|
||
[ -n "$V_FLOW" ] && flow_field=",
|
||
\"flow\": \"$V_FLOW\""
|
||
local ws_field=""
|
||
if [ "$V_NETWORK" = "ws" ]; then
|
||
ws_field=$(cat <<EOF
|
||
,
|
||
"wsSettings": { "path": "${V_PATH:-/}", "headers": { "Host": "${V_HOSTHDR:-$V_HOST}" } }
|
||
EOF
|
||
)
|
||
fi
|
||
cat <<EOF
|
||
{
|
||
"//": "cdn-bypass: 由 setup_cdn_bypass_tproxy.sh 生成,重复运行会整体覆盖本文件,请勿手改",
|
||
"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" } }
|
||
}
|
||
],
|
||
"outbounds": [
|
||
{
|
||
"tag": "cdn-bypass-out",
|
||
"protocol": "vless",
|
||
"settings": {
|
||
"vnext": [
|
||
{
|
||
"address": "$V_HOST",
|
||
"port": $V_PORT,
|
||
"users": [
|
||
{
|
||
"id": "$V_UUID",
|
||
"encryption": "none"$flow_field
|
||
}
|
||
]
|
||
}
|
||
]
|
||
},
|
||
"streamSettings": {
|
||
$stream$ws_field
|
||
}
|
||
}
|
||
],
|
||
"routing": {
|
||
"rules": [
|
||
{
|
||
"type": "field",
|
||
"inboundTag": ["cdn-bypass-in"],
|
||
"outboundTag": "cdn-bypass-out"
|
||
}
|
||
]
|
||
}
|
||
}
|
||
EOF
|
||
}
|
||
|
||
# ---------- 生成 nft 文件 ----------
|
||
render_nft() {
|
||
local extra=""
|
||
local c
|
||
for c in $EXTRA_EXEMPT_CIDRS; do extra+=" ip daddr $c accept\n"; 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
|
||
}
|
||
|
||
# ---------- 生成策略路由 unit ----------
|
||
render_route_unit() {
|
||
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'
|
||
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'
|
||
|
||
[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,只注入 confdir)"; fi
|
||
fi
|
||
XRAY_EXECSTART="$(systemctl show "$XRAY_SERVICE" -p ExecStart --value)"
|
||
[ -n "$XRAY_EXECSTART" ] || die "读不到 $XRAY_SERVICE 的 ExecStart"
|
||
case "$XRAY_EXECSTART" in
|
||
*-confdir*) die "$XRAY_SERVICE 的 ExecStart 已含 -confdir:$XRAY_EXECSTART(为避免覆盖既有用法,请手工把 $(basename "$XRAY_CONFDIR") 文件放进已有 confdir)";;
|
||
esac
|
||
# 新版 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"
|
||
info "xray 服务:$XRAY_SERVICE ExecStart:$XRAY_EXECSTART"
|
||
}
|
||
|
||
render_confdir_dropin() {
|
||
# $XRAY_EXECSTART 可能是新版 systemd "{ path=... ; argv[]=...}" 形式。
|
||
# drop-in 只接受平铺的 ExecStart= 命令行,所以从 argv[] 抽出 —— 保留原始参数(含 -config 等)。
|
||
local cmdline
|
||
if [[ "$XRAY_EXECSTART" =~ argv\[\]=([^\;]+) ]]; then
|
||
cmdline="${BASH_REMATCH[1]}"
|
||
else
|
||
cmdline="$XRAY_EXECSTART"
|
||
fi
|
||
cat <<EOF
|
||
[Service]
|
||
# 清空原 ExecStart 后追加 -confdir(systemd drop-in 语法:先置空再重设)
|
||
ExecStart=
|
||
ExecStart=$cmdline -confdir $XRAY_CONFDIR/
|
||
EOF
|
||
}
|
||
|
||
# ---------- 安装 ----------
|
||
do_install() {
|
||
# 1. 读链接
|
||
if [ -z "${VLESS_LINK:-}" ]; then
|
||
if [ -r /dev/tty ]; then
|
||
printf '请粘贴 vless:// 节点链接:' > /dev/tty
|
||
read -r VLESS_LINK < /dev/tty
|
||
fi
|
||
[ -n "${VLESS_LINK:-}" ] || die "缺少 VLESS_LINK(环境变量或交互粘贴)"
|
||
fi
|
||
parse_vless_link "$VLESS_LINK"
|
||
|
||
# 2. 渲染
|
||
local xray_json nft_conf route_unit dropin
|
||
xray_json="$(render_xray_fragment)"
|
||
nft_conf="$(render_nft)"
|
||
route_unit="$(render_route_unit)"
|
||
|
||
if [ -n "$DRY_RUN" ]; then
|
||
printf '%s\n' "$xray_json" > "$DRY_RUN_DIR/cdn-bypass.json"
|
||
printf '%s\n' "$nft_conf" > "$DRY_RUN_DIR/cdn-bypass.nft"
|
||
printf '%s\n' "$route_unit" > "$DRY_RUN_DIR/$ROUTE_UNIT"
|
||
printf '%s\n' "ExecStart=<原值> -confdir $XRAY_CONFDIR/" > "$DRY_RUN_DIR/20-cdn-bypass-confdir.conf"
|
||
printf 'include "%s"\n' "$NFT_FILE" >> "$DRY_RUN_DIR/nftables.conf"
|
||
info "已渲染:cdn-bypass.json / cdn-bypass.nft / $ROUTE_UNIT / drop-in / nftables.conf(include)"
|
||
return 0
|
||
fi
|
||
|
||
detect_xray
|
||
dropin="$(render_confdir_dropin)"
|
||
|
||
# 3. xray confdir(不动用户原 config.json)
|
||
mkdir -p "$XRAY_CONFDIR"
|
||
printf '%s\n' "$xray_json" > "$XRAY_CONFDIR/cdn-bypass.json"
|
||
chmod 644 "$XRAY_CONFDIR/cdn-bypass.json"
|
||
info "已写入 $XRAY_CONFDIR/cdn-bypass.json"
|
||
|
||
# 4. 先用 -test 验证合并后的配置,再动 systemd(验证不过则不重启 xray,避免搞挂线上)
|
||
local base_conf
|
||
base_conf="$(echo "$XRAY_EXECSTART" | grep -oE '\-config [^ ]+' | awk '{print $2}')"
|
||
[ -n "$base_conf" ] || die "从 ExecStart 解析不到 -config 路径"
|
||
if ! "$XRAY_BIN" run -test -config "$base_conf" -confdir "$XRAY_CONFDIR/" >/dev/null 2>&1; then
|
||
"$XRAY_BIN" run -test -config "$base_conf" -confdir "$XRAY_CONFDIR/" 2>&1 | tail -5
|
||
die "合并后配置自检失败,未改动 systemd,xray 未受影响"
|
||
fi
|
||
info "xray 合并配置自检通过"
|
||
|
||
mkdir -p "/etc/systemd/system/$XRAY_SERVICE.service.d"
|
||
# 文件名必须排在官方安装脚本自带的 10-donot_touch_single_conf.conf 之后
|
||
# (它重置 ExecStart 为单 -config 形式;同为 10- 前缀时按 ASCII 序它后加载会盖掉我们的 -confdir)。
|
||
# 顺便清掉旧版脚本写过的 10- 前缀同名残留。
|
||
rm -f "/etc/systemd/system/$XRAY_SERVICE.service.d/10-cdn-bypass-confdir.conf"
|
||
printf '%s\n' "$dropin" > "/etc/systemd/system/$XRAY_SERVICE.service.d/20-cdn-bypass-confdir.conf"
|
||
systemctl daemon-reload
|
||
systemctl restart "$XRAY_SERVICE"
|
||
info "xray 已带 -confdir 重启"
|
||
|
||
# 5. 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
|
||
systemctl enable --now 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 -f "$NFT_FILE"
|
||
info "nft 打标表 + tproxy 表已加载"
|
||
|
||
# 6. 策略路由(运行时 + 开机 unit)
|
||
printf '%s\n' "$route_unit" > "/etc/systemd/system/$ROUTE_UNIT"
|
||
systemctl daemon-reload
|
||
systemctl enable --now "$ROUTE_UNIT"
|
||
info "策略路由已生效(fwmark $MARK → table $ROUTE_TABLE)并随开机持久化"
|
||
|
||
# 7. 实测
|
||
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 未被改动;卸载: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; }
|
||
rm -f "$XRAY_CONFDIR/cdn-bypass.json"
|
||
if [ -n "$XRAY_SERVICE" ]; then
|
||
rm -f "/etc/systemd/system/$XRAY_SERVICE.service.d/20-cdn-bypass-confdir.conf" \
|
||
"/etc/systemd/system/$XRAY_SERVICE.service.d/10-cdn-bypass-confdir.conf"
|
||
systemctl daemon-reload
|
||
systemctl restart "$XRAY_SERVICE" && info "xray 已还原并重启"
|
||
fi
|
||
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
|
||
systemctl disable --now "$ROUTE_UNIT" 2>/dev/null || true
|
||
rm -f "/etc/systemd/system/$ROUTE_UNIT"
|
||
systemctl daemon-reload
|
||
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
|
||
info "卸载完成"
|
||
}
|
||
|
||
case "$ACTION" in
|
||
install) do_install;;
|
||
uninstall) do_uninstall;;
|
||
*) die "ACTION 只支持 install/uninstall";;
|
||
esac
|