#!/usr/bin/env bash # # migrate_existing_stack.sh — 把手动部署的 Caddy + Xray + Hysteria2 栈迁移到 # setup_caddy_xray_hysteria.sh 的管理体系(生成 /etc/caddy-xray-hy2/setup.env) # # 适用:机器上已有手工/旧脚本部署的 caddy + xray + hysteria2,凭据散落在各配置里, # 没有 setup.env,无法用 ACTION=upgrade 幂等升级。 # # 本脚本只做"读取 + 备份 + 生成 setup.env",**不重启/不修改任何运行中的服务**: # 1. 解析现有 Caddyfile / xray config.json / hysteria config.yaml, # 提取旧客户端依赖的凭据(uuid / REALITY 密钥 / shortId / WS 路径 / hy2 密码等) # 2. 交互确认 DOMAIN / EMAIL / WS_PATH 等(默认取解析值) # 3. nft 体检:hysteria_nat 表干净(不存在或与部署脚本模板完全一致)则报告 # "升级时自动接管";不干净则生成参考配置并提示自行处理,绝不插入 include # 4. Caddyfile 中主域名之外的站点块整段抽出,并安装进额外站点目录 # (CADDY_SITES_DIR,默认 /etc/caddy/sites)—— 新 Caddyfile 末尾的 # `import <目录>/*.caddy` 会自动加载,install/upgrade 永不覆盖,无需人工合并。 # 两种例外只留参考文件不自动安装:块里含 dns 等定制插件指令 # (官方 apt caddy 不含该插件,装了会让 caddy validate 失败、反而挡住升级); # 或 `caddy adapt` 预检不通过。 # 5. 备份三份旧配置到 /etc/caddy-xray-hy2/migration-backup-<时间戳>/ # 6. 写 setup.env(mode 600),格式与部署脚本 save_env 一致 # # 迁移后运行 sudo ACTION=upgrade bash setup_caddy_xray_hysteria.sh 即可完成全面升级, # 同协议旧客户端(REALITY / WS / hysteria2)凭据不变、不断线。 # # 用法: # sudo bash migrate_existing_stack.sh # 交互式迁移 # DRY_RUN=1 bash migrate_existing_stack.sh # 干跑:读真实配置,输出到 ./dry-run-output/,不动系统 # # 可选环境变量(测试/非常规路径覆盖): # CADDYFILE_PATH XRAY_CONFIG_PATH HYSTERIA_CONFIG_PATH NFTABLES_CONF # NFT_RULESET_FILE(伪造 `nft list table inet hysteria_nat` 输出,供测试) # ENV_FILE(setup.env 目标路径)DRY_RUN_DIR # set -euo pipefail DRY_RUN="${DRY_RUN:-}" DRY_RUN_DIR="${DRY_RUN_DIR:-./dry-run-output}" ENV_FILE="${ENV_FILE:-/etc/caddy-xray-hy2/setup.env}" CADDYFILE_PATH="${CADDYFILE_PATH:-/etc/caddy/Caddyfile}" XRAY_CONFIG_PATH="${XRAY_CONFIG_PATH:-/usr/local/etc/xray/config.json}" HYSTERIA_CONFIG_PATH="${HYSTERIA_CONFIG_PATH:-/etc/hysteria/config.yaml}" NFTABLES_CONF="${NFTABLES_CONF:-/etc/nftables.conf}" # 额外站点目录(与 setup_caddy_xray_hysteria.sh 的 CADDY_SITES_DIR 同义): # 抽出的非主域站点块会被安装到这里,由新 Caddyfile 末尾的 import 自动加载, # 且 install/upgrade 永不覆盖该目录里的用户文件 CADDY_SITES_DIR="${CADDY_SITES_DIR:-/etc/caddy/sites}" # 输出目录:生成物(额外站点片段、nft 参考配置)与 setup.env 的落点 if [ -n "$DRY_RUN" ]; then OUT_DIR="$DRY_RUN_DIR" else OUT_DIR="/etc/caddy-xray-hy2/migration" fi # 额外站点目录的实际落点:干跑时写进 DRY_RUN_DIR/sites(与主脚本 DRY_RUN 行为一致) sites_dir() { if [ -n "$DRY_RUN" ]; then printf '%s\n' "$DRY_RUN_DIR/sites" else printf '%s\n' "$CADDY_SITES_DIR" fi } # 是否有可交互终端(读 /dev/tty 判定,wget|bash 管道场景也可用) INTERACTIVE="" if (exec 9<>/dev/tty) 2>/dev/null; then exec 9>&- 9<&- INTERACTIVE=1 fi info() { echo "[INFO] $*"; } warn() { echo "[WARN] $*" >&2; } die() { echo "[ERROR] $*" >&2; exit 1; } # 交互提问:prompt_var VAR_NAME "提示语"(已有值作默认,回车接受;无 tty 跳过) prompt_var() { local var="${1:?}" msg="${2:?}" [ -n "$INTERACTIVE" ] || return 0 local cur input cur="${!var:-}" if [ -n "$cur" ]; then read -r -p "$msg [$cur]: " input /dev/null 2>&1 || die "需要 python3(apt install python3)" if [ -n "$DRY_RUN" ]; then info "DRY_RUN=1:读取真实配置,所有生成物写入 $DRY_RUN_DIR,不做系统变更" fi } # --------------------------------------------------------------------------- # 解析 Caddyfile(python3:按花括号配对切出顶层块,保留原文) # 输出 KEY=VALUE 行:CADDY_EMAIL / CADDY_SITES(逗号分隔) / CADDY_WS_HANDLE_PATH / CADDY_WS_PATH_MATCH # --------------------------------------------------------------------------- parse_caddy() { [ -f "$CADDYFILE_PATH" ] || { warn "未找到 $CADDYFILE_PATH,跳过 Caddy 解析"; return 0; } local out out="$(python3 - "$CADDYFILE_PATH" <<'PYEOF' import re, sys text = open(sys.argv[1], encoding="utf-8", errors="replace").read() # 花括号配对扫描:跟踪行注释状态(# 到行尾),在原文上进行以保留偏移可用 blocks = [] # [header_start, brace_start, block_end] depth = 0 i = 0 header_start = 0 n = len(text) in_comment = False while i < n: c = text[i] if in_comment: if c == "\n": in_comment = False i += 1 continue if c == "#": in_comment = True i += 1 continue if c == "{": if depth == 0: blocks.append([header_start, i, None]) depth += 1 elif c == "}": depth -= 1 if depth == 0: blocks[-1][2] = i header_start = i + 1 i += 1 email = "" sites = [] ws_handle = "" ws_path_match = "" for hs, bs, be in blocks: header = re.sub(r"(?m)#.*$", "", text[hs:bs]).strip() body = text[bs + 1:be] body_nc = re.sub(r"(?m)#.*$", "", body) if header == "": # 全局选项块 m = re.search(r"(?m)^\s*email\s+(\S+)", body_nc) if m and not email: email = m.group(1) continue # 站点块:header 为地址列表(逗号/空格分隔) addrs = [a for a in re.split(r"[,\s]+", header) if a] if not addrs: continue sites.extend(addrs) if "54442" in body_nc: m = re.search(r"(?m)^\s*handle_path\s+(\S+)", body_nc) if m and not ws_handle: ws_handle = m.group(1) m = re.search(r"(?m)^\s*path\s+(\S+)", body_nc) if m and not ws_path_match: ws_path_match = m.group(1) print("CADDY_EMAIL=" + email) print("CADDY_SITES=" + ",".join(sites)) print("CADDY_WS_HANDLE_PATH=" + ws_handle) print("CADDY_WS_PATH_MATCH=" + ws_path_match) PYEOF )" local k v while IFS='=' read -r k v; do case "$k" in CADDY_EMAIL|CADDY_SITES|CADDY_WS_HANDLE_PATH|CADDY_WS_PATH_MATCH) printf -v "$k" '%s' "$v" ;; esac done <<< "$out" info "Caddyfile:站点=${CADDY_SITES:-无} email=${CADDY_EMAIL:-无}" } # 抽取非主域站点块原文到 $OUT_DIR/caddy-extra-sites.conf;检测定制 DNS 插件指令与 bind # 抽出的块随后会被安装进额外站点目录(CADDY_SITES_DIR),由新 Caddyfile 末尾的 import 自动加载。 extract_caddy_extra_sites() { [ -f "$CADDYFILE_PATH" ] || return 0 [ -n "${CADDY_SITES:-}" ] || return 0 EXTRA_SITES_FILE="$OUT_DIR/caddy-extra-sites.conf" EXTRA_INSTALLED="" local res res="$(python3 - "$CADDYFILE_PATH" "$DOMAIN" "$EXTRA_SITES_FILE" <<'PYEOF' import re, sys, os path, domain, outfile = sys.argv[1], sys.argv[2], sys.argv[3] text = open(path, encoding="utf-8", errors="replace").read() # 与 parse_caddy 相同的注释感知花括号扫描,块文本按原文偏移 verbatim 抽取 blocks = [] depth = 0 i = 0 header_start = 0 n = len(text) in_comment = False while i < n: c = text[i] if in_comment: if c == "\n": in_comment = False i += 1 continue if c == "#": in_comment = True i += 1 continue if c == "{": if depth == 0: blocks.append([header_start, i, None]) depth += 1 elif c == "}": depth -= 1 if depth == 0: blocks[-1][2] = i header_start = i + 1 i += 1 extra = [] plugins = set() nobind = 0 for hs, bs, be in blocks: header = re.sub(r"(?m)#.*$", "", text[hs:bs]).strip() if not header: continue addrs = [a for a in re.split(r"[,\s]+", header) if a] if not addrs: continue # http:// 等协议块是部署脚本自身会生成的,不属于"额外站点" if all(a.startswith("http://") or a.startswith("https://") for a in addrs): continue if domain in addrs: continue body_nc = re.sub(r"(?m)#.*$", "", text[bs + 1:be]) for m in re.finditer(r"(?m)^\s*dns\s+(\w+)", body_nc): plugins.add(m.group(1)) # 没有 bind 127.0.0.1 的块会落到未包装的 :8003 监听器上(443 回落流量到不了它) if not re.search(r"(?m)^\s*bind\s+127\.0\.0\.1", body_nc): nobind += 1 extra.append(text[hs:be + 1].strip("\n")) if extra: os.makedirs(os.path.dirname(outfile), exist_ok=True) with open(outfile, "w", encoding="utf-8") as f: f.write("# 从旧 Caddyfile 抽取的非主域站点块(主域=%s),原文 verbatim\n" % domain) f.write("# migrate_existing_stack.sh 会把它安装到额外站点目录,由新 Caddyfile 末尾的\n") f.write("# `import <目录>/*.caddy` 自动加载;install/upgrade 永不覆盖该目录里的用户文件。\n\n") f.write("\n\n".join(extra)) f.write("\n") print("EXTRA_COUNT=%d" % len(extra)) print("EXTRA_PLUGINS=" + ",".join(sorted(plugins))) print("EXTRA_NOBIND=%d" % nobind) PYEOF )" local k v while IFS='=' read -r k v; do case "$k" in EXTRA_COUNT|EXTRA_PLUGINS|EXTRA_NOBIND) printf -v "$k" '%s' "$v" ;; esac done <<< "$res" [ "${EXTRA_COUNT:-0}" -gt 0 ] || return 0 warn "检测到 $EXTRA_COUNT 个非主域站点块,已抽取到 $EXTRA_SITES_FILE" if [ "${EXTRA_NOBIND:-0}" -gt 0 ]; then warn " → 其中 $EXTRA_NOBIND 个块没有 bind 127.0.0.1:443 的 REALITY 回落流量到不了它" warn " (会落到未包装的 :8003 监听器上),建议补上 bind 127.0.0.1 [::1]" fi # 定制 dns 插件:装了也没用(caddy validate 会失败,反而挡住升级)→ 只留参考文件 if [ -n "${EXTRA_PLUGINS:-}" ]; then warn " → 这些块使用了 DNS 插件指令(dns ${EXTRA_PLUGINS//,/, dns }):官方 apt 仓库的 caddy 不含该插件," warn " 直接安装会让 caddy validate 失败、从而挡住 ACTION=upgrade,故不自动安装;" warn " 需保留定制二进制或用 xcaddy 重编后,自行放进 $(sites_dir)/" return 0 fi # 装了 caddy 就先 adapt 预检(只解析/适配,不做 TLS provisioning):过不了就不安装 if command -v caddy >/dev/null 2>&1 \ && ! caddy adapt --config "$EXTRA_SITES_FILE" --adapter caddyfile >/dev/null 2>&1; then warn " → caddy adapt 预检不通过,未自动安装(只留参考文件;人工核对后放进 $(sites_dir)/)" return 0 fi local dir target dir="$(sites_dir)" mkdir -p "$dir" chmod 755 "$dir" target="$dir/migrated-extra-sites.caddy" if [ -e "$target" ]; then cp -a "$target" "$target.bak-$(date +%Y%m%d-%H%M%S)" warn " → $target 已存在,已备份为 .bak-<时间戳> 后再覆盖" fi install -m 644 "$EXTRA_SITES_FILE" "$target" EXTRA_INSTALLED=1 info " → 已安装到 $target:新 Caddyfile 末尾的 import 会自动加载它,**不再需要人工合并**" info " (ACTION=upgrade 之后生效;自检:sudo caddy validate --config /etc/caddy/Caddyfile)" } # --------------------------------------------------------------------------- # 解析 xray config.json(剥 // 与 /* */ 注释后按 JSON 解析) # 输出:XRAY_UUID XRAY_PRIVATE_KEY XRAY_SHORT_ID REALITY_SNI # XRAY_WS_UUID WS_XRAY_PATH XRAY_XHTTP_UUID XHTTP_PATH_OLD XHTTP_PORT_OLD # --------------------------------------------------------------------------- parse_xray() { [ -f "$XRAY_CONFIG_PATH" ] || { warn "未找到 $XRAY_CONFIG_PATH,跳过 Xray 解析"; return 0; } local out out="$(python3 - "$XRAY_CONFIG_PATH" <<'PYEOF' import json, re, sys text = open(sys.argv[1], encoding="utf-8", errors="replace").read() # 剥注释:状态机避开字符串内部 out = [] i = 0 n = len(text) in_str = False while i < n: c = text[i] if in_str: out.append(c) if c == "\\": if i + 1 < n: out.append(text[i + 1]) i += 1 elif c == '"': in_str = False i += 1 continue if c == '"': in_str = True out.append(c) i += 1 elif c == "/" and i + 1 < n and text[i + 1] == "/": while i < n and text[i] != "\n": i += 1 elif c == "/" and i + 1 < n and text[i + 1] == "*": i += 2 while i + 1 < n and not (text[i] == "*" and text[i + 1] == "/"): i += 1 i += 2 elif c == "#": while i < n and text[i] != "\n": i += 1 else: out.append(c) i += 1 cfg = json.loads("".join(out)) vals = {} sids = [] for ib in cfg.get("inbounds", []): ss = ib.get("streamSettings", {}) net = ss.get("network", "tcp") sec = ss.get("security", "none") clients = ib.get("settings", {}).get("clients", []) cid = clients[0].get("id", "") if clients else "" if sec == "reality": rs = ss.get("realitySettings", {}) vals["XRAY_UUID"] = cid vals["XRAY_PRIVATE_KEY"] = rs.get("privateKey", "") sids = rs.get("shortIds", []) vals["XRAY_SHORT_ID"] = sids[0] if sids else "" sns = rs.get("serverNames", []) vals["REALITY_SNI"] = sns[0] if sns else "" vals["SHORTID_COUNT"] = str(len(sids)) elif net == "ws": vals["XRAY_WS_UUID"] = cid vals["WS_XRAY_PATH"] = ss.get("wsSettings", {}).get("path", "") elif net == "xhttp": vals["XRAY_XHTTP_UUID"] = cid vals["XHTTP_PATH_OLD"] = ss.get("xhttpSettings", {}).get("path", "") vals["XHTTP_PORT_OLD"] = str(ib.get("port", "")) for k, v in vals.items(): print("%s=%s" % (k, v)) PYEOF )" || die "xray 配置解析失败(JSON 不合法?):$XRAY_CONFIG_PATH" local k v while IFS='=' read -r k v; do case "$k" in XRAY_UUID|XRAY_PRIVATE_KEY|XRAY_SHORT_ID|REALITY_SNI|SHORTID_COUNT|\ XRAY_WS_UUID|WS_XRAY_PATH|XRAY_XHTTP_UUID|XHTTP_PATH_OLD|XHTTP_PORT_OLD) printf -v "$k" '%s' "$v" ;; esac done <<< "$out" if [ "${SHORTID_COUNT:-0}" -gt 1 ]; then warn "旧 REALITY 配置有 $SHORTID_COUNT 个 shortId,只迁移第一个($XRAY_SHORT_ID);其余 shortId 的客户端升级后会断,需换新链接" fi [ -n "$XRAY_UUID" ] || warn "未在 xray 配置中找到 REALITY 入站(无 REALITY 凭据可迁移,升级时会生成全新凭据)" info "Xray:REALITY uuid=$(mask "${XRAY_UUID:-}") sni=${REALITY_SNI:-无} WS uuid=$(mask "${XRAY_WS_UUID:-}")" } # --------------------------------------------------------------------------- # 解析 hysteria config.yaml(结构浅,awk 按顶格块划分即可,不依赖 pyyaml) # 输出:HY2_PASSWORD HY2_ACME_TYPE HY2_ACME_DOMAIN HY2_ACME_EMAIL # HY2_DNS_PROVIDER CF_API_TOKEN HY2_MASQUERADE_URL # --------------------------------------------------------------------------- parse_hysteria() { [ -f "$HYSTERIA_CONFIG_PATH" ] || { warn "未找到 $HYSTERIA_CONFIG_PATH,跳过 Hysteria2 解析"; return 0; } HY2_PASSWORD="$(awk '/^auth:/{f=1;next} /^[a-z]/{f=0} f && /password:/{print $2; exit}' "$HYSTERIA_CONFIG_PATH" | tr -d '"' | tr -d "'")" HY2_ACME_TYPE="$(awk '/^acme:/{f=1;next} /^[a-z]/{f=0} f && /^\s*type:/{print $2; exit}' "$HYSTERIA_CONFIG_PATH")" HY2_ACME_DOMAIN="$(awk '/^acme:/{f=1;next} /^[a-z]/{f=0} f && /^[[:space:]]*-[[:space:]]*/{sub(/^[[:space:]]*-[[:space:]]*/,""); print; exit}' "$HYSTERIA_CONFIG_PATH" | tr -d "\"'")" HY2_ACME_EMAIL="$(awk '/^acme:/{f=1;next} /^[a-z]/{f=0} f && /^\s*email:/{print $2; exit}' "$HYSTERIA_CONFIG_PATH")" HY2_DNS_PROVIDER="$(awk '/^acme:/{f=1;next} /^[a-z]/{f=0} f && /^\s*name:/{print $2; exit}' "$HYSTERIA_CONFIG_PATH")" if [ "$HY2_ACME_TYPE" = "dns" ]; then # acme 块内第一个 *_api_token / api_token 值 CF_API_TOKEN="$(awk '/^acme:/{f=1;next} /^[a-z]/{f=0} f && /api_token:/{print $2; exit}' "$HYSTERIA_CONFIG_PATH")" fi HY2_MASQUERADE_URL="$(awk '/^masquerade:/{f=1;next} /^[a-z]/{f=0} f && /^\s*url:/{print $2; exit}' "$HYSTERIA_CONFIG_PATH")" info "Hysteria2:password=$(mask "${HY2_PASSWORD:-}") acme=${HY2_ACME_TYPE:-无} domain=${HY2_ACME_DOMAIN:-无} provider=${HY2_DNS_PROVIDER:-无}" if [ "$HY2_ACME_TYPE" = "dns" ] && [ "$HY2_DNS_PROVIDER" != "cloudflare" ]; then warn "hy2 DNS-01 provider 是 $HY2_DNS_PROVIDER,部署脚本仅内置 cloudflare(HY2_CERT_MODE=dns + CF_API_TOKEN)" warn " → 迁移后若无法沿用 dns 模式,证书形态会变化,通配符 sni 的存量 hy2 客户端可能断线,请人工确认" fi } # --------------------------------------------------------------------------- # nft 体检(只检测不修改):hysteria_nat 表不存在或与部署脚本模板一致 → 干净 # --------------------------------------------------------------------------- NFT_CLEAN="" check_nft() { local table_out="" if [ -n "${NFT_RULESET_FILE:-}" ]; then table_out="$(cat "$NFT_RULESET_FILE")" elif command -v nft >/dev/null 2>&1; then table_out="$(nft list table inet hysteria_nat 2>/dev/null || true)" fi if [ -z "$table_out" ]; then NFT_CLEAN=1 info "nft:hysteria_nat 表不存在(干净,升级时会自动创建并接管)" else # 统计 nat/redirect 类规则数;恰好一条且是模板那条 → 干净 local rule_count rule_count="$(printf '%s\n' "$table_out" | grep -cE 'udp dport|redirect|dnat|masquerade' || true)" if [ "$rule_count" = "1" ] && printf '%s\n' "$table_out" | grep -qF 'udp dport 20000-30000 redirect to :8443'; then NFT_CLEAN=1 info "nft:hysteria_nat 表与部署脚本模板一致(干净,升级时幂等接管,无重复规则)" else NFT_CLEAN="" warn "nft:hysteria_nat 表存在但与部署脚本模板不一致(含自定义/额外规则)" warn " → 请自行核对:升级脚本会删除并重建 hysteria_nat 表,自定义规则将丢失" mkdir -p "$OUT_DIR" cat > "$OUT_DIR/hysteria-reference.nft" <<'NFT_REF' # 部署脚本的标准端口跳跃表(参考,不会自动插入 nftables.conf): table inet hysteria_nat { chain prerouting { type nat hook prerouting priority dstnat; policy accept; udp dport 20000-30000 redirect to :8443 } } NFT_REF warn " → 参考配置已写入 $OUT_DIR/hysteria-reference.nft(不会插入 $NFTABLES_CONF,请人工合并)" fi fi # include 行重复检测(仅提示) if [ -f "$NFTABLES_CONF" ]; then local inc_count inc_count="$(grep -cF 'include "/etc/nftables/hysteria.nft"' "$NFTABLES_CONF" || true)" if [ "$inc_count" -gt 1 ]; then warn "$NFTABLES_CONF 中 hysteria.nft 的 include 行重复 $inc_count 次,请人工去重" fi fi } # --------------------------------------------------------------------------- # 备份旧配置 # --------------------------------------------------------------------------- backup_configs() { local ts dir ts="$(date +%Y%m%d-%H%M%S)" if [ -n "$DRY_RUN" ]; then dir="$DRY_RUN_DIR/migration-backup-$ts" else dir="/etc/caddy-xray-hy2/migration-backup-$ts" fi mkdir -p "$dir" local f for f in "$CADDYFILE_PATH" "$XRAY_CONFIG_PATH" "$HYSTERIA_CONFIG_PATH"; do [ -f "$f" ] && cp -a "$f" "$dir/$(basename "$f").$(basename "$(dirname "$f")")" done info "旧配置已备份到 $dir" } # --------------------------------------------------------------------------- # 写 setup.env(与部署脚本 save_env 同格式:KEY=%q) # --------------------------------------------------------------------------- write_setup_env() { local target="$ENV_FILE" if [ -n "$DRY_RUN" ]; then mkdir -p "$DRY_RUN_DIR" target="$DRY_RUN_DIR/setup.env" else mkdir -p "$(dirname "$ENV_FILE")" fi umask 077 { printf '# setup.env(由 migrate_existing_stack.sh 迁移生成,重跑部署脚本自动读取)\n' printf '# 迁移时间 %s\n' "$(date '+%F %T')" local var for var in DOMAIN EMAIL HY2_CERT_MODE CF_API_TOKEN HY2_PASSWORD HY2_MASQUERADE_URL \ XRAY_UUID XRAY_WS_UUID XRAY_PRIVATE_KEY XRAY_PUBLIC_KEY XRAY_SHORT_ID WS_PATH; do printf '%s=%q\n' "$var" "${!var:-}" done } > "$target" chmod 600 "$target" info "setup.env 已写入 $target" } # --------------------------------------------------------------------------- # 主流程 # --------------------------------------------------------------------------- main() { preflight # 解析(只读) CADDY_EMAIL="" CADDY_SITES="" CADDY_WS_HANDLE_PATH="" CADDY_WS_PATH_MATCH="" XRAY_UUID="" XRAY_PRIVATE_KEY="" XRAY_SHORT_ID="" REALITY_SNI="" SHORTID_COUNT=0 XRAY_WS_UUID="" WS_XRAY_PATH="" XRAY_XHTTP_UUID="" XHTTP_PATH_OLD="" XHTTP_PORT_OLD="" HY2_PASSWORD="" HY2_ACME_TYPE="" HY2_ACME_DOMAIN="" HY2_ACME_EMAIL="" HY2_DNS_PROVIDER="" CF_API_TOKEN="" HY2_MASQUERADE_URL="" parse_caddy parse_xray parse_hysteria # DOMAIN:默认 REALITY sni → Caddy 站点第一个非特殊地址 → 空 local default_domain="${REALITY_SNI:-}" if [ -z "$default_domain" ] && [ -n "$CADDY_SITES" ]; then default_domain="$(printf '%s' "$CADDY_SITES" | tr ',' '\n' | grep -vE '^http://|^https://|\*' | head -n1)" fi DOMAIN="${DOMAIN:-$default_domain}" prompt_var DOMAIN "主域名(须与旧 REALITY sni 一致,否则旧客户端断线)" [ -n "$DOMAIN" ] || die "缺少 DOMAIN" [[ "$DOMAIN" =~ ^([A-Za-z0-9-]+\.)+[A-Za-z]{2,}$ ]] || die "DOMAIN 格式不合法:$DOMAIN" if [ -n "$REALITY_SNI" ] && [ "$REALITY_SNI" != "$DOMAIN" ]; then warn "DOMAIN=$DOMAIN 与旧 REALITY serverName=$REALITY_SNI 不一致!" warn " 旧 REALITY 客户端的 sni 钉死 $REALITY_SNI,换域后全部断线(新链接可恢复)" confirm "确认用新 DOMAIN 继续(旧 REALITY 客户端会断)?" || die "已中止" fi EMAIL="${EMAIL:-${CADDY_EMAIL:-${HY2_ACME_EMAIL:-}}}" prompt_var EMAIL "ACME 注册邮箱" EMAIL="${EMAIL:-admin@${DOMAIN#*.}}" # WS_PATH 优先级:Caddy handle_path > xray wsSettings.path(非 / 时)> Caddy path 匹配器 > /scilad WS_PATH="${WS_PATH:-${CADDY_WS_HANDLE_PATH:-}}" if [ -z "$WS_PATH" ] && [ -n "$WS_XRAY_PATH" ] && [ "$WS_XRAY_PATH" != "/" ]; then WS_PATH="$WS_XRAY_PATH" fi WS_PATH="${WS_PATH:-${CADDY_WS_PATH_MATCH:-}}" WS_PATH="${WS_PATH:-/scilad}" prompt_var WS_PATH "WS 入站路径(旧 WS 客户端使用的路径)" [[ "$WS_PATH" == /* ]] || WS_PATH="/$WS_PATH" WS_PATH="${WS_PATH%/}" [ -n "$WS_PATH" ] || WS_PATH="/" # hy2 证书模式:dns(仅 cloudflare 可直接沿用)/ http HY2_CERT_MODE="" if [ "$HY2_ACME_TYPE" = "dns" ] && [ "$HY2_DNS_PROVIDER" = "cloudflare" ] && [ -n "$CF_API_TOKEN" ]; then HY2_CERT_MODE="dns" elif [ "$HY2_ACME_TYPE" = "http" ]; then HY2_CERT_MODE="http" fi if [ -z "$HY2_CERT_MODE" ] && [ -n "$HY2_ACME_TYPE" ]; then warn "hy2 原证书模式($HY2_ACME_TYPE/${HY2_DNS_PROVIDER:-?})无法自动沿用,将回退为 http 模式(单域名证书)" HY2_CERT_MODE="http" fi # REALITY 公钥:有私钥且本机有 xray 二进制则现推;否则留空(升级时推导) XRAY_PUBLIC_KEY="" if [ -n "$XRAY_PRIVATE_KEY" ] && command -v xray >/dev/null 2>&1 && [ -z "$DRY_RUN" ]; then XRAY_PUBLIC_KEY="$(xray x25519 -i "$XRAY_PRIVATE_KEY" 2>/dev/null | grep -iE 'public|password' | awk -F': ' '{print $2}' | tr -d '[:space:]' || true)" fi # 汇总确认 cat <