Files
workbuddy_skills/dual-link-proxy/scripts/install-dual-link.sh
T

535 lines
22 KiB
Bash
Raw Normal View History

#!/usr/bin/env bash
# 双链路代理一键安装(交互确认 / 可用 --yes 全默认)
#
# 一台空机器跑完即得:3x-ui 面板 + 两条 VLESS+WS 入站 + nginx 反代 + Let's Encrypt 证书
# 支持:Ubuntu/Debian 系 VPS,nginx 是宝塔/aaPanel 或系统自带均可
#
# 用法:
# ./install-dual-link.sh # 交互确认
# ./install-dual-link.sh --dry-run # 只打印将执行的动作,不改任何东西
# ./install-dual-link.sh --yes --domain-a a.example.com --domain-b b.example.com
# ./install-dual-link.sh --yes --domain-a a.x.com --domain-b b.x.com --panel-domain p.x.com
#
# 约定:本脚本只接 nginx 转发这一种架构(Reality 与 nginx 转发互斥)。
set -uo pipefail
HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
API_PY="${HERE}/3xui-api.py"
# ─── 参数默认值 ────────────────────────────────────────────────────────────
DOMAIN_A="" # 橙云链路(走 Cloudflare 代理)
DOMAIN_B="" # 灰云链路(A 记录直连回源)
PATH_A="/linkA" # 链路 A 的 WebSocket 路径
PATH_B="/linkB"
REMARK_A="linkA" # 入站备注名(也是客户端里链路的名字)
REMARK_B="linkB"
PORT_A=15004 # 后端入站端口(只绑 127.0.0.1)
PORT_B=15005
PANEL_PORT=2053 # 3x-ui 面板端口(只绑 127.0.0.1)
PANEL_DOMAIN="" # 可选:给面板一个子域,nginx 反代 + 一层 Basic Auth
PANEL_PASSWORD="" # 留空则随机生成;机器上已有 3x-ui 时必须显式给
OWNER_EMAIL="owner" # 首个账号标识
ACME_EMAIL="" # Let's Encrypt 注册邮箱,留空则用 admin@<域名A>
DRY=0
ASSUME=0
# ─── 输出工具 ──────────────────────────────────────────────────────────────
c_ok="\033[32m"; c_warn="\033[33m"; c_err="\033[31m"; c_dim="\033[2m"; c_end="\033[0m"
say() { printf "%b\n" "$*"; }
ok() { printf "${c_ok} ✓${c_end} %s\n" "$*"; }
warn() { printf "${c_warn} !${c_end} %s\n" "$*"; }
die() { printf "${c_err} ✗ %s${c_end}\n" "$*" >&2; exit 1; }
dim() { printf "${c_dim}%s${c_end}\n" "$*"; }
hr() { printf "${c_dim}%s${c_end}\n" "────────────────────────────────────────────────────"; }
run_sh() {
if [ "$DRY" = 1 ]; then printf "${c_dim} [dry-run] %s${c_end}\n" "$1"; return 0; fi
bash -c "$1"
}
ask() { # ask 变量名 "提示" 默认值
local __var="$1" __prompt="$2" __def="${3:-}" __ans=""
if [ -n "$__def" ]; then __prompt="$__prompt [$__def]"; fi
if [ "$ASSUME" = 1 ] || [ ! -t 0 ]; then
printf -v "$__var" '%s' "$__def"; return 0
fi
read -r -p " ${__prompt}: " __ans || true
[ -z "$__ans" ] && __ans="$__def"
printf -v "$__var" '%s' "$__ans"
}
usage() { sed -n '2,15p' "$0"; exit 0; }
# ─── 环境探测 ──────────────────────────────────────────────────────────────
NGINX_BIN=""; VHOST_DIR=""; CERT_DIR=""; WEBROOT_BASE=""; WEB_USER=""; LOG_DIR=""
detect_env() {
if [ -x /www/server/nginx/sbin/nginx ]; then
NGINX_BIN=/www/server/nginx/sbin/nginx
VHOST_DIR=/www/server/panel/vhost/nginx
CERT_DIR=/www/server/panel/vhost/cert
WEBROOT_BASE=/www/wwwroot
LOG_DIR=/www/wwwlogs
WEB_USER="www"
ok "检测到宝塔/aaPanel 的 nginx(${NGINX_BIN})"
else
NGINX_BIN="$(command -v nginx || true)"
[ -z "$NGINX_BIN" ] && die "没找到 nginx,请先安装 nginx"
VHOST_DIR=/etc/nginx/conf.d
CERT_DIR=/etc/nginx/certs
WEBROOT_BASE=/var/www
LOG_DIR=/var/log/nginx
WEB_USER="www-data"
ok "检测到系统 nginx(${NGINX_BIN})"
fi
id -u "$WEB_USER" >/dev/null 2>&1 || die "nginx 的运行用户 ${WEB_USER} 不存在"
if [ "$DRY" = 1 ]; then dim " [dry-run] mkdir -p ${VHOST_DIR} ${CERT_DIR} ${WEBROOT_BASE} ${LOG_DIR}"
else mkdir -p "$VHOST_DIR" "$CERT_DIR" "$WEBROOT_BASE" "$LOG_DIR"; fi
}
port_in_use() { # 只看该端口有没有 LISTEN;用 ss 自带的端口过滤器,避免列对齐导致的误判
local out
out="$(ss -tlnH "sport = :$1" 2>/dev/null)"
[ -n "$out" ]
}
need_free() { # 需要是空的:新后端/面板端口
if port_in_use "$1"; then die "端口 $1 已被占用(ss -tln | grep :$1),换一个再跑"; fi
ok "端口 $1 空闲"
}
need_nginx() { # 需要被 nginx 占着:80 / 443
if port_in_use "$1"; then ok "端口 $1 由 nginx 监听(预期)"
else warn "端口 $1 没有监听——nginx 没起来?证书签发会失败"; fi
}
my_ip() { curl -s --max-time 8 https://api.ipify.org 2>/dev/null || echo ""; }
backup_file() {
[ -f "$1" ] || return 0
local b="$1.bak-$(date +%Y%m%d-%H%M%S)"
if [ "$DRY" = 1 ]; then dim " [dry-run] cp -a $1 $b"; return 0; fi
cp -a "$1" "$b" && warn "原配置已备份 → $b"
}
nginx_apply() {
if [ "$DRY" = 1 ]; then dim " [dry-run] ${NGINX_BIN} -t && ${NGINX_BIN} -s reload"; return 0; fi
${NGINX_BIN} -t >/tmp/nginx-t.log 2>&1 || { tail -5 /tmp/nginx-t.log; die "nginx 配置检查未通过"; }
${NGINX_BIN} -s reload >/dev/null 2>&1 || die "nginx reload 失败"
ok "nginx 已重载"
}
# ─── 步骤 1:前置检查 ──────────────────────────────────────────────────────
step_preflight() {
say ""
say "[1/6] 前置检查"
[ "$(id -u)" = 0 ] || die "需要 root 权限执行"
detect_env
command -v python3 >/dev/null 2>&1 || die "需要 python3(调面板接口靠它)"
[ -f "$API_PY" ] || die "找不到 ${API_PY},请与 3xui-api.py 放在同一目录"
local pubip; pubip="$(my_ip)"
if [ -n "$pubip" ]; then ok "本机公网出口 IP:${pubip}"; else warn "取不到公网 IP,DNS 校验跳过"; fi
local d resolved
for d in "$DOMAIN_A" "$DOMAIN_B" $PANEL_DOMAIN; do
[ -z "$d" ] && continue
resolved="$(getent hosts "$d" | awk '{print $1}' | head -1)"
if [ -z "$resolved" ]; then
warn "${d} 解析不到 —— 先加 DNS 记录再跑,否则证书签发必然失败"
elif [ -n "$pubip" ] && [ "$resolved" != "$pubip" ]; then
warn "${d} → ${resolved}(与出口 IP 不同:走 CDN 属正常,直连链路必须指向本机)"
else
ok "${d} → ${resolved}"
fi
done
need_nginx 80
need_nginx 443
need_free "$PORT_A"
need_free "$PORT_B"
if [ -x /usr/local/x-ui/x-ui ] && port_in_use "$PANEL_PORT"; then
warn "面板端口 ${PANEL_PORT} 已被现有 3x-ui 占用 —— 走复用安装(需 --panel-password)"
else
need_free "$PANEL_PORT"
fi
[ -f /root/.acme.sh/acme.sh ] || warn "acme.sh 未安装,第 4 步会自动装"
}
# ─── 步骤 2:装后端 3x-ui ──────────────────────────────────────────────────
step_backend() {
say ""
say "[2/6] 安装后端 3x-ui(面板本体)"
if [ -x /usr/local/x-ui/x-ui ]; then
[ -z "$PANEL_PASSWORD" ] && die "本机已有 3x-ui。为免覆盖现有账号,请显式给出现有面板密码:
./install-dual-link.sh --panel-password '<现有密码>' ..."
warn "复用已存在的 /usr/local/x-ui(不重新下载)"
else
local ver
ver="$(curl -sI --max-time 20 https://github.com/MHSanaei/3x-ui/releases/latest 2>/dev/null \
| grep -i '^location:' | sed 's#.*/tag/##' | tr -d '\r')"
[ -z "$ver" ] && die "取不到 3x-ui 最新版本号(检查出网)"
ok "最新版本 ${ver}"
run_sh "cd /tmp && rm -rf x-ui xui.tar.gz && \
curl -fsSL -o xui.tar.gz https://github.com/MHSanaei/3x-ui/releases/download/${ver}/x-ui-linux-amd64.tar.gz && \
tar xzf xui.tar.gz && rm -rf /usr/local/x-ui && mv /tmp/x-ui /usr/local/x-ui && \
chmod +x /usr/local/x-ui/x-ui /usr/local/x-ui/bin/xray-linux-amd64"
run_sh "cp /usr/local/x-ui/x-ui.service.debian /etc/systemd/system/x-ui.service"
fi
if [ -z "$PANEL_PASSWORD" ]; then
PANEL_PASSWORD="$(head -c 18 /dev/urandom | base64 | tr -dc 'A-Za-z0-9' | head -c 16)"
fi
# 面板只绑本地:要么经 nginx 子域进来,要么 SSH 隧道进来
run_sh "/usr/local/x-ui/x-ui setting -port ${PANEL_PORT} -username admin \
-password '${PANEL_PASSWORD}' -listenIP 127.0.0.1"
run_sh "systemctl daemon-reload && systemctl enable x-ui >/dev/null 2>&1 && systemctl restart x-ui"
if [ "$DRY" = 0 ]; then
local i
for i in 1 2 3 4 5 6 7 8 9 10; do
port_in_use "$PANEL_PORT" && break
sleep 1
done
port_in_use "$PANEL_PORT" || die "面板没起来,看 journalctl -u x-ui -n 40"
ok "面板已在 127.0.0.1:${PANEL_PORT} 监听"
fi
}
# ─── 步骤 3:建两条入站 ────────────────────────────────────────────────────
api() { python3 "$API_PY" --base "http://127.0.0.1:${PANEL_PORT}" --password "$PANEL_PASSWORD" "$@"; }
step_inbounds() {
say ""
say "[3/6] 创建两条 VLESS+WS 入站(各绑 127.0.0.1)"
run_sh "python3 '${API_PY}' --base 'http://127.0.0.1:${PANEL_PORT}' --password '${PANEL_PASSWORD}' \
add --remark ${REMARK_A} --port ${PORT_A} --path '${PATH_A}' --email '${OWNER_EMAIL}'"
run_sh "python3 '${API_PY}' --base 'http://127.0.0.1:${PANEL_PORT}' --password '${PANEL_PASSWORD}' \
add --remark ${REMARK_B} --port ${PORT_B} --path '${PATH_B}' --email '${OWNER_EMAIL}'"
ok "${REMARK_A} → 127.0.0.1:${PORT_A}${PATH_A} ${REMARK_B} → 127.0.0.1:${PORT_B}${PATH_B}"
if [ "$DRY" = 0 ]; then
api list | sed 's/^/ /'
fi
}
# ─── 步骤 4/5:nginx + 证书 ────────────────────────────────────────────────
# write_http_vhost 域名 [路径] [端口] —— 有路径就给 WS,没路径就只做 ACME + 跳转
write_http_vhost() {
local domain="$1" path="${2:-}" port="${3:-}"
local root="${WEBROOT_BASE}/${domain}" f="${VHOST_DIR}/${domain}.conf"
local ws=""
if [ -n "$path" ]; then
ws=$(cat <<WS
location ${path} {
if (\$http_upgrade = "") { return 404; }
proxy_http_version 1.1;
proxy_set_header Upgrade \$http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host \$http_host;
proxy_set_header X-Real-IP \$remote_addr;
proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for;
proxy_read_timeout 3600s;
proxy_send_timeout 3600s;
proxy_pass http://127.0.0.1:${port};
}
WS
)
fi
[ -n "$path" ] && [ "$DRY" = 1 ] && dim " [dry-run] 写入 ${f}(含 ${path} → 127.0.0.1:${port} 的 WS 反代)" && return 0
[ -z "$path" ] && [ "$DRY" = 1 ] && dim " [dry-run] 写入 ${f}(仅 ACME 验证目录 + 跳 443)" && return 0
[ "$DRY" = 1 ] || mkdir -p "$root"
backup_file "$f"
cat > "$f" <<CONF
server
{
listen 80;
server_name ${domain};
root ${root};
access_log ${LOG_DIR}/${domain}.log;
error_log ${LOG_DIR}/${domain}.error.log;
location ^~ /.well-known/acme-challenge/ {
default_type text/plain;
root ${root};
}
${ws}
location / { return 301 https://\$host\$request_uri; }
}
CONF
say " 写入 ${f}"
}
# write_https_vhost 域名 [路径] [端口] [basic-auth 文件]
write_https_vhost() {
local domain="$1" path="${2:-}" port="${3:-}" authfile="${4:-}"
local root="${WEBROOT_BASE}/${domain}" cert="${CERT_DIR}/${domain}" f="${VHOST_DIR}/${domain}.conf"
local block=""
if [ -n "$path" ]; then
block=$(cat <<BK
location ${path} {
if (\$http_upgrade = "") { return 404; }
proxy_http_version 1.1;
proxy_set_header Upgrade \$http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host \$http_host;
proxy_set_header X-Real-IP \$remote_addr;
proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for;
proxy_read_timeout 3600s;
proxy_send_timeout 3600s;
proxy_pass http://127.0.0.1:${port};
}
location / { try_files \$uri \$uri/ =404; }
BK
)
else
block=$(cat <<BK
location / {
auth_basic "panel";
auth_basic_user_file ${authfile};
proxy_http_version 1.1;
proxy_set_header Upgrade \$http_upgrade;
proxy_set_header Connection \$http_connection;
proxy_set_header Host \$host;
proxy_set_header X-Real-IP \$remote_addr;
proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto \$scheme;
proxy_read_timeout 600s;
proxy_pass http://127.0.0.1:${PANEL_PORT};
}
BK
)
fi
if [ "$DRY" = 1 ]; then
if [ -n "$path" ]; then
dim " [dry-run] 写入 ${f}(443 + ${path} → 127.0.0.1:${port},证书 ${cert})"
else
dim " [dry-run] 写入 ${f}(443 + 面板反代 → 127.0.0.1:${PANEL_PORT},Basic Auth ${authfile})"
fi
return 0
fi
backup_file "$f"
cat > "$f" <<CONF
server
{
listen 80;
server_name ${domain};
root ${root};
location ^~ /.well-known/acme-challenge/ { default_type text/plain; root ${root}; }
location / { return 301 https://\$host\$request_uri; }
}
server
{
listen 443 ssl http2;
server_name ${domain};
root ${root};
access_log ${LOG_DIR}/${domain}.log;
error_log ${LOG_DIR}/${domain}.error.log;
ssl_certificate ${cert}/fullchain.pem;
ssl_certificate_key ${cert}/privkey.pem;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-CHACHA20-POLY1305:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-RSA-AES128-GCM-SHA256;
ssl_prefer_server_ciphers on;
ssl_session_cache shared:SSL_${domain//[.-]/_}:10m;
ssl_session_timeout 1d;
location ^~ /.well-known/acme-challenge/ { default_type text/plain; root ${root}; }
${block}
}
CONF
say " 写入 ${f}"
}
step_nginx_http() {
say ""
say "[4/6] 建 80 端 vhost 并签发证书"
write_http_vhost "$DOMAIN_A" "$PATH_A" "$PORT_A"
write_http_vhost "$DOMAIN_B" "$PATH_B" "$PORT_B"
if [ -n "$PANEL_DOMAIN" ]; then write_http_vhost "$PANEL_DOMAIN"; fi
nginx_apply
ok "80 已生效(ACME 验证目录就绪)"
if [ "$DRY" = 0 ] && [ ! -f /root/.acme.sh/acme.sh ]; then
run_sh "curl -fsSL https://get.acme.sh | sh -s email=${ACME_EMAIL:-admin@${DOMAIN_A}} >/dev/null 2>&1"
run_sh "/root/.acme.sh/acme.sh --set-default-ca --server letsencrypt >/dev/null 2>&1"
ok "acme.sh 已安装"
fi
[ "$DRY" = 1 ] && dim " [dry-run] acme.sh --issue -w ${WEBROOT_BASE}/<域名> -d <域名> --keylength ec-256"
local d
for d in "$DOMAIN_A" "$DOMAIN_B" $PANEL_DOMAIN; do
[ -z "$d" ] && continue
run_sh "/root/.acme.sh/acme.sh --issue -d ${d} -w ${WEBROOT_BASE}/${d} --keylength ec-256 >/tmp/acme-${d}.log 2>&1"
if [ "$DRY" = 0 ] && [ ! -f "/root/.acme.sh/${d}_ecc/${d}.cer" ]; then
tail -6 "/tmp/acme-${d}.log"; die "${d} 证书签发失败(80 端口要能回源到本机 80)"
fi
run_sh "/root/.acme.sh/acme.sh --install-cert -d ${d} --ecc \
--key-file ${CERT_DIR}/${d}/privkey.pem \
--fullchain-file ${CERT_DIR}/${d}/fullchain.pem \
--reloadcmd '${NGINX_BIN} -s reload' >/dev/null 2>&1"
ok "${d} 证书就绪 → ${CERT_DIR}/${d}"
done
}
step_nginx_https() {
say ""
say "[5/6] 切到 443"
write_https_vhost "$DOMAIN_A" "$PATH_A" "$PORT_A"
write_https_vhost "$DOMAIN_B" "$PATH_B" "$PORT_B"
if [ -n "$PANEL_DOMAIN" ]; then
local root="${WEBROOT_BASE}/${PANEL_DOMAIN}" passfile
passfile="${root}/.panel-pass"
local bpw; bpw="$(head -c 12 /dev/urandom | base64 | tr -dc 'A-Za-z0-9' | head -c 10)"
if [ "$DRY" = 1 ]; then
dim " [dry-run] printf 'ops:%s\\n' \"\$(openssl passwd -apr1 '******')\" > ${passfile}"
else
mkdir -p "$root"
printf 'ops:%s\n' "$(openssl passwd -apr1 "$bpw")" > "$passfile"
chown "$WEB_USER":"$WEB_USER" "$passfile"
chmod 640 "$passfile"
fi
PANEL_BASIC_USER="ops"; PANEL_BASIC_PASS="$bpw"
write_https_vhost "$PANEL_DOMAIN" "" "" "$passfile"
ok "面板子域 ${PANEL_DOMAIN} 已配好(Basic Auth 用户 ops / ${bpw})"
fi
nginx_apply
ok "443 已生效"
}
# ─── 步骤 6:验收 ──────────────────────────────────────────────────────────
code_of() { # code_of URL [额外 curl 参数...]
local url="$1"; shift
curl -s -i --http1.1 --max-time 20 "$@" "$url" 2>/dev/null | grep -oE 'HTTP/1.1 [0-9]{3}' | head -1
}
step_verify() {
say ""
say "[6/6] 验收"
if [ "$DRY" = 1 ]; then dim " [dry-run] 跳过实测"; return 0; fi
local d p c
for d in "$DOMAIN_A" "$DOMAIN_B"; do
if [ "$d" = "$DOMAIN_A" ]; then p="$PATH_A"; else p="$PATH_B"; fi
c="$(code_of "https://${d}${p}" -H 'Upgrade: websocket' -H 'Connection: Upgrade' \
-H 'Sec-WebSocket-Version: 13' -H 'Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==')"
case "$c" in
*101*) ok "${d}${p} → ${c}(WebSocket 已通)" ;;
*) warn "${d}${p} → ${c:-无响应}(期望 101,看 ${VHOST_DIR}/${d}.conf 与 nginx error log)" ;;
esac
c="$(code_of "https://${d}${p}")"
[ "$c" = "HTTP/1.1 404" ] && ok "${d}${p} 不带 Upgrade 时返回 404(探针伪装,符合预期)" \
|| warn "${d}${p} 不带 Upgrade 返回 ${c:-无响应}(期望 404)"
done
if [ -n "$PANEL_DOMAIN" ]; then
c="$(curl -s -o /dev/null -w '%{http_code}' --max-time 15 "https://${PANEL_DOMAIN}/" 2>/dev/null)"
[ "$c" = "401" ] && ok "面板子域未认证返回 401(Basic Auth 生效)" \
|| warn "面板子域返回 ${c:-无响应},期望 401"
c="$(code_of "https://${PANEL_DOMAIN}/" -u "ops:${PANEL_BASIC_PASS}")"
[ "$c" = "HTTP/1.1 200" ] && ok "面板子域带认证返回 200" || warn "面板子域带认证返回 ${c:-无响应}"
fi
}
# ─── 收尾 ──────────────────────────────────────────────────────────────────
step_summary() {
say ""
say "─── 装好了 ──────────────────────────────────────────────"
if [ -n "$PANEL_DOMAIN" ]; then
say "面板: https://${PANEL_DOMAIN}/ 外层 ${PANEL_BASIC_USER} / ${PANEL_BASIC_PASS} 内层 admin / ${PANEL_PASSWORD}"
else
say "面板: ssh -L 2053:127.0.0.1:${PANEL_PORT} root@<本机IP> → 本地开 http://127.0.0.1:2053"
say " 账号 admin / ${PANEL_PASSWORD}"
fi
say "入站: ${REMARK_A} 127.0.0.1:${PORT_A}${PATH_A}(→ ${DOMAIN_A}) ${REMARK_B} 127.0.0.1:${PORT_B}${PATH_B}(→ ${DOMAIN_B})"
say ""
if [ "$DRY" = 1 ]; then
dim " [dry-run] 真实链接在装完后用下面命令取:"
dim " python3 ${API_PY} --password '<面板密码>' link --inbound ${REMARK_A} --email ${OWNER_EMAIL} --domain ${DOMAIN_A}"
hr; return 0
fi
say "你的两条链接(可直接导入客户端):"
api link --inbound "$REMARK_A" --email "$OWNER_EMAIL" --domain "$DOMAIN_A" --fragment "${REMARK_A}-橙云" 2>/dev/null \
| sed 's/^/ A /'
api link --inbound "$REMARK_B" --email "$OWNER_EMAIL" --domain "$DOMAIN_B" --fragment "${REMARK_B}-灰云" 2>/dev/null \
| sed 's/^/ B /'
say ""
say "给新用户开号:python3 ${API_PY} --password '<面板密码>' add-user --email <名字>"
say "取某人某条链路的链接:… link --inbound ${REMARK_A} --email <名字> --domain ${DOMAIN_A}"
say "销号: python3 ${API_PY} --password '<面板密码>' del-user --email <名字>"
hr
}
# ─── 主流程 ────────────────────────────────────────────────────────────────
main() {
while [ $# -gt 0 ]; do
case "$1" in
--dry-run) DRY=1; shift ;;
--yes|-y) ASSUME=1; shift ;;
--domain-a) DOMAIN_A="$2"; shift 2 ;;
--domain-b) DOMAIN_B="$2"; shift 2 ;;
--path-a) PATH_A="$2"; shift 2 ;;
--path-b) PATH_B="$2"; shift 2 ;;
--remark-a) REMARK_A="$2"; shift 2 ;;
--remark-b) REMARK_B="$2"; shift 2 ;;
--port-a) PORT_A="$2"; shift 2 ;;
--port-b) PORT_B="$2"; shift 2 ;;
--panel-port) PANEL_PORT="$2"; shift 2 ;;
--panel-domain) PANEL_DOMAIN="$2"; shift 2 ;;
--panel-password) PANEL_PASSWORD="$2"; shift 2 ;;
--owner) OWNER_EMAIL="$2"; shift 2 ;;
--acme-email) ACME_EMAIL="$2"; shift 2 ;;
-h|--help) usage ;;
*) die "未知参数:$1(--help 看用法)" ;;
esac
done
say "双链路代理安装向导(Ctrl+C 退出)"
say "本脚本只做 nginx 转发 + 3x-ui 面板这一种架构。"
[ -z "$DOMAIN_A" ] && ask DOMAIN_A "橙云链路域名(走 CF 代理)" ""
[ -z "$DOMAIN_B" ] && ask DOMAIN_B "灰云链路域名(A 记录直连)" ""
ask PATH_A "链路 A 的 WebSocket 路径" "$PATH_A"
ask PATH_B "链路 B 的 WebSocket 路径" "$PATH_B"
ask PORT_A "链路 A 的后端本地端口" "$PORT_A"
ask PORT_B "链路 B 的后端本地端口" "$PORT_B"
ask PANEL_PORT "3x-ui 面板端口(只绑 127.0.0.1)" "$PANEL_PORT"
ask PANEL_DOMAIN "面板子域名(留空=只走 SSH 隧道)" "$PANEL_DOMAIN"
ask OWNER_EMAIL "首个账号标识(你自己)" "$OWNER_EMAIL"
say ""
say "将执行:"
say " ${DOMAIN_A} → 127.0.0.1:${PORT_A}${PATH_A}"
say " ${DOMAIN_B} → 127.0.0.1:${PORT_B}${PATH_B}"
say " 面板 127.0.0.1:${PANEL_PORT}$([ -n "$PANEL_DOMAIN" ] && echo " → https://${PANEL_DOMAIN}"),首账号 ${OWNER_EMAIL}"
[ "$DRY" = 1 ] && say " (dry-run:只打印不执行)"
if [ "$ASSUME" = 0 ] && [ -t 0 ]; then
local go; read -r -p " 确认开始?[y/N] " go || true
[ "$go" = y ] || [ "$go" = Y ] || { say "已取消"; exit 0; }
fi
step_preflight
step_backend
step_inbounds
step_nginx_http
step_nginx_https
step_verify
step_summary
}
if [ "${BASH_SOURCE[0]}" = "$0" ]; then main "$@"; fi