#!/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 < 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 < /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