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

536 lines
22 KiB
Bash
Executable File
Raw 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
# 双链路代理一键安装(交互确认 / 可用 --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