#!/bin/bash if [ -z "$BASH_VERSION" ]; then exec bash "$0" "$@" fi set -e RED='\033[0;31m' GREEN='\033[0;32m' YELLOW='\033[1;33m' BLUE='\033[0;34m' PURPLE='\033[0;35m' CYAN='\033[0;36m' NC='\033[0m' BOLD='\033[1m' log_title() { echo -e "\n${PURPLE}${BOLD}==========================================${NC}"; echo -e " ${CYAN}${BOLD}$1${NC}"; echo -e "${PURPLE}${BOLD}==========================================${NC}"; } log_info() { echo -e "${BLUE}[INFO]${NC} $1"; } log_succ() { echo -e "${GREEN}[SUCCESS]${NC} $1"; } log_warn() { echo -e "${YELLOW}[WARN]${NC} $1"; } log_err() { echo -e "${RED}[ERROR]${NC} $1"; } CORRECT_KEY="urTMjh522lnFkJwOt7anJMvhwdp0KQhk" PROVIDED_KEY="" if [ -z "$PROVIDED_KEY" ]; then echo -e -n "${YELLOW}请输入授权 Key: ${NC}" read -s PROVIDED_KEY echo "" fi if [ "$PROVIDED_KEY" != "$CORRECT_KEY" ]; then log_err "密钥无效,拒绝执行!" exit 1 fi LOCAL_IP=$(hostname -I | awk '{print $1}') # 清洗实例名称(去中划线、转小写、保留字母数字下划线) sanitize_name() { echo "$1" | tr 'A-Z' 'a-z' | tr '-' '_' | sed 's/[^a-z0-9_]//g' } prepare_env() { log_info "正在检查并初始化基础环境..." sudo systemctl restart systemd-timesyncd 2>/dev/null || true if ! command -v docker &> /dev/null; then log_info "正在安装 Docker 引擎..." curl -fsSL https://get.docker.com | bash -s docker --mirror Aliyun || true sudo systemctl enable docker || true sudo systemctl start docker || true fi sudo mkdir -p /etc/docker cat < /dev/null { "registry-mirrors": [ "https://docker.1ms.run", "https://docker.aipub.pro", "https://docker.hpcloud.cloud" ] } EOF sudo systemctl daemon-reload 2>/dev/null || true sudo systemctl restart docker 2>/dev/null || true export DEBIAN_FRONTEND=noninteractive sudo apt-get update -y >/dev/null 2>&1 && sudo apt-get install -y sshpass jq net-tools >/dev/null 2>&1 || true if [ ! "$(sudo docker ps -q -f name=portainer 2>/dev/null)" ]; then log_info "启动 Portainer 管理面板..." sudo docker volume create portainer_data || true sudo docker run -d \ -p 0.0.0.0:9000:9000 \ -p 0.0.0.0:9443:9443 \ --name portainer \ --restart=always \ -v /var/run/docker.sock:/var/run/docker.sock \ -v portainer_data:/var/lib/docker/volumes/portainer_data/_data \ portainer/portainer-ce:latest >/dev/null 2>&1 || true fi } scan_status() { log_title "已安装组件与 RustDesk 实例状态汇总" echo -e "${BOLD}${CYAN}[1] Docker 引擎状态:${NC}" if command -v docker &>/dev/null && sudo docker info &>/dev/null; then echo -e " - Docker: ${GREEN}运行中${NC}" else echo -e " - Docker: ${RED}未运行${NC}" fi echo -e "\n${BOLD}${CYAN}[2] Portainer 图形面板:${NC}" if [ "$(sudo docker ps -q -f name=portainer 2>/dev/null)" ]; then echo -e " - 状态: ${GREEN}运行中${NC}" echo -e " - HTTP 地址: http://${LOCAL_IP}:9000" echo -e " - HTTPS 地址: https://${LOCAL_IP}:9443" else echo -e " - 状态: ${RED}未启动${NC}" fi echo -e "\n${BOLD}${CYAN}[3] RustDesk 服务端实例列表:${NC}" INST_DIRS=($(sudo find /opt/rustdesk -mindepth 1 -maxdepth 1 -type d 2>/dev/null || true)) if [ ${#INST_DIRS[@]} -eq 0 ]; then echo -e " ${YELLOW}(暂未检测到任何已部署的 RustDesk 实例)${NC}" return fi for dir in "${INST_DIRS[@]}"; do INST_NAME=$(basename "$dir") CONFIG_FILE="$dir/env_config.env" PUBLIC_HOST="未知" BASE_PORT="21115" if [ -f "$CONFIG_FILE" ]; then source "$CONFIG_FILE" fi P_21115=${BASE_PORT} P_21116=$((BASE_PORT + 1)) P_21117=$((BASE_PORT + 2)) P_21118=$((BASE_PORT + 3)) P_21119=$((BASE_PORT + 4)) PUB_KEY="无法读取 Key" if [ -s "$dir/id_ed25519.pub" ]; then sudo chmod 644 "$dir/id_ed25519.pub" 2>/dev/null || true PUB_KEY=$(sudo cat "$dir/id_ed25519.pub" 2>/dev/null) fi HBBS_STATUS="${RED}已停止${NC}" if [ "$(sudo docker ps -q -f name=${INST_NAME}_hbbs 2>/dev/null)" ] || [ "$(sudo docker ps -q -f name=${INST_NAME}-hbbs 2>/dev/null)" ]; then HBBS_STATUS="${GREEN}运行中${NC}" fi echo -e " ${GREEN}► 实例名称:${NC} ${BOLD}${INST_NAME}${NC} [状态: $HBBS_STATUS]" echo -e " - 绑定域名/IP: $PUBLIC_HOST" echo -e " - ID Server : $PUBLIC_HOST:$P_21116" echo -e " - Relay Server: $PUBLIC_HOST:$P_21117" echo -e " - Public Key : $PUB_KEY" echo -e " - 占用端口组 : $P_21115, $P_21116, $P_21117, $P_21118, $P_21119" echo "" done } deploy_instance() { local RAW_NAME="$1" local PUBLIC_HOST="$2" local BASE_PORT="$3" local CUSTOM_KEY="$4" local INST_NAME=$(sanitize_name "$RAW_NAME") DATA_DIR="/opt/rustdesk/$INST_NAME" sudo mkdir -p "$DATA_DIR" cat < /dev/null PUBLIC_HOST="${PUBLIC_HOST}" BASE_PORT="${BASE_PORT}" EOF P_21115=$BASE_PORT P_21116=$((BASE_PORT + 1)) P_21117=$((BASE_PORT + 2)) P_21118=$((BASE_PORT + 3)) P_21119=$((BASE_PORT + 4)) if [ -n "$CUSTOM_KEY" ]; then echo -n "$CUSTOM_KEY" | sudo tee "$DATA_DIR/id_ed25519.pub" >/dev/null fi # 清理旧容器 sudo docker rm -f "${INST_NAME}_hbbs" "${INST_NAME}_hbbr" "${INST_NAME}-hbbs" "${INST_NAME}-hbbr" 2>/dev/null || true log_info "正在启动/重构 ${INST_NAME}..." sudo docker run -d \ --name "${INST_NAME}_hbbs" \ --restart=always \ -v "$DATA_DIR:/root" \ -w /root \ -p 0.0.0.0:${P_21115}:21115 \ -p 0.0.0.0:${P_21116}:21116 \ -p 0.0.0.0:${P_21116}:21116/udp \ -p 0.0.0.0:${P_21118}:21118 \ rustdesk/rustdesk-server:latest hbbs -r ${PUBLIC_HOST}:${P_21117} -4 >/dev/null sudo docker run -d \ --name "${INST_NAME}_hbbr" \ --restart=always \ -v "$DATA_DIR:/root" \ -w /root \ -p 0.0.0.0:${P_21117}:21117 \ -p 0.0.0.0:${P_21119}:21119 \ rustdesk/rustdesk-server:latest hbbr -4 >/dev/null log_info "正在等待 RustDesk 生成公私钥对..." for i in {1..6}; do if [ -s "$DATA_DIR/id_ed25519.pub" ]; then break fi sleep 1 done if [ -s "$DATA_DIR/id_ed25519.pub" ]; then sudo chmod 644 "$DATA_DIR/id_ed25519.pub" 2>/dev/null || true GENERATED_KEY=$(sudo cat "$DATA_DIR/id_ed25519.pub") log_succ "实例 ${INST_NAME} 部署完成!" log_succ "已成功获取 Public Key: ${GENERATED_KEY}" else log_succ "实例 ${INST_NAME} 部署完成!(密钥生成稍有延迟,可在主菜单 [1] 中随时查看)" fi } sync_openwrt() { echo -e -n "${YELLOW}是否配置/同步 OpenWrt 防火墙端口转发 (DNAT)? (y/N): ${NC}" read DO_OP if [[ ! "$DO_OP" =~ ^[Yy]$ ]]; then return fi echo -e -n " 请输入 OpenWrt 路由器 IP [默认 192.168.1.1]: " read OP_IP OP_IP=${OP_IP:-"192.168.1.1"} echo -e -n " 请输入 OpenWrt root 密码: " read -s OP_PASS echo "" log_info "正在连接 OpenWrt (${OP_IP}) 并同步规则..." INST_DIRS=($(sudo find /opt/rustdesk -mindepth 1 -maxdepth 1 -type d 2>/dev/null || true)) for dir in "${INST_DIRS[@]}"; do INST_NAME=$(basename "$dir") CONFIG_FILE="$dir/env_config.env" if [ -f "$CONFIG_FILE" ]; then source "$CONFIG_FILE" else continue fi P_21115=${BASE_PORT} P_21116=$((BASE_PORT + 1)) P_21117=$((BASE_PORT + 2)) P_21118=$((BASE_PORT + 3)) P_21119=$((BASE_PORT + 4)) SAFE_INST_NAME=$(sanitize_name "$INST_NAME") CMD_STR="" for p in ${P_21115} ${P_21116} ${P_21117} ${P_21118} ${P_21119}; do RULE_NAME="rd_${SAFE_INST_NAME}_${p}" CMD_STR+="uci delete firewall.${RULE_NAME} 2>/dev/null || true; " CMD_STR+="uci set firewall.${RULE_NAME}=redirect; " CMD_STR+="uci set firewall.${RULE_NAME}.name='${RULE_NAME}'; " CMD_STR+="uci set firewall.${RULE_NAME}.src='wan'; " CMD_STR+="uci set firewall.${RULE_NAME}.src_dport='${p}'; " CMD_STR+="uci set firewall.${RULE_NAME}.dest='lan'; " CMD_STR+="uci set firewall.${RULE_NAME}.dest_ip='${LOCAL_IP}'; " CMD_STR+="uci set firewall.${RULE_NAME}.dest_port='${p}'; " CMD_STR+="uci set firewall.${RULE_NAME}.proto='tcpudp'; " CMD_STR+="uci set firewall.${RULE_NAME}.target='DNAT'; " done CMD_STR+="uci commit firewall; /etc/init.d/firewall restart >/dev/null 2>&1;" sshpass -p "$OP_PASS" ssh -o StrictHostKeyChecking=no -o ConnectTimeout=5 root@$OP_IP "$CMD_STR" done log_succ "OpenWrt 防火墙端口转发规则一键同步成功!" } prepare_env while true; do log_title "RustDesk & Docker 管理与控制中心" echo -e " ${CYAN}1.${NC} 查看已部署服务与实例明细 (检测)" echo -e " ${CYAN}2.${NC} 新建/部署 RustDesk 实例" echo -e " ${CYAN}3.${NC} 修改现有 RustDesk 实例配置" echo -e " ${CYAN}4.${NC} 删除指定 RustDesk 实例" echo -e " ${CYAN}5.${NC} 一键同步端口转发至 OpenWrt" echo -e " ${CYAN}0.${NC} 退出脚本" echo -e "${PURPLE}==========================================${NC}" echo -e -n "${YELLOW}请选择操作选项 [0-5]: ${NC}" read CHOICE case "$CHOICE" in 1) scan_status ;; 2) echo -e -n "${YELLOW}实例名称 [默认: rustdesk_server_1]: ${NC}" read INPUT_NAME INPUT_NAME=${INPUT_NAME:-"rustdesk_server_1"} INST_NAME=$(sanitize_name "$INPUT_NAME") echo -e -n "${YELLOW}公网域名/IP [默认: $LOCAL_IP]: ${NC}" read PUBLIC_HOST PUBLIC_HOST=${PUBLIC_HOST:-"$LOCAL_IP"} echo -e -n "${YELLOW}起始端口 offset (推荐 21115, 31125 等): ${NC}" read BASE_PORT BASE_PORT=${BASE_PORT:-21115} echo -e -n "${YELLOW}自定义 Key (留空将自动生成): ${NC}" read CUSTOM_KEY deploy_instance "$INST_NAME" "$PUBLIC_HOST" "$BASE_PORT" "$CUSTOM_KEY" sync_openwrt ;; 3) scan_status echo -e -n "${YELLOW}请输入要修改的实例名称 (例如: rustdesk_server_1): ${NC}" read TARGET_INPUT TARGET_NAME=$(sanitize_name "$TARGET_INPUT") if [ ! -d "/opt/rustdesk/$TARGET_NAME" ]; then log_err "不存在名称为 $TARGET_NAME 的实例!" continue fi source "/opt/rustdesk/$TARGET_NAME/env_config.env" 2>/dev/null || true echo -e -n "${YELLOW}新公网域名/IP [当前: $PUBLIC_HOST]: ${NC}" read NEW_HOST NEW_HOST=${NEW_HOST:-"$PUBLIC_HOST"} echo -e -n "${YELLOW}新起始端口 [当前: $BASE_PORT]: ${NC}" read NEW_PORT NEW_PORT=${NEW_PORT:-"$BASE_PORT"} echo -e -n "${YELLOW}修改自定义 Key (直接回车保持现有 Key 不变): ${NC}" read NEW_KEY deploy_instance "$TARGET_NAME" "$NEW_HOST" "$NEW_PORT" "$NEW_KEY" sync_openwrt ;; 4) scan_status echo -e -n "${YELLOW}请输入要删除的实例名称: ${NC}" read DEL_INPUT DEL_NAME=$(sanitize_name "$DEL_INPUT") if [ -d "/opt/rustdesk/$DEL_NAME" ]; then sudo docker rm -f "${DEL_NAME}_hbbs" "${DEL_NAME}_hbbr" "${DEL_NAME}-hbbs" "${DEL_NAME}-hbbr" 2>/dev/null || true sudo rm -rf "/opt/rustdesk/$DEL_NAME" log_succ "实例 $DEL_NAME 已被彻底删除!" else log_err "未找到该实例。" fi ;; 5) sync_openwrt ;; 0) log_info "已安全退出。" exit 0 ;; *) log_err "无效选项,请重新选择。" ;; esac done