Files
vpsetup/setup_cdn_bypass_tproxy.sh

606 lines
20 KiB
Bash
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/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