VPS 开荒配置脚本集
新购 VPS 的一键初始化脚本集合。每个脚本都是单文件、自包含、无外部依赖的 bash,
wget 下来直接 sudo bash 就跑;参数全部支持环境变量传入(支持 wget|bash 管道),
缺失的凭据自动生成并落盘保存,重跑幂等、不会失效已分发的客户端配置。
索引
- 选型总览
- setup_warp_zerotrust.sh — IPv6-only VPS 的 IPv4 出口
- setup_caddy_xray_hysteria.sh — 自建代理全家桶
- setup_cdn_bypass_tproxy.sh — CDN 绕行透明代理
- migrate_existing_stack.sh — 手动部署栈迁移进脚本管理
选型总览
| 脚本 | 用途 | 适用场景 |
|---|---|---|
| setup_warp_zerotrust.sh | 给 IPv6-only VPS 加 Cloudflare WARP IPv4 出口(纯 WireGuard,无官方客户端) | 没有 IPv4 的廉价 VPS 需要访问 IPv4 网络 |
| setup_caddy_xray_hysteria.sh | Caddy + Xray(REALITY/WS/XHTTP)+ Hysteria2 + nftables 端口跳跃 | 自建代理全家桶:隐蔽直连 + CDN 备用 + UDP 加速 |
| setup_cdn_bypass_tproxy.sh | 本机到 CDN 的流量透明代理到上游 xray 节点 | Google Cloud 等封锁/计费 CDN 出口的 VPS |
| migrate_existing_stack.sh | 把手动部署的 Caddy+Xray+Hy2 栈纳入 setup.env 管理(凭据保留、客户端不断线) | 老机器想纳入 ACTION=upgrade 统一升级体系 |
flowchart TD
A["新购 VPS"] --> B{"缺什么?"}
B -->|"IPv6-only<br/>没有 IPv4 出口"| C["setup_warp_zerotrust.sh<br/>WARP WireGuard IPv4 出口"]
B -->|"要自建代理全家桶"| D["setup_caddy_xray_hysteria.sh<br/>Caddy + Xray + Hy2 + 端口跳跃"]
B -->|"CDN 出口被封/计费<br/>(如 Google Cloud)"| E["setup_cdn_bypass_tproxy.sh<br/>CDN 流量透明代理到上游节点"]
D -.->|"装了全家桶后才发现<br/>CDN 被封 (证书无法续期)"| E
共同约定:Debian/Ubuntu;中文注释;ACTION=uninstall 卸载;凭据仅存本机 600 权限文件。
setup_warp_zerotrust.sh
给 IPv6-only 的 VPS 加一个 Cloudflare WARP 的 IPv4 出口 —— 纯 WireGuard 实现, 不装官方 WARP 客户端(不引入任何桌面组件),全部软件只走 apt 官方签名源。
flowchart LR
subgraph VPS["IPv6-only VPS"]
WG["wg0 WireGuard<br/>AllowedIPs 0.0.0.0/0"]
V6["原生 IPv6 协议栈"]
end
V6 -->|"注册 / 隧道握手走 IPv6"| API["Cloudflare 注册 API<br/>IPv6 anycast"]
WG -->|"IPv4 流量"| WARP["Cloudflare WARP 骨干网"]
V6 -->|"IPv6 流量不接管"| NET["原生 IPv6 出口"]
两种模式:
| 模式 | 说明 | 设备管理 |
|---|---|---|
| 免费版(默认) | 匿名注册 consumer WARP,拿到 IPv4 出口 | 无 |
| Zero Trust 团队版 | 注册为组织的团队设备,出口走 Cloudflare 骨干网 | 设备出现在 Zero Trust 仪表盘,受 Gateway L3 策略管控 |
快速开始
# 免费版: 交互提示出现时直接回车
wget -qO- https://git.henhaoji.site/eli/vpsetup/raw/branch/main/setup_warp_zerotrust.sh | bash
# 团队版: 填团队名 + Service Token 凭据即可(见下方配置步骤)
可选环境变量: ALLOWED_IPS(默认 0.0.0.0/0)、MTU(默认 1280)、
DNS64_SERVERS(替换安装期临时 DNS64)、ACTION=delete(注销设备并卸载)。
Zero Trust 仪表盘配置(2026 年最新路径)
⚠️ 网上老教程写的
Settings > WARP Client > Device enrollment permissions是旧版导航,现在找不到很正常。以下是 2026 年现行界面路径 (依据官方文档 Deploy the Cloudflare One Client on headless Linux machines)。
第 0 步:开通 Zero Trust(已有组织可跳过)
- 登录 Cloudflare 仪表盘,左侧进入 Zero Trust
- 首次进入会引导创建组织、设置 team name(团队名,全局唯一)
- 选 Free 计划。注意: 免费计划也要求绑定支付方式(信用卡/PayPal) 做验证,不会收费;Free 计划上限 50 台设备
第 1 步:确认团队名(team name)
- 路径: Zero Trust > Settings,页面上的 Team name 即你的团队名,
下面的 Team domain 是由它生成的注册域名(
<团队名>.cloudflareaccess.com) - 脚本的
CF_TEAM填 name 或完整 domain 都可以,内部自动归一化
第 2 步:创建 Service Token(拿 Client ID / Secret)
- 路径: Zero Trust > Access controls > Service credentials > Service Tokens
- 点 Create Service Token
- 起个名字(比如
vps-warp),选择 Service Token Duration(有效期)- 到期只是不能再注册新设备,已注册设备不受影响;建议选一年或更长
- 点 Generate token,得到:
- Client ID,形如
88bf3b6d86161464f6509f7219099e57.access - Client Secret,64 位十六进制,只显示这一次,务必当场复制
- Client ID,形如
第 3 步:允许用 Service Token 注册设备(关键,漏了会 403)
- 路径: Zero Trust > Team & Resources > Devices,切到 Management 标签页
- 找到 Device enrollment permissions,点 Manage
- 在 Policies 标签页点 Create new policy(会新开一个标签页):
- Action 必须选
Service Auth—— ⚠️ 选成Allow是常见错误: Allow 是交互式身份登录策略,service token 永远拿不到注册 JWT, 脚本会报no enrollment JWT returned (HTTP 302, Location: .../cdn-cgi/access/login/...) - 规则: Include → Selector 选
Service Token→ Value 选你刚建的 token 名 (想省事也可以选Any Access Service Token) - 保存策略
- Action 必须选
- 回到 Device enrollment permissions,把刚创建的策略添加进去,点 Save
第 4 步:在 VPS 上运行
交互式(推荐):
wget -qO- https://git.henhaoji.site/eli/vpsetup/raw/branch/main/setup_warp_zerotrust.sh | bash
# Zero Trust 团队名(回车跳过): myteam
# Service Token Client ID (形如 xxxx.access): 88bf...access
# Service Token Client Secret (输入不显示): ********
或非交互(环境变量齐全时不会有任何提问):
wget -qO /tmp/zt.sh https://git.henhaoji.site/eli/vpsetup/raw/branch/main/setup_warp_zerotrust.sh
CF_TEAM=myteam \
CF_ACCESS_CLIENT_ID=88bf...access \
CF_ACCESS_CLIENT_SECRET=bdd3... \
bash /tmp/zt.sh
凭据不全时自动回退免费版(会有明显提示)。
验证
- 脚本结尾:
IPv4 Exit显示 Cloudflare 的出口 IP,IPv6 Exit仍是 VPS 原生 IPv6,WARP status: warp=on - 仪表盘: Zero Trust > Team & Resources > Devices 列表里出现一台新设备,
身份为
non_identity@<团队名>.cloudflareaccess.com
常见问题
注册报 403 / 429? 403 = 你的 VPS IP 段被 Cloudflare 标记(机房 IP 常见),换 IP 或稍后再试; 429 = 注册限频,等几分钟。脚本会打印服务器原始返回,按 error code 判断。
报 no enrollment JWT returned?
脚本会打印 HTTP 状态码和重定向目标,对照排查:
0. HTTP 404 = 团队域名不存在 → 团队名拼写错误
(建议从 Zero Trust > Settings 页面复制,避免手打)
- 策略 Action 选成了
Allow(最常见)——必须是Service Auth, 特征是 HTTP 302 且 Location 指向/cdn-cgi/access/login/... - 策略建好后没加进 Device enrollment permissions 列表(要点 Save)
- Client ID/Secret 复制不全,或 token 已过期(重建一个即可)
重复运行会重复占设备坑吗?
不会。凭据存在 /etc/wireguard/warp-account.json,重跑时对同一设备 PATCH
换新密钥;仅当模式切换(免费↔团队)时才重新注册。卸载并释放席位:
ACTION=delete bash setup_warp_zerotrust.sh
纯 WireGuard 接入和官方客户端有什么区别?
- 仪表盘的 split tunnel、设备配置文件不会下发——路由以本机
wg0.conf的AllowedIPs为准(默认只接管 IPv4,不碰 IPv6) - Gateway 的 L3(网络层)策略正常生效;L7/HTTP 检查类策略需要官方客户端
- 设备为非身份(non-identity)注册,基于用户身份的策略不适用
内核没有 WireGuard 模块(LXC/老内核)? 脚本自动探测并回退到用户态 wireguard-go(apt 官方源,约 1MB),速度略慢 但功能一致,开机自启同样生效;LXC 容器需先在面板开启 TUN/TAP。
安全说明
- 只通过 apt 官方签名源装包,不添加第三方源,不下载/执行任何远程二进制
- 外联仅 Cloudflare 官方域名;连通性检测用官方
cdn-cgi/trace - 私钥/凭据仅存本机
/etc/wireguard/(600 权限),只有公钥提交给注册接口 - 安装期临时使用公共 DNS64(
DNS64_SERVERS可覆盖),结束后自动还原系统 DNS
参考实现: wgcf · Aether · fscarmen/warp
setup_caddy_xray_hysteria.sh
在 Debian/Ubuntu VPS 上一键部署 Caddy + Xray + Hysteria2 + nftables 端口跳跃。
架构图与端口表
flowchart LR
subgraph CLI["客户端"]
R["REALITY 客户端"]
W["WS / XHTTP 客户端"]
H["Hysteria2 客户端"]
end
subgraph VPS["VPS"]
X443["Xray :443 TCP<br/>VLESS+REALITY"]
FB{"REALITY 探测通过?"}
CADDY["Caddy 127.0.0.1:8003<br/>伪装反代 (PROXY protocol)"]
WS["Xray 127.0.0.1:54442<br/>WS 路径 /scilad"]
XH["Xray 127.0.0.1:8080<br/>XHTTP 随机路径"]
HY2["Hysteria2 :8443 UDP"]
NFT["nftables 端口跳跃<br/>UDP 20000-30000 → 8443"]
C80["Caddy :80"]
ACME["hy2 ACME HTTP-01<br/>127.0.0.1:9180"]
end
R -->|"直连 443"| X443
X443 --> FB
FB -->|"是"| OK["VLESS 代理"]
FB -->|"否: 回落"| CADDY
W -->|"橙云 CDN 回源 443"| X443
CADDY -->|"路径 /scilad"| WS
CADDY -->|"随机路径"| XH
H -->|"UDP 任意跳跃端口"| NFT
NFT --> HY2
LE["Let's Encrypt"] -->|"HTTP-01 到 80"| C80
C80 -->|"反代 challenge"| ACME
ACME -.->|"签 DOMAIN 证书"| HY2
| 组件 | 监听 | 说明 |
|---|---|---|
| Xray | 443/TCP | VLESS+REALITY(xtls-rprx-vision),非 REALITY 流量回落到 Caddy |
| Xray | 127.0.0.1:54442 | VLESS+WS(路径 /scilad,经 Caddy 反代) |
| Xray | 127.0.0.1:8080 | VLESS+XHTTP(随机路径,mode auto,经 Caddy 反代) |
| Caddy | 127.0.0.1:8003 | 收 REALITY 回落(PROXY protocol),伪装反代 + WS/XHTTP 入口 |
| Hysteria2 | 8443/UDP | 默认 HTTP-01 签 DOMAIN 证书(challenge 仅监听 127.0.0.1:9180,由 Caddy 80 端口反代验证);可选 DNS-01 签 *.主域 通配符 |
| nftables | UDP 20000-30000 | redirect 到 8443(端口跳跃) |
快速开始
wget -qO setup_caddy_xray_hysteria.sh \
https://git.henhaoji.site/eli/vpsetup/raw/branch/main/setup_caddy_xray_hysteria.sh
sudo bash setup_caddy_xray_hysteria.sh # 交互式:只需回答主域名
sudo ACTION=upgrade bash setup_caddy_xray_hysteria.sh # 升级全部组件到最新版(沿用已存参数,不交互)
sudo ACTION=uninstall bash setup_caddy_xray_hysteria.sh # 卸载(PURGE=1 连包一起删)
三个组件的安装方式本身就是幂等升级(Caddy 走 apt 仓库、Xray/hy2 官方脚本重跑即拉 最新版),所以重复运行安装也能升级;
ACTION=upgrade的区别是:未安装过会报错 而不是误装,且完全不交互。
必填:DOMAIN(灰云子域名,Caddy 站点、REALITY serverName、hy2 证书都用它)。
Cloudflare 回源设置指引(2026 年现行界面)
部署前后的域名与回源配置按此操作。核心原则:REALITY 域名必须灰云直连 (REALITY 原理上不能过 CDN),CDN 域名橙云代理(只承载 WS/XHTTP)。
A. 直连域名 DOMAIN(Caddy 站点 + REALITY + hy2,必配)
- DNS → Records:添加 A 记录(IPv4)或 AAAA 记录(IPv6-only VPS), 名称 = 你的子域名,Proxy status 保持灰色云(DNS only)——不要点亮。
- 无其他设置;REALITY 流量由客户端直连 443,Caddy 证书自己向 Let's Encrypt 签。
B. CDN 域名 CDN_DOMAIN(可选,VPS IP 被封后的备用通道)
- DNS → Records:同样指向 VPS IP,但点亮橙色云(Proxied)。
- SSL/TLS → Overview(概述):加密模式选 Full (strict)。 回源证书由 Caddy 用 Let's Encrypt 签(CF 透传 HTTP-01 challenge),满足 strict 校验。 不要用 Flexible(回源走 80 明文且与 hy2 的 HTTP-01 端口语义冲突)。
- Origin Rules:无需配置(全部 Preserve 即可)。CF 回源时 SNI/Host 默认
就是
CDN_DOMAIN:请求先到 Xray 443,REALITY serverNames 不匹配即原样回落 Caddy,Caddy 按 SNI 匹配到 CDN 站点块(WS/XHTTP/伪装反代),全链路不依赖 任何 Host/SNI 改写。注意 Origin Rules 的 Host Header / SNI / DNS record 改写 是 Enterprise 专属(免费版灰色不可选,仅剩 Destination Port 可用)—— 对本架构毫无影响,无需为此升级 plan。 - SSL/TLS → Edge Certificates(边缘证书):
- Encrypted ClientHello (ECH):Free 版默认已开启且界面不显示开关
(官方:"ECH is enabled by default on Free zones"),无需操作。可用
dig +short HTTPS 你的CDN域名验证返回含ech=(本机 systemd-resolved stub 查不出 type65 时换dig @1.1.1.1或 DoH JSON 接口)。 开之后审查者只能看到"你连了 Cloudflare"(外层 SNI 统一为 cloudflare-ech.com),看不到CDN_DOMAIN;客户端 Xray ≥25.7.26 内核, 在节点上启用 ECH(或链接加ech参数)。 - Always Use HTTPS:建议打开。不会与源站 Caddy 的 80→443 跳转形成循环
("too many redirects" 只在源站把 HTTPS 跳回 HTTP 时发生;Caddy 是
http→https 单向跳,方向一致)。打开后访客到边缘的 80 请求由 CF 直接 301,
不再回源 80;ACME challenge 走
/.well-known/路径不受影响。 - Minimum TLS Version:改为 1.2(面板默认 1.0 是兼容上古客户端; 代理客户端全是 TLS 1.2+,收紧无影响)。
- HTTP/3 (QUIC):开关在 Speed → Settings → Protocol Optimization (2026 面板位置,不在 Network 也不在边缘证书页),默认开启即可; 访客↔CF 边缘走 H3,回源仍是 TCP,与本架构无关。
- Advanced Certificate Manager (ACM):付费插件(自定义边缘证书 CA/有效期、 Total TLS、边缘 cipher suites),与本架构无关,无需激活。
- Encrypted ClientHello (ECH):Free 版默认已开启且界面不显示开关
(官方:"ECH is enabled by default on Free zones"),无需操作。可用
- Network → WebSockets:保持开启(默认开),WS 回源需要。 XHTTP 基于普通 HTTP 流式请求,无需额外开关。
C. 常见误区
- 两个域名不能是同一条 DNS 记录切换灰/橙:REALITY 要灰、CDN 要橙,必须两个独立子域名。
- 橙云域名不要再在客户端里配 REALITY——CF 终止 TLS 后源站收到的是普通 HTTPS, REALITY 客户端连不上(会回落成伪装网站,这就是它"看起来正常但代理不通"的原因)。
- VPS 防火墙/安全组只需放行:
443/TCP(Xray)、443/UDP(若想让 CF 边缘 H3 直连,可不开)、8443/UDP(hy2)+20000-30000/UDP(端口跳跃)、80/TCP(HTTP-01 证书验证与跳转)。8003、54442、8080、9180全部只绑 127.0.0.1,无需放行。
hy2 证书模式
hy2 证书两种模式(HY2_CERT_MODE):
http(默认):hy2 内置 ACME(certmagic)走 HTTP-01,challenge 只监听127.0.0.1:9180,由 Caddy 的 80 端口把/.well-known/acme-challenge/反代过去 完成验证。签的是 DOMAIN 非通配证书,自动续期、热生效、无需重启, 不需要任何 DNS API Token,也不新开公网端口(TLS-ALPN 必须独占 443/TCP, 与 Xray 冲突,故不可用)。dns:Cloudflare DNS-01 签*.主域通配符证书,需CF_API_TOKEN(Zone.DNS 编辑权限)。域名不想指向本机 80 端口或想要通配符时用。
可选环境变量
全部留空则自动生成并保存到 /etc/caddy-xray-hy2/setup.env(600 权限),
重跑自动读取作默认值,不会重新生成导致已分发客户端失效:
| 变量 | 默认 | 用途 |
|---|---|---|
EMAIL |
admin@主域 |
ACME 注册邮箱 |
CDN_DOMAIN |
空 | 橙云域名,独立站点块,仅承载 WS/XHTTP 回源(REALITY 不能过 CDN,二者必须分开) |
CADDY_SITES_DIR |
/etc/caddy/sites |
额外站点目录(升级安全的扩展点):脚本只渲染 import <目录>/*.caddy,目录内容归用户所有,详见下节 |
HY2_CERT_MODE |
http |
hy2 证书模式:http(默认,经 Caddy 80 验证)或 dns(CF DNS-01 通配符) |
CF_API_TOKEN |
空 | 仅 HY2_CERT_MODE=dns 时必填 |
HY2_ACME_PORT |
9180 |
hy2 HTTP-01 challenge 的本机监听端口 |
XRAY_UUID / XRAY_WS_UUID / XRAY_XHTTP_UUID |
随机 | 三个 inbound 的 UUID |
XRAY_PRIVATE_KEY |
xray x25519 生成 |
REALITY 私钥(公钥自动推导) |
XRAY_SHORT_ID |
8 位 hex | REALITY shortId |
XRAY_VLESS_ENC |
关 | =1 启用 VLESS 后量子加密(见下方「可选增强」) |
XHTTP_PATH / XHTTP_PORT |
/8位hex / 8080 |
XHTTP 路径与本地端口 |
XHTTP_PADDING |
开 | XHTTP extra padding 混淆(随机 header/key),服务端 extra 块与分享链接 extra= 参数(分享标准 4.3.19)带同一份键名。=0 关闭。注意:忽略 extra 的旧客户端会连不上(服务端 400),升级开启后旧链接作废、需重发 |
HY2_PASSWORD |
32 位 hex | Hysteria2 密码 |
HY2_MASQUERADE_URL |
https://engineersblog.net |
hy2 伪装反代目标 |
SKIP_CADDY / SKIP_XRAY / SKIP_HYSTERIA / SKIP_NFTABLES |
— | =1 跳过对应组件 |
DIST_UPGRADE |
— | =1 安装前 apt-get dist-upgrade |
DRY_RUN |
— | =1 干跑:所有配置渲染到 ./dry-run-output/,不做系统变更 |
安装结束自动打印 REALITY / WS / XHTTP /(可选 CDN)/ Hysteria2 的分享链接。
Caddy 官方 apt 源不可用时(2026-10 cloudsmith 402)
Caddy 的 apt 源托管在 Cloudsmith 的免费 OSS 额度上。2026-10-09 起该仓库的包投递配额被 CI
滥用打满,dl.cloudsmith.io/public/caddy/stable/deb/debian 下所有下载一律返回
402 Payment Required(上游 issue:caddyserver/dist#142;
2023、2024 年也各发生过一次,2024 那次约 8 天恢复)。apt-get update 因此必然失败。
脚本对此只做一种、且很窄的容错(apt_update_tolerant()):
- 报错全部来自 caddy/cloudsmith 源、且本机已经装了 caddy → 打
[WARN]继续: 本次跳过 caddy 二进制升级,但配置照常重新渲染 →caddy validate→ 重载服务; - 报错牵涉任何别的仓库,或本机还没有 caddy → 依旧硬失败退出(不吞真问题)。
所以 402 期间 ACTION=upgrade 仍能跑完(caddy 版本停在已装的那个),只是拿不到新二进制。
若上游长期不恢复,可选 Caddy 官方 GitHub Release 的 .deb(同一份产物,带 Sigstore 签名与
校验和)——那是供应链口径的变更,需单独决定,脚本默认不做。
额外站点(升级安全的扩展点)
需要在同一台 VPS 上再挂自己的站点(反代内网服务、给别的项目当公网入口……)时,
不要手改 /etc/caddy/Caddyfile —— 脚本每次 install/upgrade 都整体重写它,
手改必丢。把你的站点块放进额外站点目录(CADDY_SITES_DIR,默认 /etc/caddy/sites):
# /etc/caddy/sites/netbird.caddy
*.netbird.example.com {
bind 127.0.0.1 [::1]
tls /etc/caddy/certs/netbird/fullchain.pem /etc/caddy/certs/netbird/key.pem
tls { alpn h2 http/1.1 }
reverse_proxy https://127.0.0.1:8444 {
transport http { tls_insecure_skip_verify }
}
}
改完自检并生效(reload 不会重启进程,连接不中断):
sudo caddy validate --config /etc/caddy/Caddyfile && sudo systemctl reload caddy
为什么它是升级安全的:脚本只在主 Caddyfile 末尾渲染一行
import /etc/caddy/sites/*.caddy,并对该目录只读 —— 只做两件事:建目录;
目录全空时补一个纯注释的 00-example.caddy。永不覆盖、永不删除你放进去的
任何文件,ACTION=uninstall 也不删。import 用的是 glob:Caddy 对"空匹配的 glob"
只警告不报错(v2.0→v2.11 一直如此),所以目录空着也不会让 caddy validate 失败。
四条硬性约束(00-example.caddy 里有一份同样的说明):
| 约束 | 原因 |
|---|---|
每个站点块必须 bind 127.0.0.1 [::1] |
公网 443 归 Xray REALITY,Caddy 只在 127.0.0.1:8003 收回落流量(经 listener_wrappers 的 PROXY protocol + tls 包装)。没有 bind 的块会落到另一个未包装的 :8003 监听器上:配置合法、caddy 也能起来,但 443 的流量永远到不了它(静默不可达),还可能在其它网卡上多开一个端口。脚本会体检告警(不阻断) |
建议沿用 tls { alpn h2 http/1.1 } |
该监听器只收 TCP 回落流量,h3/QUIC 永远用不上;通告 h3 反而会让客户端在 TCP 上误协商 h3 而挂死 |
| 通配符证书必须自己喂 | apt 版 caddy 没有 DNS 插件 → 签不了通配符。用 tls <fullchain.pem> <key.pem>(或 tls { load /dir })提供现成证书。普通单域名则可以自动签:Caddy 占着公网 80,且 ACME HTTP-01 的拦截先于 http:// 跳转块 |
换证书后必须 systemctl reload caddy |
Caddy 不 watch 证书文件:tls <cert> <key> / tls { load } 只在配置加载时读一次。ExecReload 已是 caddy reload --force(会重新 provisioning) |
另注:这里不能写全局选项块({ ... })—— 它必须是主 Caddyfile 的第一个块。
需要全局设置(例如 on_demand_tls)请改脚本/setup.env。
风险联动:额外站点写错会让 caddy validate 失败,从而让 ACTION=upgrade
中止。中止本身是安全的(旧配置与在跑的服务不受影响、升级只是没继续),但要知道
这个联动 —— 所以改完先自己跑一次 caddy validate。
防火墙兼容性(Oracle Cloud 等)
脚本对 nftables 的处理遵循一条原则:只管自己的表(inet hysteria_nat 端口跳跃),
绝不整体执行 /etc/nftables.conf。因此在各种发行版/云镜像下都安全共存:
- Oracle Cloud / Debian 云镜像:默认启用
iptables-persistent+/etc/iptables/rules.v4。 由于这些发行版的iptables实际是 iptables-nft,persistent 加载的规则活在 nft 的table ip filter里,与脚本的table inet hysteria_nat同内核、不同表,天然互不干扰。 - 纯 nftables 机器:同理,脚本只追加 include 行和自己的表文件,不动其余规则。
为什么绝不能图省事 nft -f /etc/nftables.conf(或 systemctl reload nftables):
发行版 conf 首行的 flush ruleset 是无差别清空所有表,运行时执行会瞬间抹掉
主机防火墙(含 SSH 放行)和 Docker 的 NAT 表,而 netfilter-persistent 只在开机时补回——
防火墙会一直裸奔到下次重启。开机时则没有这个顾虑:systemd 的硬依赖链保证
nftables.service(sysinit)先于 netfilter-persistent(multi-user)执行,flush 后
规则随即补回,且两者都 Before=network-pre.target,对外不可达的窗口不存在。
可选增强(默认关闭,按需开启)
VLESS 后量子加密(XRAY_VLESS_ENC=1):防"现在录制流量、未来量子计算机解密"
(harvest-now-decrypt-later)。用 xray vlessenc 生成 ML-KEM-768 后量子密钥对,
写入三个 VLESS inbound 的 decryption,分享链接自动带上客户端 encryption= 参数。
对速度/延迟几乎无感(握手包大约 1-2KB),但要求所有客户端内核 Xray ≥25.8,
不支持的客户端会连不上。密钥持久化在 setup.env,重跑不换。防封锁/限流无帮助,
属保密性强化,客户端生态统一新版时才建议开。
TLS ECH(仅 CDN_DOMAIN 场景,纯客户端配置):走 CF CDN 的 XHTTP/WS 链路上,不开 ECH 时
审查者能看到明文 SNI=CDN_DOMAIN(你的私有域名,被标记就整域报废)。开启后外层
SNI 统一为 CF 公共 ECH 名(cloudflare-ech.com),真实域名加密——审查者只知道你连了
Cloudflare。ECH 在 CF 边缘终止,VPS 无需任何改动(Free 版默认已开,见上方 B 节;
dig +short HTTPS CDN域名 应返回含 ech=)。客户端 Xray ≥25.7.26:分享链接加
&ech=<base64 ECHConfigList>(dig 返回里 ech= 后那串,URL 编码),或 JSON 出站
tlsSettings 里配 echConfigList(静态 base64,或 "域名+https://1.1.1.1/dns-query"
走内置 DoH 动态查询)与 echForceQuery(full/half/none,26.3.27 起默认 full)。
静态配置下 ECH 协商失败会直接报错而非回落明文 SNI,能连通即生效。
CF 轮换 ECH 密钥很勤(静态快照实测很快失效),所以脚本默认在两条 CDN 分享链接
内嵌动态查询形式 ech=CDN域名+udp://9.9.9.9——客户端 Xray 拨号时自查最新
配置并按 TTL 缓存刷新,轮换无感,无需手动更新。查询协议按 scheme 选择:
udp://9.9.9.9(默认):明文 UDP53,Quad9——8.8.8.8 偶发被断,9.9.9.9 实测 不被屏蔽、延迟略高;备选 149.112.112.112(Quad9 备用),或国内 223.5.5.5 / 119.29.29.29(最快最稳,查询记录国内可见——节点域名的 A 查询默认本来也走 国内 DNS,不多暴露什么)。报文理论上可被窃听/投毒,但配合 full 策略,伪造 记录只会导致断连、不会静默降级泄露 SNI,可接受。域名+https://8.8.8.8/dns-query:内置 DoH(h2 + chrome 指纹,最隐蔽),但 8.8.8.8:443 在墙内实测不稳——passwall 的"本机代理"会把路由器自身流量(含 Xray 的 DoH)劫进代理通道,代理一抖查询就失败。DoH 端点必须指到/dns-query(RFC8484 wireformat),/resolve是 JSON 调试接口、Xray 不认。tcp://不支持。 查询失败会硬失败(无明文 SNI 回落),不合适的网络用ECH_CONFIG=off或ECH_CONFIG=<静态 base64>快照(零 DNS 依赖但会过期)。 图形客户端(passwall 等):「ECH 配置」框填CDN域名+udp://9.9.9.9(动态) 或静态 base64;「ECH 查询策略」动态形式选 full(最严,防降级)或 half (查询失败可回落连通),静态 base64 选 none。 直连的 REALITY 域名不需要(SNI 本来就是公开伪装域名);直连 Caddy 的 TLS 链路 用不上(Caddy 的 ECH 2026 年才落地且需自行发布 DNS HTTPS 记录,CDN 链路才是零改动方案)。
hy2 场景化武器(不集成,需要时手动改 /etc/hysteria/config.yaml):
UDP 被选择性限速时试 obfs: salamander(不行再试实验性 gecko);UDP 整体被封的
网络用 mimic(XDP 伪装 TCP,需内核模块 + 客户端同开,与端口跳跃互斥)。
改完 systemctl restart hysteria-server。
安全说明
- 与 setup_warp_zerotrust.sh 不同,本脚本按官方文档引入 cloudsmith apt 源 (Caddy 官方仓库),并执行 Xray / Hysteria2 官方安装脚本;除此之外不下载 任何二进制或远程代码
- 凭据仅存本机
/etc/caddy-xray-hy2/setup.env(600 权限)
setup_cdn_bypass_tproxy.sh
适用场景与原理
Google Cloud 等厂商的 VPS 封锁(或对 CDN 出口计费)Cloudflare / Fastly / Akamai
三大 CDN 网段。影响最大的往往不是浏览网页,而是本机进程的自动化出站——
例如 Hysteria2/Caddy 的 ACME 证书续期要访问 acme-v02.api.letsencrypt.org
(Let's Encrypt 的 API 挂在 Cloudflare 上)和 api.cloudflare.com(DNS-01 验证),
被封后证书静默过期、代理服务瘫痪,这正是本脚本要解决的典型事故。
原理:本机发往 CDN 网段的流量由 nftables 打标 → 策略路由送进 lo → xray 的 dokodemo-door(tproxy)入站接管 → 经一个不限流上游节点出站。
flowchart LR
APP["本机出站进程<br/>curl / hysteria ACME / apt"] --> OUT{"nft cdn_bypass_mark<br/>OUTPUT 链"}
OUT -->|"目标是 CDN 段<br/>(287 段清单)"| MARK["打 mark 1"]
OUT -->|"私网 / mark 255 / 其他"| DIRECT["main 表直连"]
MARK --> RULE["ip rule fwmark 1<br/>lookup table 100"]
RULE --> LO["local 0.0.0.0/0 dev lo<br/>包被路由进 lo"]
LO --> TP["nft cdn_bypass_tproxy<br/>prerouting 链"]
TP --> DKD["xray dokodemo-door<br/>127.0.0.1:54443 followRedirect"]
DKD --> VLESS["vless 出站 (sockopt.mark=255 防回环)"]
VLESS --> UP["上游不限流节点"]
UP --> CDN["Cloudflare / Fastly / Akamai"]
BOOT["cdn-bypass-route.service<br/>(oneshot, 开机补 ip rule/route)"] -.->|"持久化"| RULE
前提:VPS 已用官方脚本装好 xray 或 v2ray(service 存在即可,本脚本不装 xray)。 准备一个上游节点的 vless:// 分享链接(支持 reality/tls/tcp/ws 参数)。
用法
wget -qO setup_cdn_bypass_tproxy.sh \
https://git.henhaoji.site/eli/vpsetup/raw/branch/main/setup_cdn_bypass_tproxy.sh
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 ... # 只渲染到 ./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")。
安装末尾自动实测 https://www.cloudflare.com/cdn-cgi/trace(直连必超时,
通了即证明走了代理),并打印出口身份 IP 供对照。
实现结构
脚本只做四件事,全部幂等、可重复执行:
- xray confdir 注入(不碰原配置):解析 vless 链接生成
/etc/xray-cdn-bypass/cdn-bypass.json(tproxy 入站 + vless 出站 + routing 规则), 通过 systemd drop-in 给 xray 的 ExecStart 追加-confdir——原 config.json 一个字节不动。重启前先xray run -test验证合并后的配置,失败则中止、 线上 xray 不受影响。若你的 xray 已在用 confdir,脚本会拒绝并提示手工合并。 - nft 两张表:
/etc/nftables/cdn-bypass.nft(cdn_bypass_mark打标 +cdn_bypass_tproxy接管),向 nftables.conf 幂等追加 include。与主脚本同一约定: 运行时只nft -f自己的文件,绝不执行整份 conf。CDN 清单为内嵌的 287 段快照 (Cloudflare + Fastly + Akamai)——被封锁的机器在代理起来之前无法在线拉取,必须内嵌。 - 策略路由持久化:
cdn-bypass-route.service(oneshot)开机补ip rule fwmark 1 lookup 100+ip route add local 0.0.0.0/0 dev lo table 100。 ip 命令是纯内核运行时状态,重启即失,必须由 service 持久化;运行时误删可用systemctl restart cdn-bypass-route恢复。 - 防回环:注入的 vless 出站带
sockopt.mark=255,nft 第一条规则豁免 mark 255——即使上游节点 IP 落在 CDN 段内也不会被重新劫回 xray 自身。
边界
- 只处理 IPv4(table ip):若 VPS 后续获得全局 IPv6,CDN 的 v6 流量不经过本代理
- 安装/卸载会重启 xray 一次(现有 REALITY 连接秒级中断)
- 只代理"目标是 CDN 段"的流量,其余出站路径不变,不影响 xray 原有入站/出站逻辑
- 卸载即全部还原(confdir 文件、drop-in、nft 表、route unit 一并清除)
migrate_existing_stack.sh
适用场景
老机器上是手动(或旧脚本)部署的 Caddy + Xray + Hysteria2:凭据散落在三份
配置里、没有 /etc/caddy-xray-hy2/setup.env,无法用 ACTION=upgrade 幂等升级。
本脚本只做读取 + 备份 + 生成 setup.env,不重启、不修改任何运行中的服务;
迁移后再跑一次 ACTION=upgrade 即完成全面升级,且 REALITY / WS / hysteria2
的存量客户端凭据不变、不断线。
flowchart LR
OLD["旧配置<br/>Caddyfile / xray config.json<br/>hysteria config.yaml"] --> PARSE["解析提取凭据<br/>uuid / REALITY 密钥 / shortId<br/>WS 路径 / hy2 密码 / ACME 模式"]
PARSE --> CONFIRM{"交互确认<br/>DOMAIN / EMAIL / WS_PATH"}
CONFIRM --> ENV["写 setup.env (600)<br/>+ 备份旧配置"]
CONFIRM --> EXTRA["非主域 Caddy 站点块<br/>→ 抽出 + 安装进额外站点目录(升级不再覆盖)"]
CONFIRM --> NFT{"nft 体检"}
NFT -->|"干净/与模板一致"| NFTOK["升级时幂等接管"]
NFT -->|"有自定义规则"| NFTREF["生成参考配置<br/>提示自行处理,绝不插入 include"]
ENV --> UPGRADE["ACTION=upgrade<br/>setup_caddy_xray_hysteria.sh"]
UPGRADE --> DONE["全面升级完成<br/>旧客户端不断线"]
用法
wget -qO migrate_existing_stack.sh \
https://git.henhaoji.site/eli/vpsetup/raw/branch/main/migrate_existing_stack.sh
sudo bash migrate_existing_stack.sh # 交互式:默认值全部取自旧配置,逐项回车确认
DRY_RUN=1 bash migrate_existing_stack.sh # 干跑:读真实配置,输出到 ./dry-run-output/,不动系统
# 迁移完成后全面升级:
sudo ACTION=upgrade bash setup_caddy_xray_hysteria.sh
迁移行为细则
- Xray:提取 REALITY 入站的 uuid / privateKey / shortId / serverName 和 WS 入站
uuid,写入 setup.env;公钥用
xray x25519 -i现推。多个 shortId 只迁移第一个 (会警告)。旧 XHTTP 入站不迁移(部署脚本会生成新 XHTTP 凭据,旧 XHTTP 客户端 需换链接)。配置里的//注释自动剥离后解析。 - WS 路径:旧部署若是
handle_path /vlessws这类自定义路径,迁移会把WS_PATH=/vlessws写入 setup.env——部署脚本(WS_PATH 参数化后)按原路径渲染, 旧 WS 客户端无感。DOMAIN 与旧 REALITY serverName 不一致会显著警告 (旧客户端 sni 钉死旧域,换域即断)。 - Hysteria2:password / masquerade / ACME 模式整体迁移。
dns + cloudflare模式连同 CF_API_TOKEN 一起写入,通配符证书形态保持不变(certmagic 复用/etc/hysteria/acme_certs里现有证书,不重签)。其他 DNS provider 会警告并 回退 http 模式(单域名证书,存量通配符 sni 的 hy2 客户端需人工确认)。 - Caddy 额外站点:主域名之外的站点块verbatim 抽出,并自动安装进额外站点目录
(
CADDY_SITES_DIR,默认/etc/caddy/sites)里的migrated-extra-sites.caddy—— 升级后由新 Caddyfile 末尾的import <目录>/*.caddy自动加载,不再需要人工合并, 也不会被后续升级覆盖(详见上文「额外站点」一节)。参考件仍留一份在迁移输出目录。 两种例外只留参考文件、不自动安装:① 块里含dns <provider>等定制插件指令 (如 xcaddy 编译的 duckdns/namecheap 插件)——官方 apt 的 caddy 不含该插件,装了会让caddy validate失败、反而挡住升级,需保留定制二进制或用 xcaddy 重编后自行放入; ② 装了 caddy 时caddy adapt预检不通过(只解析适配,不做 TLS 签发)。 另外,缺bind 127.0.0.1的块会告警:443 的 REALITY 回落流量到不了它。 目标文件已存在时会先备份为.bak-<时间戳>再覆盖。 - nft:只体检不修改。
hysteria_nat表不存在或与部署脚本模板完全一致 → 干净,升级时幂等接管;检测到自定义/额外规则 → 生成参考配置hysteria-reference.nft并提示自行处理,绝不插入 nftables.conf 的 include。 - 备份:三份旧配置原样复制到
/etc/caddy-xray-hy2/migration-backup-<时间戳>/。 - 幂等防线:setup.env 已存在时直接拒绝运行(说明已在管理体系内,应直接用
ACTION=upgrade)。
开发与测试
bash -n setup_*.sh migrate_*.sh # 语法检查
shellcheck setup_*.sh migrate_*.sh # lint
bash tests/run_tests.sh # DRY_RUN 断言测试(含 migrate 的 fixtures 场景)