From c67f0cb0cff93c42781099583801f7978bc47db2 Mon Sep 17 00:00:00 2001 From: Muse Date: Tue, 20 Oct 2026 12:36:50 +0000 Subject: [PATCH] =?UTF-8?q?=E9=93=BE=E6=8E=A5=E8=BF=81=E7=A7=BB:=20fork=20?= =?UTF-8?q?=E8=84=9A=E6=9C=AC=E5=A4=8D=E6=B4=BB=E6=8C=87=20Gitea=E3=80=81?= =?UTF-8?q?=E4=B8=8A=E6=B8=B8=E4=B8=89=E4=BB=B6=E5=A5=97=E4=B8=AD=E5=92=8C?= =?UTF-8?q?=E3=80=81=E9=9D=A2=E6=9D=BF=E8=87=AA=E6=9B=B4=E6=96=B0=E9=93=BE?= =?UTF-8?q?=E6=94=B9=E6=8C=87=E6=9C=AC=E4=BB=93?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - install-master.sh/install-agent.sh: 二进制下载源由已封账号 aaaamoooo/3x-ui-diy 改指本仓 Gitea Releases (tag 沿用 v3.7.0-agent.1); README 同步撤下'暂不可用'标注 - install.sh/update.sh: 未被 fork 部署链引用, 整文件替换为中英双语短桩 (exit 1), 防旧习惯调用时静默装成/更新成上游原版 - x-ui.sh: 保留全部管理功能, 仅中和 install/update/update_dev/update_menu/ legacy_version/update_shell 的上游下载分支与 uninstall 打印的上游命令, 一律改为提示本仓 Gitea Releases 地址后退出该分支 - panel.go: 面板自更新运行时耦合处理 — panelUpdaterURL 与版本检查 API 由 MHSanaei GitHub 改指本仓 Gitea (raw update.sh + /api/v1 releases, 响应字段 tag_name/body/target_commitish 与原解析结构一致); 下载到的将是本仓 update.sh 短桩, 面板更新会安全失败而非覆盖成原版 - deploy/cloud-init: curl 上游 install.sh 改为本仓 install-master.sh (Gitea raw); 头部注明 XUI_* 非交互旋钮与 install-result.env 凭据回显在 fork 安装脚本下无等价物 --- README.md | 2 +- deploy/cloud-init/cloud-init.yaml | 9 +- install-agent.sh | 4 +- install-master.sh | 4 +- install.sh | 1764 +-------------------------- internal/web/service/panel/panel.go | 16 +- update.sh | 1241 +------------------ x-ui.sh | 93 +- 8 files changed, 87 insertions(+), 3046 deletions(-) diff --git a/README.md b/README.md index 5941fb9..3fcfc40 100644 --- a/README.md +++ b/README.md @@ -15,7 +15,7 @@ 二进制下载:本仓库 [Releases](https://git.881218.xyz/rogue/3x-ui-agent/releases),按架构选 `x-ui-linux-amd64` 或 `x-ui-linux-arm64`。 -> 仓库根目录的 `install-master.sh` / `install-agent.sh` 是早期一键脚本,其下载源仍指向已下线的 GitHub 账号,**暂不可用**;请按下面步骤手动部署。 +> 仓库根目录的 `install-master.sh` / `install-agent.sh` 是一键脚本,下载源已迁至本仓 Gitea Releases(tag `v3.7.0-agent.1`),可直接使用;也可按下面步骤手动部署。 ### 主控 diff --git a/deploy/cloud-init/cloud-init.yaml b/deploy/cloud-init/cloud-init.yaml index 05ee07a..dbedcea 100644 --- a/deploy/cloud-init/cloud-init.yaml +++ b/deploy/cloud-init/cloud-init.yaml @@ -10,8 +10,11 @@ # serial console (owner-only); /etc/motd (world-readable) shows just the access # URL + username. Nothing is baked in advance — every instance is unique. # -# Requires the non-interactive install.sh (3x-ui with XUI_NONINTERACTIVE support). -# Edit the exported XUI_* knobs in /opt/xui-bootstrap.sh below to customise. +# Fork note (3x-ui-agent): the bootstrap below installs this fork's +# install-master.sh from its own Gitea repository. The fork installer does +# NOT consume the XUI_* non-interactive knobs and does NOT write +# /etc/x-ui/install-result.env, so the XUI_* exports and the credential- +# surfacing block below are inert leftovers from the upstream installer. # --------------------------------------------------------------------------- package_update: true @@ -44,7 +47,7 @@ write_files: # export XUI_DB_DSN="postgres://user:pass@host:5432/db?sslmode=disable" # ------------------------------------------------------------------------ - curl -fsSL https://raw.githubusercontent.com/MHSanaei/3x-ui/main/install.sh | bash + curl -fsSL https://git.881218.xyz/rogue/3x-ui-agent/raw/branch/main/install-master.sh | bash # Surface the generated credentials. Full creds (incl. password + API token) # go ONLY to the serial console (/dev/console, owner-only). /etc/motd is diff --git a/install-agent.sh b/install-agent.sh index aa8e006..60fe9a2 100755 --- a/install-agent.sh +++ b/install-agent.sh @@ -31,8 +31,8 @@ case "$ARCH" in *) error "不支持的架构: $ARCH (仅支持 amd64/arm64)";; esac -# 二进制下载地址(GitHub release,替换为实际地址) -XUI_URL="${XUI_URL:-https://github.com/aaaamoooo/3x-ui-diy/releases/download/v3.7.0-agent.1/x-ui-linux-${BIN_ARCH}}" +# 二进制下载地址(本仓 Gitea Releases,tag 沿用 v3.7.0-agent.1) +XUI_URL="${XUI_URL:-https://git.881218.xyz/rogue/3x-ui-agent/releases/download/v3.7.0-agent.1/x-ui-linux-${BIN_ARCH}}" XRAY_VER="26.6.27" info "创建目录..." diff --git a/install-master.sh b/install-master.sh index 2de67df..6546f57 100755 --- a/install-master.sh +++ b/install-master.sh @@ -18,8 +18,8 @@ case "$ARCH" in *) error "不支持的架构: $ARCH (仅支持 amd64/arm64)";; esac -# 二进制下载地址(GitHub release,替换为实际地址) -XUI_URL="${XUI_URL:-https://github.com/aaaamoooo/3x-ui-diy/releases/download/v3.7.0-agent.1/x-ui-linux-${BIN_ARCH}}" +# 二进制下载地址(本仓 Gitea Releases,tag 沿用 v3.7.0-agent.1) +XUI_URL="${XUI_URL:-https://git.881218.xyz/rogue/3x-ui-agent/releases/download/v3.7.0-agent.1/x-ui-linux-${BIN_ARCH}}" XRAY_VER="26.6.27" info "创建目录..." diff --git a/install.sh b/install.sh index 4ff60a0..eedc8a8 100644 --- a/install.sh +++ b/install.sh @@ -1,1757 +1,9 @@ #!/bin/bash - -red='\033[0;31m' -green='\033[0;32m' -blue='\033[0;34m' -yellow='\033[0;33m' -plain='\033[0m' - -xui_folder="${XUI_MAIN_FOLDER:=/usr/local/x-ui}" -xui_service="${XUI_SERVICE:=/etc/systemd/system}" - -# check root -[[ $EUID -ne 0 ]] && echo -e "${red}Fatal error: ${plain} Please run this script with root privilege \n " && exit 1 - -# Check OS and set release variable -if [[ -f /etc/os-release ]]; then - source /etc/os-release - release=$ID -elif [[ -f /usr/lib/os-release ]]; then - source /usr/lib/os-release - release=$ID -else - echo "Failed to check the system OS, please contact the author!" >&2 - exit 1 -fi -echo "The OS release is: $release" - -arch() { - case "$(uname -m)" in - x86_64 | x64 | amd64) echo 'amd64' ;; - i*86 | x86) echo '386' ;; - armv8* | armv8 | arm64 | aarch64) echo 'arm64' ;; - armv7* | armv7 | arm) echo 'armv7' ;; - armv6* | armv6) echo 'armv6' ;; - armv5* | armv5) echo 'armv5' ;; - s390x) echo 's390x' ;; - *) echo -e "${green}Unsupported CPU architecture! ${plain}" && rm -f "$(realpath "$0")" && exit 1 ;; - esac -} - -echo "Arch: $(arch)" - -# Non-interactive mode: triggered explicitly via XUI_NONINTERACTIVE=1, or -# implicitly when stdin is not a TTY (e.g. `curl ... | bash`, cloud-init). -# In this mode every prompt below is replaced by an env var or a sane default. -if [[ "${XUI_NONINTERACTIVE:-0}" == "1" ]] || [[ ! -t 0 ]]; then - NONINTERACTIVE=1 -else - NONINTERACTIVE=0 -fi -export NONINTERACTIVE - -# Simple helpers -is_ipv4() { - [[ "$1" =~ ^([0-9]{1,3}\.){3}[0-9]{1,3}$ ]] && return 0 || return 1 -} -is_ipv6() { - [[ "$1" =~ : ]] && return 0 || return 1 -} -is_ip() { - is_ipv4 "$1" || is_ipv6 "$1" -} -is_domain() { - [[ "$1" =~ ^([A-Za-z0-9](-*[A-Za-z0-9])*\.)+(xn--[a-z0-9]{2,}|[A-Za-z]{2,})$ ]] && return 0 || return 1 -} - -# acme.sh's standalone server binds IPv4 by default; --listen-v6 makes it -# v6-only, which breaks HTTP-01 validation when the domain's A record points -# at this host's IPv4 (#4994). Only force IPv6 when the host has no global -# IPv4 address at all. -acme_listen_flag() { - if ip -4 addr show scope global 2> /dev/null | grep -q "inet "; then - echo "" - else - echo "--listen-v6" - fi -} - -# Port helpers -is_port_in_use() { - local port="$1" - if command -v ss > /dev/null 2>&1; then - ss -ltn 2> /dev/null | awk -v p=":${port}$" '$4 ~ p {exit 0} END {exit 1}' - return - fi - if command -v netstat > /dev/null 2>&1; then - netstat -lnt 2> /dev/null | awk -v p=":${port} " '$4 ~ p {exit 0} END {exit 1}' - return - fi - if command -v lsof > /dev/null 2>&1; then - lsof -nP -iTCP:${port} -sTCP:LISTEN > /dev/null 2>&1 && return 0 - fi - return 1 -} - -install_base() { - case "${release}" in - ubuntu | debian | armbian) - apt-get update && apt-get install -y -q cron curl tar tzdata socat ca-certificates openssl - ;; - fedora | amzn | virtuozzo | rhel | almalinux | rocky | ol) - dnf makecache -y && dnf install -y -q cronie curl tar tzdata socat ca-certificates openssl - ;; - centos) - if [[ "${VERSION_ID}" =~ ^7 ]]; then - yum makecache -y && yum install -y cronie curl tar tzdata socat ca-certificates openssl - else - dnf makecache -y && dnf install -y -q cronie curl tar tzdata socat ca-certificates openssl - fi - ;; - arch | manjaro | parch) - pacman -Sy --noconfirm cronie curl tar tzdata socat ca-certificates openssl - ;; - opensuse-tumbleweed | opensuse-leap) - zypper refresh && zypper -q install -y cron curl tar timezone socat ca-certificates openssl - ;; - alpine) - apk update && apk add dcron curl tar tzdata socat ca-certificates openssl - ;; - *) - apt-get update && apt-get install -y -q cron curl tar tzdata socat ca-certificates openssl - ;; - esac -} - -gen_random_string() { - local length="$1" - openssl rand -base64 $((length * 2)) \ - | tr -dc 'a-zA-Z0-9' \ - | head -c "$length" -} - -# prompt_or_default VARNAME "prompt text" "default" [ENV_NAME] -# Interactive: read into VARNAME. Non-interactive: VARNAME = ${ENV_NAME:-default}. -# ENV_NAME defaults to VARNAME when omitted. Keeps every interactive prompt -# string byte-for-byte identical to the original `read -rp`. -prompt_or_default() { - local __var="$1" __prompt="$2" __default="$3" __env="${4:-$1}" - if [[ "$NONINTERACTIVE" == "1" ]]; then - printf -v "$__var" '%s' "${!__env:-$__default}" - else - # shellcheck disable=SC2229 - read -rp "$__prompt" "$__var" - fi -} - -# write_install_result -# Persists a parseable, root-only credentials file consumed by cloud-init/MOTD. -# Values are written with printf '%q' so a pinned password/username containing -# spaces, quotes, $(...) or backticks is shell-escaped and the file stays safely -# source-able (consumers do '. install-result.env'). For the alphanumeric random -# values gen_random_string emits, %q is a no-op. This is a DIFFERENT file from the -# Postgres env file (/etc/default/x-ui). -write_install_result() { - local u="$1" p="$2" port="$3" wbp="$4" scheme="$5" host="$6" token="$7" dbtype="$8" - local result_file="/etc/x-ui/install-result.env" - local url_host="${host:-SERVER_IP_UNKNOWN}" - install -d -m 755 /etc/x-ui 2> /dev/null - local prev_umask - prev_umask=$(umask) - umask 077 - if ! { - printf 'XUI_USERNAME=%q\n' "$u" - printf 'XUI_PASSWORD=%q\n' "$p" - printf 'XUI_PANEL_PORT=%q\n' "$port" - printf 'XUI_WEB_BASE_PATH=%q\n' "$wbp" - printf 'XUI_ACCESS_URL=%q\n' "${scheme}://${url_host}:${port}/${wbp}" - printf 'XUI_API_TOKEN=%q\n' "$token" - printf 'XUI_DB_TYPE=%q\n' "$dbtype" - } > "$result_file"; then - umask "$prev_umask" - echo -e "${yellow}Warning: failed to write ${result_file}.${plain}" >&2 - return 1 - fi - umask "$prev_umask" - chmod 600 "$result_file" 2> /dev/null - chown root:root "$result_file" 2> /dev/null || true - echo -e "${green}Install result written to ${result_file} (mode 600).${plain}" -} - -# RHEL-family initdb writes pg_hba.conf host rules with ident auth, which -# compares the OS username against the Postgres role and always rejects the -# randomly generated panel role over TCP (#5806). Prepend password-auth rules -# scoped to the panel database; first match wins, and md5 also accepts -# scram-sha-256-stored verifiers, so this works on every supported distro. -pg_ensure_hba_password_auth() { - local pg_db="$1" - local hba_file - hba_file=$(sudo -u postgres psql -tAc 'SHOW hba_file' 2> /dev/null | tr -d '[:space:]') - [[ -n "${hba_file}" && -f "${hba_file}" ]] || return 0 - grep -Eq "^host[[:space:]]+${pg_db}[[:space:]]" "${hba_file}" && return 0 - local tmp - tmp=$(mktemp) || return 1 - { - echo "# Added by 3x-ui: allow password logins for the panel database." - echo "host ${pg_db} all 127.0.0.1/32 md5" - echo "host ${pg_db} all ::1/128 md5" - cat "${hba_file}" - } > "${tmp}" || { - rm -f "${tmp}" - return 1 - } - cat "${tmp}" > "${hba_file}" || { - rm -f "${tmp}" - return 1 - } - rm -f "${tmp}" - sudo -u postgres psql -tAc 'SELECT pg_reload_conf()' > /dev/null 2>&1 || true -} - -install_postgres_local() { - local pg_user pg_pass - pg_pass=$(gen_random_string 24) - local pg_db="xui" - local pg_host="127.0.0.1" - local pg_port="5432" - - case "${release}" in - ubuntu | debian | armbian) - apt-get update >&2 && apt-get install -y -q postgresql >&2 || return 1 - ;; - fedora | amzn | virtuozzo | rhel | almalinux | rocky | ol) - dnf install -y -q postgresql-server postgresql-contrib >&2 || return 1 - [[ -d /var/lib/pgsql/data && -f /var/lib/pgsql/data/PG_VERSION ]] || postgresql-setup --initdb >&2 || return 1 - ;; - centos) - if [[ "${VERSION_ID}" =~ ^7 ]]; then - yum install -y postgresql-server postgresql-contrib >&2 || return 1 - else - dnf install -y -q postgresql-server postgresql-contrib >&2 || return 1 - fi - [[ -d /var/lib/pgsql/data && -f /var/lib/pgsql/data/PG_VERSION ]] || postgresql-setup --initdb >&2 || return 1 - ;; - arch | manjaro | parch) - pacman -Sy --noconfirm postgresql >&2 || return 1 - if [[ ! -f /var/lib/postgres/data/PG_VERSION ]]; then - sudo -u postgres initdb -D /var/lib/postgres/data >&2 || return 1 - fi - ;; - opensuse-tumbleweed | opensuse-leap) - zypper -q install -y postgresql-server postgresql-contrib >&2 || return 1 - if [[ ! -f /var/lib/pgsql/data/PG_VERSION ]]; then - install -d -o postgres -g postgres -m 700 /var/lib/pgsql/data >&2 || return 1 - su - postgres -c "initdb -D /var/lib/pgsql/data" >&2 || return 1 - fi - ;; - alpine) - apk add --no-cache postgresql postgresql-contrib >&2 || return 1 - if [[ ! -f /var/lib/postgresql/data/PG_VERSION ]]; then - /etc/init.d/postgresql setup >&2 || return 1 - fi - rc-update add postgresql default >&2 2> /dev/null || true - rc-service postgresql start >&2 || return 1 - ;; - *) - echo -e "${red}Unsupported distro for automatic PostgreSQL install: ${release}${plain}" >&2 - return 1 - ;; - esac - - if [[ "${release}" != "alpine" ]]; then - systemctl enable --now postgresql >&2 || return 1 - fi - - # Wait briefly for the server to accept connections. - local i - for i in 1 2 3 4 5; do - sudo -u postgres psql -tAc 'SELECT 1' > /dev/null 2>&1 && break - sleep 1 - done - - local existing_owner="" - existing_owner=$(sudo -u postgres psql -tAc \ - "SELECT pg_catalog.pg_get_userbyid(datdba) FROM pg_database WHERE datname='${pg_db}'" 2> /dev/null \ - | tr -d '[:space:]') - if [[ -n "${existing_owner}" && "${existing_owner}" != "postgres" ]]; then - pg_user="${existing_owner}" - else - pg_user=$(gen_random_string 8) - fi - - # Idempotent role/db creation. Identifiers are double-quoted because a - # random username may start with a digit, which Postgres rejects unquoted. - sudo -u postgres psql -tAc "SELECT 1 FROM pg_roles WHERE rolname='${pg_user}'" 2> /dev/null \ - | grep -q 1 \ - || sudo -u postgres psql -c "CREATE USER \"${pg_user}\" WITH PASSWORD '${pg_pass}';" >&2 || return 1 - - sudo -u postgres psql -tAc "SELECT 1 FROM pg_database WHERE datname='${pg_db}'" 2> /dev/null \ - | grep -q 1 \ - || sudo -u postgres psql -c "CREATE DATABASE \"${pg_db}\" OWNER \"${pg_user}\";" >&2 || return 1 - - sudo -u postgres psql -c "ALTER USER \"${pg_user}\" WITH PASSWORD '${pg_pass}';" >&2 || return 1 - - pg_ensure_hba_password_auth "${pg_db}" \ - || echo -e "${yellow}Warning: could not update pg_hba.conf; PostgreSQL may reject the panel's TCP login (ident auth).${plain}" >&2 - - local pg_pass_enc - pg_pass_enc=$(printf '%s' "${pg_pass}" | sed -e 's/%/%25/g' -e 's/:/%3A/g' -e 's/@/%40/g' -e 's|/|%2F|g' -e 's/?/%3F/g' -e 's/#/%23/g') - - if [[ -n "${PG_CRED_FILE:-}" ]]; then - local prev_umask - prev_umask=$(umask) - umask 077 - if ! cat > "${PG_CRED_FILE}" << EOF; then -PG_USER=${pg_user} -PG_PASS=${pg_pass} -PG_HOST=${pg_host} -PG_PORT=${pg_port} -PG_DB=${pg_db} -EOF - umask "${prev_umask}" - echo -e "${red}Failed to write PostgreSQL credentials to ${PG_CRED_FILE}${plain}" >&2 - return 1 - fi - umask "${prev_umask}" - fi - - echo "postgres://${pg_user}:${pg_pass_enc}@${pg_host}:${pg_port}/${pg_db}?sslmode=disable" - return 0 -} - -ensure_pg_client() { - if command -v pg_dump > /dev/null 2>&1 && command -v pg_restore > /dev/null 2>&1; then - return 0 - fi - echo -e "${yellow}Installing PostgreSQL client tools (pg_dump/pg_restore) for in-panel backup...${plain}" >&2 - case "${release}" in - ubuntu | debian | armbian) - apt-get update >&2 && apt-get install -y -q postgresql-client >&2 || return 1 - ;; - fedora | amzn | virtuozzo | rhel | almalinux | rocky | ol) - dnf install -y -q postgresql >&2 || return 1 - ;; - centos) - if [[ "${VERSION_ID}" =~ ^7 ]]; then - yum install -y postgresql >&2 || return 1 - else - dnf install -y -q postgresql >&2 || return 1 - fi - ;; - arch | manjaro | parch) - pacman -Sy --noconfirm postgresql >&2 || return 1 - ;; - opensuse-tumbleweed | opensuse-leap) - zypper -q install -y postgresql >&2 || return 1 - ;; - alpine) - apk add --no-cache postgresql-client >&2 || return 1 - ;; - *) - return 1 - ;; - esac - command -v pg_dump > /dev/null 2>&1 && command -v pg_restore > /dev/null 2>&1 -} - -install_acme() { - echo -e "${green}Installing acme.sh for SSL certificate management...${plain}" - cd ~ || return 1 - curl -s https://get.acme.sh | sh > /dev/null 2>&1 - if [ $? -ne 0 ]; then - echo -e "${red}Failed to install acme.sh${plain}" - return 1 - else - echo -e "${green}acme.sh installed successfully${plain}" - fi - return 0 -} - -setup_ssl_certificate() { - local domain="$1" - local server_ip="$2" - local existing_port="$3" - local existing_webBasePath="$4" - - echo -e "${green}Setting up SSL certificate...${plain}" - - # Check if acme.sh is installed - if ! command -v ~/.acme.sh/acme.sh &> /dev/null; then - install_acme - if [ $? -ne 0 ]; then - echo -e "${yellow}Failed to install acme.sh, skipping SSL setup${plain}" - return 1 - fi - fi - - # Create certificate directory - local certPath="/root/cert/${domain}" - mkdir -p "$certPath" - - # Issue certificate - echo -e "${green}Issuing SSL certificate for ${domain}...${plain}" - echo -e "${yellow}Note: Port 80 must be open and accessible from the internet${plain}" - - ~/.acme.sh/acme.sh --set-default-ca --server letsencrypt --force > /dev/null 2>&1 - ~/.acme.sh/acme.sh --issue -d ${domain} $(acme_listen_flag) --standalone --httpport 80 --force - - if [ $? -ne 0 ]; then - echo -e "${yellow}Failed to issue certificate for ${domain}${plain}" - echo -e "${yellow}Please ensure port 80 is open and try again later with: x-ui${plain}" - rm -rf ~/.acme.sh/${domain} ~/.acme.sh/${domain}_ecc 2> /dev/null - rm -rf "$certPath" 2> /dev/null - return 1 - fi - - # Install certificate - ~/.acme.sh/acme.sh --installcert --force -d ${domain} \ - --key-file /root/cert/${domain}/privkey.pem \ - --fullchain-file /root/cert/${domain}/fullchain.pem \ - --reloadcmd "systemctl restart x-ui" > /dev/null 2>&1 - - if [ $? -ne 0 ]; then - echo -e "${yellow}Failed to install certificate${plain}" - return 1 - fi - - # Enable auto-renew - ~/.acme.sh/acme.sh --upgrade --auto-upgrade > /dev/null 2>&1 - # Secure permissions: private key readable only by owner - chmod 600 $certPath/privkey.pem 2> /dev/null - chmod 644 $certPath/fullchain.pem 2> /dev/null - - # Set certificate for panel - local webCertFile="/root/cert/${domain}/fullchain.pem" - local webKeyFile="/root/cert/${domain}/privkey.pem" - - if [[ -f "$webCertFile" && -f "$webKeyFile" ]]; then - ${xui_folder}/x-ui cert -webCert "$webCertFile" -webCertKey "$webKeyFile" > /dev/null 2>&1 - echo -e "${green}SSL certificate installed and configured successfully!${plain}" - return 0 - else - echo -e "${yellow}Certificate files not found${plain}" - return 1 - fi -} - -# Issue Let's Encrypt IP certificate with shortlived profile (~6 days validity) -# Requires acme.sh and port 80 open for HTTP-01 challenge -setup_ip_certificate() { - local ipv4="$1" - local ipv6="$2" # optional - - echo -e "${green}Setting up Let's Encrypt IP certificate (shortlived profile)...${plain}" - echo -e "${yellow}Note: IP certificates are valid for ~6 days and will auto-renew.${plain}" - echo -e "${yellow}Default listener is port 80. If you choose another port, ensure external port 80 forwards to it.${plain}" - - # Check for acme.sh - if ! command -v ~/.acme.sh/acme.sh &> /dev/null; then - install_acme - if [ $? -ne 0 ]; then - echo -e "${red}Failed to install acme.sh${plain}" - return 1 - fi - fi - - # Validate IP address - if [[ -z "$ipv4" ]]; then - echo -e "${red}IPv4 address is required${plain}" - return 1 - fi - - if ! is_ipv4 "$ipv4"; then - echo -e "${red}Invalid IPv4 address: $ipv4${plain}" - return 1 - fi - - # Create certificate directory - local certDir="/root/cert/ip" - mkdir -p "$certDir" - - # Build domain arguments - local domain_args="-d ${ipv4}" - if [[ -n "$ipv6" ]] && is_ipv6 "$ipv6"; then - domain_args="${domain_args} -d ${ipv6}" - echo -e "${green}Including IPv6 address: ${ipv6}${plain}" - fi - - # Set reload command for auto-renewal (add || true so it doesn't fail during first install) - local reloadCmd="systemctl restart x-ui 2>/dev/null || rc-service x-ui restart 2>/dev/null || true" - - # Choose port for HTTP-01 listener (default 80, prompt override) - local WebPort="" - prompt_or_default WebPort "Port to use for ACME HTTP-01 listener (default 80): " "80" XUI_ACME_HTTP_PORT - WebPort="${WebPort:-80}" - if ! [[ "${WebPort}" =~ ^[0-9]+$ ]] || ((WebPort < 1 || WebPort > 65535)); then - echo -e "${red}Invalid port provided. Falling back to 80.${plain}" - WebPort=80 - fi - echo -e "${green}Using port ${WebPort} for standalone validation.${plain}" - if [[ "${WebPort}" -ne 80 ]]; then - echo -e "${yellow}Reminder: Let's Encrypt still connects on port 80; forward external port 80 to ${WebPort}.${plain}" - fi - - # Ensure chosen port is available - while true; do - if is_port_in_use "${WebPort}"; then - echo -e "${yellow}Port ${WebPort} is in use.${plain}" - - local alt_port="" - if [[ "$NONINTERACTIVE" == "1" ]]; then - echo -e "${red}Port ${WebPort} is busy; cannot proceed in non-interactive mode.${plain}" - return 1 - fi - read -rp "Enter another port for acme.sh standalone listener (leave empty to abort): " alt_port - alt_port="${alt_port// /}" - if [[ -z "${alt_port}" ]]; then - echo -e "${red}Port ${WebPort} is busy; cannot proceed.${plain}" - return 1 - fi - if ! [[ "${alt_port}" =~ ^[0-9]+$ ]] || ((alt_port < 1 || alt_port > 65535)); then - echo -e "${red}Invalid port provided.${plain}" - return 1 - fi - WebPort="${alt_port}" - continue - else - echo -e "${green}Port ${WebPort} is free and ready for standalone validation.${plain}" - break - fi - done - - # Issue certificate with shortlived profile - echo -e "${green}Issuing IP certificate for ${ipv4}...${plain}" - ~/.acme.sh/acme.sh --set-default-ca --server letsencrypt --force > /dev/null 2>&1 - [[ -n "${XUI_ACME_EMAIL:-}" ]] && ~/.acme.sh/acme.sh --register-account -m "${XUI_ACME_EMAIL}" > /dev/null 2>&1 - - ~/.acme.sh/acme.sh --issue \ - ${domain_args} \ - --standalone \ - --server letsencrypt \ - --certificate-profile shortlived \ - --days 6 \ - --httpport ${WebPort} \ - --force - - if [ $? -ne 0 ]; then - echo -e "${red}Failed to issue IP certificate${plain}" - echo -e "${yellow}Please ensure port ${WebPort} is reachable (or forwarded from external port 80)${plain}" - # Cleanup acme.sh data for both IPv4 and IPv6 if specified - rm -rf ~/.acme.sh/${ipv4} ~/.acme.sh/${ipv4}_ecc 2> /dev/null - [[ -n "$ipv6" ]] && rm -rf ~/.acme.sh/${ipv6} ~/.acme.sh/${ipv6}_ecc 2> /dev/null - rm -rf ${certDir} 2> /dev/null - return 1 - fi - - echo -e "${green}Certificate issued successfully, installing...${plain}" - - # Install certificate - # Note: acme.sh may report "Reload error" and exit non-zero if reloadcmd fails, - # but the cert files are still installed. We check for files instead of exit code. - ~/.acme.sh/acme.sh --installcert --force -d ${ipv4} \ - --key-file "${certDir}/privkey.pem" \ - --fullchain-file "${certDir}/fullchain.pem" \ - --reloadcmd "${reloadCmd}" 2>&1 || true - - # Verify certificate files exist (don't rely on exit code - reloadcmd failure causes non-zero) - if [[ ! -f "${certDir}/fullchain.pem" || ! -f "${certDir}/privkey.pem" ]]; then - echo -e "${red}Certificate files not found after installation${plain}" - # Cleanup acme.sh data for both IPv4 and IPv6 if specified - rm -rf ~/.acme.sh/${ipv4} ~/.acme.sh/${ipv4}_ecc 2> /dev/null - [[ -n "$ipv6" ]] && rm -rf ~/.acme.sh/${ipv6} ~/.acme.sh/${ipv6}_ecc 2> /dev/null - rm -rf ${certDir} 2> /dev/null - return 1 - fi - - echo -e "${green}Certificate files installed successfully${plain}" - - # Enable auto-upgrade for acme.sh (ensures cron job runs) - ~/.acme.sh/acme.sh --upgrade --auto-upgrade > /dev/null 2>&1 - - # Secure permissions: private key readable only by owner - chmod 600 ${certDir}/privkey.pem 2> /dev/null - chmod 644 ${certDir}/fullchain.pem 2> /dev/null - - # Configure panel to use the certificate - echo -e "${green}Setting certificate paths for the panel...${plain}" - ${xui_folder}/x-ui cert -webCert "${certDir}/fullchain.pem" -webCertKey "${certDir}/privkey.pem" - - if [ $? -ne 0 ]; then - echo -e "${yellow}Warning: Could not set certificate paths automatically${plain}" - echo -e "${yellow}Certificate files are at:${plain}" - echo -e " Cert: ${certDir}/fullchain.pem" - echo -e " Key: ${certDir}/privkey.pem" - else - echo -e "${green}Certificate paths configured successfully${plain}" - fi - - echo -e "${green}IP certificate installed and configured successfully!${plain}" - echo -e "${green}Certificate valid for ~6 days, auto-renews via acme.sh cron job.${plain}" - echo -e "${yellow}acme.sh will automatically renew and reload x-ui before expiry.${plain}" - return 0 -} - -# Comprehensive manual SSL certificate issuance via acme.sh -ssl_cert_issue() { - local existing_webBasePath=$(${xui_folder}/x-ui setting -show true | grep 'webBasePath:' | awk -F': ' '{print $2}' | tr -d '[:space:]' | sed 's#^/##') - local existing_port=$(${xui_folder}/x-ui setting -show true | grep 'port:' | awk -F': ' '{print $2}' | tr -d '[:space:]') - - # check for acme.sh first - if ! command -v ~/.acme.sh/acme.sh &> /dev/null; then - echo "acme.sh could not be found. Installing now..." - cd ~ || return 1 - curl -s https://get.acme.sh | sh - if [ $? -ne 0 ]; then - echo -e "${red}Failed to install acme.sh${plain}" - return 1 - else - echo -e "${green}acme.sh installed successfully${plain}" - fi - fi - - # get the domain here, and we need to verify it - local domain="" - if [[ "$NONINTERACTIVE" == "1" ]]; then - domain="${XUI_DOMAIN// /}" - if [[ -z "$domain" ]] || ! is_domain "$domain"; then - echo -e "${red}XUI_SSL_MODE=domain requires a valid XUI_DOMAIN (got: '${XUI_DOMAIN:-}').${plain}" - return 1 - fi - else - while true; do - read -rp "Please enter your domain name: " domain - domain="${domain// /}" # Trim whitespace - - if [[ -z "$domain" ]]; then - echo -e "${red}Domain name cannot be empty. Please try again.${plain}" - continue - fi - - if ! is_domain "$domain"; then - echo -e "${red}Invalid domain format: ${domain}. Please enter a valid domain name.${plain}" - continue - fi - - break - done - fi - echo -e "${green}Your domain is: ${domain}, checking it...${plain}" - SSL_ISSUED_DOMAIN="${domain}" - - # detect existing certificate and reuse it only if its files are actually - # present and non-empty. acme.sh stores ECC certs under ${domain}_ecc and RSA - # certs under ${domain}; a failed issuance can leave a domain entry in --list - # with no usable cert files, which must not be reused (it produces a 0-byte - # fullchain.pem). Broken partial state is cleaned up so issuance can proceed. - local cert_exists=0 - if ~/.acme.sh/acme.sh --list 2> /dev/null | awk '{print $1}' | grep -Fxq "${domain}"; then - local acmeCertDir="" - if [[ -s ~/.acme.sh/${domain}_ecc/fullchain.cer && -s ~/.acme.sh/${domain}_ecc/${domain}.key ]]; then - acmeCertDir=~/.acme.sh/${domain}_ecc - elif [[ -s ~/.acme.sh/${domain}/fullchain.cer && -s ~/.acme.sh/${domain}/${domain}.key ]]; then - acmeCertDir=~/.acme.sh/${domain} - fi - if [[ -n "${acmeCertDir}" ]]; then - cert_exists=1 - local certInfo=$(~/.acme.sh/acme.sh --list 2> /dev/null | grep -F "${domain}") - echo -e "${yellow}Existing certificate found for ${domain}, will reuse it.${plain}" - [[ -n "${certInfo}" ]] && echo "$certInfo" - else - echo -e "${yellow}Found incomplete acme.sh state for ${domain} (no valid certificate files); cleaning it up and re-issuing.${plain}" - rm -rf ~/.acme.sh/${domain} ~/.acme.sh/${domain}_ecc - fi - fi - if [[ ${cert_exists} -eq 0 ]]; then - echo -e "${green}Your domain is ready for issuing certificates now...${plain}" - fi - - # create a directory for the certificate - certPath="/root/cert/${domain}" - if [ ! -d "$certPath" ]; then - mkdir -p "$certPath" - else - rm -rf "$certPath" - mkdir -p "$certPath" - fi - - # get the port number for the standalone server - local WebPort=80 - prompt_or_default WebPort "Please choose which port to use (default is 80): " "80" XUI_ACME_HTTP_PORT - if [[ -z ${WebPort} ]]; then - WebPort=80 - elif [[ ! ${WebPort} =~ ^[1-9][0-9]*$ || ${WebPort} -gt 65535 ]]; then - echo -e "${yellow}Your input ${WebPort} is invalid, will use default port 80.${plain}" - WebPort=80 - fi - echo -e "${green}Will use port: ${WebPort} to issue certificates. Please make sure this port is open.${plain}" - - # Stop panel temporarily - echo -e "${yellow}Stopping panel temporarily...${plain}" - systemctl stop x-ui 2> /dev/null || rc-service x-ui stop 2> /dev/null - - if [[ ${cert_exists} -eq 0 ]]; then - # issue the certificate - ~/.acme.sh/acme.sh --set-default-ca --server letsencrypt --force - [[ -n "${XUI_ACME_EMAIL:-}" ]] && ~/.acme.sh/acme.sh --register-account -m "${XUI_ACME_EMAIL}" > /dev/null 2>&1 - ~/.acme.sh/acme.sh --issue -d ${domain} $(acme_listen_flag) --standalone --httpport ${WebPort} --force - if [ $? -ne 0 ]; then - echo -e "${red}Issuing certificate failed, please check logs.${plain}" - rm -rf ~/.acme.sh/${domain} ~/.acme.sh/${domain}_ecc - systemctl start x-ui 2> /dev/null || rc-service x-ui start 2> /dev/null - return 1 - else - echo -e "${green}Issuing certificate succeeded, installing certificates...${plain}" - fi - else - echo -e "${green}Using existing certificate, installing certificates...${plain}" - fi - - # Setup reload command - reloadCmd="systemctl restart x-ui || rc-service x-ui restart" - echo -e "${green}Default --reloadcmd for ACME is: ${yellow}systemctl restart x-ui || rc-service x-ui restart${plain}" - echo -e "${green}This command will run on every certificate issue and renew.${plain}" - if [[ "$NONINTERACTIVE" == "1" ]]; then - setReloadcmd="n" - else - read -rp "Would you like to modify --reloadcmd for ACME? (y/n): " setReloadcmd - fi - if [[ "$setReloadcmd" == "y" || "$setReloadcmd" == "Y" ]]; then - echo -e "\n${green}\t1.${plain} Preset: systemctl reload nginx ; systemctl restart x-ui" - echo -e "${green}\t2.${plain} Input your own command" - echo -e "${green}\t0.${plain} Keep default reloadcmd" - read -rp "Choose an option: " choice - case "$choice" in - 1) - echo -e "${green}Reloadcmd is: systemctl reload nginx ; systemctl restart x-ui${plain}" - reloadCmd="systemctl reload nginx ; systemctl restart x-ui" - ;; - 2) - echo -e "${yellow}It's recommended to put x-ui restart at the end${plain}" - read -rp "Please enter your custom reloadcmd: " reloadCmd - echo -e "${green}Reloadcmd is: ${reloadCmd}${plain}" - ;; - *) - echo -e "${green}Keeping default reloadcmd${plain}" - ;; - esac - fi - - # install the certificate - local installOutput="" - installOutput=$(~/.acme.sh/acme.sh --installcert --force -d ${domain} \ - --key-file /root/cert/${domain}/privkey.pem \ - --fullchain-file /root/cert/${domain}/fullchain.pem --reloadcmd "${reloadCmd}" 2>&1) - local installRc=$? - echo "${installOutput}" - - local installWroteFiles=0 - if echo "${installOutput}" | grep -q "Installing key to:" && echo "${installOutput}" | grep -q "Installing full chain to:"; then - installWroteFiles=1 - fi - - if [[ -f "/root/cert/${domain}/privkey.pem" && -f "/root/cert/${domain}/fullchain.pem" && (${installRc} -eq 0 || ${installWroteFiles} -eq 1) ]]; then - echo -e "${green}Installing certificate succeeded, enabling auto renew...${plain}" - else - echo -e "${red}Installing certificate failed, exiting.${plain}" - if [[ ${cert_exists} -eq 0 ]]; then - rm -rf ~/.acme.sh/${domain} ~/.acme.sh/${domain}_ecc - fi - systemctl start x-ui 2> /dev/null || rc-service x-ui start 2> /dev/null - return 1 - fi - - # enable auto-renew - ~/.acme.sh/acme.sh --upgrade --auto-upgrade - if [ $? -ne 0 ]; then - echo -e "${yellow}Auto renew setup had issues, certificate details:${plain}" - ls -lah /root/cert/${domain}/ - # Secure permissions: private key readable only by owner - chmod 600 $certPath/privkey.pem 2> /dev/null - chmod 644 $certPath/fullchain.pem 2> /dev/null - else - echo -e "${green}Auto renew succeeded, certificate details:${plain}" - ls -lah /root/cert/${domain}/ - # Secure permissions: private key readable only by owner - chmod 600 $certPath/privkey.pem 2> /dev/null - chmod 644 $certPath/fullchain.pem 2> /dev/null - fi - - # start panel - systemctl start x-ui 2> /dev/null || rc-service x-ui start 2> /dev/null - - # Prompt user to set panel paths after successful certificate installation - if [[ "$NONINTERACTIVE" == "1" ]]; then - setPanel="y" - else - read -rp "Would you like to set this certificate for the panel? (y/n): " setPanel - fi - if [[ "$setPanel" == "y" || "$setPanel" == "Y" ]]; then - local webCertFile="/root/cert/${domain}/fullchain.pem" - local webKeyFile="/root/cert/${domain}/privkey.pem" - - if [[ -f "$webCertFile" && -f "$webKeyFile" ]]; then - ${xui_folder}/x-ui cert -webCert "$webCertFile" -webCertKey "$webKeyFile" - echo -e "${green}Certificate paths set for the panel${plain}" - echo -e "${green}Certificate File: $webCertFile${plain}" - echo -e "${green}Private Key File: $webKeyFile${plain}" - echo "" - echo -e "${green}Access URL: https://${domain}:${existing_port}/${existing_webBasePath}${plain}" - echo -e "${yellow}Panel will restart to apply SSL certificate...${plain}" - systemctl restart x-ui 2> /dev/null || rc-service x-ui restart 2> /dev/null - else - echo -e "${red}Error: Certificate or private key file not found for domain: $domain.${plain}" - fi - else - echo -e "${yellow}Skipping panel path setting.${plain}" - fi - - return 0 -} - -# Reusable interactive SSL setup (domain or IP) -# Sets global `SSL_HOST` to the chosen domain/IP for Access URL usage -prompt_and_setup_ssl() { - local panel_port="$1" - local web_base_path="$2" - local server_ip="$3" - - local ssl_choice="" - SSL_SCHEME="https" - - echo -e "${yellow}Choose SSL certificate setup method:${plain}" - echo -e "${green}1.${plain} Let's Encrypt for Domain (90-day validity, auto-renews)" - echo -e "${green}2.${plain} Let's Encrypt for IP Address (6-day validity, auto-renews)" - echo -e "${green}3.${plain} Custom SSL Certificate (Path to existing files)" - echo -e "${green}4.${plain} Skip SSL (advanced — behind reverse proxy / SSH tunnel only)" - echo -e "${blue}Note:${plain} Options 1 & 2 require port 80 open. Option 3 requires manual paths." - echo -e "${blue}Note:${plain} Option 4 serves the panel over plain HTTP — only safe behind nginx/Caddy or an SSH tunnel." - if [[ "$NONINTERACTIVE" == "1" ]]; then - case "${XUI_SSL_MODE:-none}" in - domain) ssl_choice="1" ;; - ip) ssl_choice="2" ;; - none | "") ssl_choice="4" ;; - *) - echo -e "${yellow}Unknown XUI_SSL_MODE='${XUI_SSL_MODE}', defaulting to none (HTTP).${plain}" - ssl_choice="4" - ;; - esac - else - read -rp "Choose an option (default 2 for IP): " ssl_choice - ssl_choice="${ssl_choice// /}" # Trim whitespace - - # Default to 2 (IP cert) if input is empty or invalid (not 1, 3 or 4) - if [[ "$ssl_choice" != "1" && "$ssl_choice" != "3" && "$ssl_choice" != "4" ]]; then - ssl_choice="2" - fi - fi - - case "$ssl_choice" in - 1) - # User chose Let's Encrypt domain option - echo -e "${green}Using Let's Encrypt for domain certificate...${plain}" - if ssl_cert_issue; then - local cert_domain="${SSL_ISSUED_DOMAIN}" - if [[ -z "${cert_domain}" ]]; then - cert_domain=$(~/.acme.sh/acme.sh --list 2> /dev/null | tail -1 | awk '{print $1}') - fi - - if [[ -n "${cert_domain}" ]]; then - SSL_HOST="${cert_domain}" - echo -e "${green}✓ SSL certificate configured successfully with domain: ${cert_domain}${plain}" - else - echo -e "${yellow}SSL setup may have completed, but domain extraction failed${plain}" - SSL_HOST="${server_ip}" - fi - else - echo -e "${red}SSL certificate setup failed for domain mode.${plain}" - SSL_HOST="${server_ip}" - fi - ;; - 2) - # User chose Let's Encrypt IP certificate option - echo -e "${green}Using Let's Encrypt for IP certificate (shortlived profile)...${plain}" - - # Confirm the auto-detected IP before issuing for it: with asymmetric - # routing / multi-WAN the echo services can return a transit address. - if [[ "$NONINTERACTIVE" != "1" ]]; then - local ip_confirm="" - read -rp "Is ${server_ip} the correct incoming public IPv4 address for this server? [Default y]: " ip_confirm - if [[ -n "$ip_confirm" && "$ip_confirm" != "y" && "$ip_confirm" != "Y" ]]; then - server_ip="" - while [[ -z "$server_ip" ]]; do - read -rp "Please enter your server's public IPv4 address: " server_ip - server_ip="${server_ip// /}" - if [[ ! "$server_ip" =~ ^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$ ]]; then - echo -e "${red}Invalid IPv4 address. Please try again.${plain}" - server_ip="" - fi - done - fi - fi - - # Ask for optional IPv6 - local ipv6_addr="" - prompt_or_default ipv6_addr "Do you have an IPv6 address to include? (leave empty to skip): " "" XUI_SSL_IPV6 - ipv6_addr="${ipv6_addr// /}" # Trim whitespace - - # Stop panel if running (port 80 needed) - if [[ $release == "alpine" ]]; then - rc-service x-ui stop > /dev/null 2>&1 - else - systemctl stop x-ui > /dev/null 2>&1 - fi - - setup_ip_certificate "${server_ip}" "${ipv6_addr}" - if [ $? -eq 0 ]; then - SSL_HOST="${server_ip}" - echo -e "${green}✓ Let's Encrypt IP certificate configured successfully${plain}" - else - echo -e "${red}✗ IP certificate setup failed. Please check port 80 is open.${plain}" - SSL_HOST="${server_ip}" - fi - ;; - 3) - # User chose Custom Paths (User Provided) option - echo -e "${green}Using custom existing certificate...${plain}" - local custom_cert="" - local custom_key="" - local custom_domain="" - - # 3.1 Request Domain to compose Panel URL later - read -rp "Please enter domain name certificate issued for: " custom_domain - custom_domain="${custom_domain// /}" # Remove spaces - - # 3.2 Loop for Certificate Path - while true; do - read -rp "Input certificate path (keywords: .crt / fullchain): " custom_cert - # Strip quotes if present - custom_cert=$(echo "$custom_cert" | tr -d '"' | tr -d "'") - - if [[ -f "$custom_cert" && -r "$custom_cert" && -s "$custom_cert" ]]; then - break - elif [[ ! -f "$custom_cert" ]]; then - echo -e "${red}Error: File does not exist! Try again.${plain}" - elif [[ ! -r "$custom_cert" ]]; then - echo -e "${red}Error: File exists but is not readable (check permissions)!${plain}" - else - echo -e "${red}Error: File is empty!${plain}" - fi - done - - # 3.3 Loop for Private Key Path - while true; do - read -rp "Input private key path (keywords: .key / privatekey): " custom_key - # Strip quotes if present - custom_key=$(echo "$custom_key" | tr -d '"' | tr -d "'") - - if [[ -f "$custom_key" && -r "$custom_key" && -s "$custom_key" ]]; then - break - elif [[ ! -f "$custom_key" ]]; then - echo -e "${red}Error: File does not exist! Try again.${plain}" - elif [[ ! -r "$custom_key" ]]; then - echo -e "${red}Error: File exists but is not readable (check permissions)!${plain}" - else - echo -e "${red}Error: File is empty!${plain}" - fi - done - - # 3.4 Apply Settings via x-ui binary - ${xui_folder}/x-ui cert -webCert "$custom_cert" -webCertKey "$custom_key" > /dev/null 2>&1 - - # Set SSL_HOST for composing Panel URL - if [[ -n "$custom_domain" ]]; then - SSL_HOST="$custom_domain" - else - SSL_HOST="${server_ip}" - fi - - echo -e "${green}✓ Custom certificate paths applied.${plain}" - echo -e "${yellow}Note: You are responsible for renewing these files externally.${plain}" - - systemctl restart x-ui > /dev/null 2>&1 || rc-service x-ui restart > /dev/null 2>&1 - ;; - 4) - echo "" - echo -e "${red}⚠ Panel will be installed WITHOUT SSL/TLS.${plain}" - echo -e "${yellow}Login credentials and cookies will travel as plain HTTP.${plain}" - echo -e "${yellow}Only safe when:${plain}" - echo -e "${yellow} • A reverse proxy (nginx, Caddy, Traefik) terminates TLS for you, or${plain}" - echo -e "${yellow} • You access the panel exclusively via SSH tunnel${plain}" - echo "" - - SSL_SCHEME="http" - SSL_HOST="${server_ip}" - - local bind_local="" - if [[ "$NONINTERACTIVE" == "1" ]]; then - # Cloud images must stay reachable on their public interface. - bind_local="n" - else - read -rp "Bind the panel to 127.0.0.1 only? (recommended — forces SSH tunnel / reverse-proxy access) [y/N]: " bind_local - fi - if [[ "$bind_local" == "y" || "$bind_local" == "Y" ]]; then - ${xui_folder}/x-ui setting -listenIP "127.0.0.1" > /dev/null 2>&1 - SSL_HOST="127.0.0.1" - echo -e "${green}✓ Panel bound to 127.0.0.1 only. It is now unreachable from the public internet.${plain}" - echo "" - echo -e "${green}SSH Port Forwarding — open the panel from your local machine via:${plain}" - echo -e " Standard SSH command:" - echo -e " ${yellow}ssh -L 2222:127.0.0.1:${panel_port} root@${server_ip}${plain}" - echo -e " If using an SSH key:" - echo -e " ${yellow}ssh -i -L 2222:127.0.0.1:${panel_port} root@${server_ip}${plain}" - echo -e " Then open in your browser:" - echo -e " ${yellow}http://localhost:2222/${web_base_path}${plain}" - echo "" - echo -e "${yellow}Alternative: point a reverse proxy (nginx/Caddy) at 127.0.0.1:${panel_port} and let it terminate TLS.${plain}" - else - echo -e "${yellow}Panel will listen on all interfaces over plain HTTP. Make sure something else is terminating TLS in front of it.${plain}" - fi - - systemctl restart x-ui > /dev/null 2>&1 || rc-service x-ui restart > /dev/null 2>&1 - echo -e "${green}✓ SSL setup skipped.${plain}" - ;; - *) - echo -e "${red}Invalid option. Skipping SSL setup.${plain}" - SSL_HOST="${server_ip}" - ;; - esac -} - -config_after_install() { - local existing_hasDefaultCredential=$(${xui_folder}/x-ui setting -show true | grep -Eo 'hasDefaultCredential: .+' | awk '{print $2}') - local existing_webBasePath=$(${xui_folder}/x-ui setting -show true | grep -Eo 'webBasePath: .+' | awk '{print $2}' | sed 's#^/##') - local existing_port=$(${xui_folder}/x-ui setting -show true | grep -Eo 'port: .+' | awk '{print $2}') - # Properly detect empty cert by checking if cert: line exists and has content after it - local existing_cert=$(${xui_folder}/x-ui setting -getCert true | grep 'cert:' | awk -F': ' '{print $2}' | tr -d '[:space:]') - local URL_lists=( - "https://api4.ipify.org" - "https://ipv4.icanhazip.com" - "https://v4.api.ipinfo.io/ip" - "https://ipv4.myexternalip.com/raw" - "https://4.ident.me" - "https://check-host.net/ip" - ) - local server_ip="" - for ip_address in "${URL_lists[@]}"; do - local response=$(curl -s -w "\n%{http_code}" --max-time 3 "${ip_address}" 2> /dev/null) - local http_code=$(echo "$response" | tail -n1) - local ip_result=$(echo "$response" | head -n-1 | tr -d '[:space:]"') - if [[ "${http_code}" == "200" && "${ip_result}" =~ ^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$ ]]; then - server_ip="${ip_result}" - break - fi - done - - if [[ -z "$server_ip" ]]; then - if [[ "$NONINTERACTIVE" == "1" ]]; then - # Panel binds 0.0.0.0 regardless; the IP is only used to compose the - # displayed access URL. Fall back to XUI_SERVER_IP or leave blank. - server_ip="${XUI_SERVER_IP:-}" - else - echo -e "${yellow}Could not auto-detect server IP from any provider.${plain}" - while [[ -z "$server_ip" ]]; do - read -rp "Please enter your server's public IPv4 address: " server_ip - server_ip="${server_ip// /}" - if [[ ! "$server_ip" =~ ^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$ ]]; then - echo -e "${red}Invalid IPv4 address. Please try again.${plain}" - server_ip="" - fi - done - fi - fi - - if [[ ${#existing_webBasePath} -lt 4 ]]; then - if [[ "$existing_hasDefaultCredential" == "true" ]]; then - local config_webBasePath="${XUI_WEB_BASE_PATH:-$(gen_random_string 18)}" - local config_username="${XUI_USERNAME:-$(gen_random_string 10)}" - local config_password="${XUI_PASSWORD:-$(gen_random_string 10)}" - local config_port="" - - local db_label="SQLite (/etc/x-ui/x-ui.db)" - echo "" - echo -e "${green}═══════════════════════════════════════════${plain}" - echo -e "${green} Database Selection ${plain}" - echo -e "${green}═══════════════════════════════════════════${plain}" - echo -e " 1) SQLite (default — recommended for < 500 clients)" - echo -e " 2) PostgreSQL (recommended for high client counts / many nodes)" - if [[ "$NONINTERACTIVE" == "1" ]]; then - if [[ "${XUI_DB_TYPE:-sqlite}" == "postgres" ]]; then - db_choice="2" - else - db_choice="1" - fi - else - read -rp "Choose [1]: " db_choice - db_choice="${db_choice:-1}" - fi - if [[ "$db_choice" == "2" ]]; then - local xui_env_file - case "${release}" in - ubuntu | debian | armbian) - xui_env_file="/etc/default/x-ui" - ;; - arch | manjaro | parch | alpine) - xui_env_file="/etc/conf.d/x-ui" - ;; - *) - xui_env_file="/etc/sysconfig/x-ui" - ;; - esac - - local xui_dsn="" - local pg_mode="" - local pg_local_installed=0 - while [[ -z "$xui_dsn" ]]; do - if [[ "$NONINTERACTIVE" == "1" ]]; then - if [[ -n "${XUI_DB_DSN:-}" ]]; then - xui_dsn="${XUI_DB_DSN}" - db_label="PostgreSQL (external)" - break - fi - echo -e "${yellow}Installing PostgreSQL locally (non-interactive)...${plain}" - local pg_cred_file - pg_cred_file=$(mktemp 2> /dev/null) || pg_cred_file=$(mktemp -t x-ui-pg-creds.XXXXXXXX) - if [[ -n "${pg_cred_file}" ]] && xui_dsn=$(PG_CRED_FILE="${pg_cred_file}" install_postgres_local); then - pg_local_installed=1 - if [[ -r "${pg_cred_file}" ]]; then - # shellcheck disable=SC1090 - source "${pg_cred_file}" - fi - rm -f "${pg_cred_file}" - db_label="PostgreSQL (${PG_USER}@${PG_HOST}:${PG_PORT}/${PG_DB})" - break - fi - rm -f "${pg_cred_file}" - echo -e "${red}PostgreSQL installation failed in non-interactive mode; aborting.${plain}" - echo -e "${yellow}Set XUI_DB_DSN to use an existing server, or XUI_DB_TYPE=sqlite.${plain}" - exit 1 - fi - echo "" - echo -e " 1) Install PostgreSQL locally and create a dedicated user/db (recommended)" - echo -e " 2) Use an existing PostgreSQL server (enter DSN)" - read -rp "Choose [1]: " pg_mode - pg_mode="${pg_mode:-1}" - if [[ "$pg_mode" == "2" ]]; then - while [[ -z "$xui_dsn" ]]; do - read -rp "Enter PostgreSQL DSN (postgres://user:pass@host:port/dbname?sslmode=disable): " xui_dsn - xui_dsn="${xui_dsn// /}" - done - db_label="PostgreSQL (external)" - else - echo -e "${yellow}Installing PostgreSQL — this may take a moment...${plain}" - local pg_cred_file - pg_cred_file=$(mktemp 2> /dev/null) || pg_cred_file=$(mktemp -t x-ui-pg-creds.XXXXXXXX) - if [[ -z "${pg_cred_file}" ]]; then - echo -e "${red}Failed to create temporary credentials file.${plain}" - xui_dsn="" - continue - fi - if xui_dsn=$(PG_CRED_FILE="${pg_cred_file}" install_postgres_local); then - pg_local_installed=1 - if [[ -r "${pg_cred_file}" ]]; then - # shellcheck disable=SC1090 - source "${pg_cred_file}" - fi - rm -f "${pg_cred_file}" - db_label="PostgreSQL (${PG_USER}@${PG_HOST}:${PG_PORT}/${PG_DB})" - else - rm -f "${pg_cred_file}" - echo "" - echo -e "${red}PostgreSQL installation failed.${plain}" - echo -e " 1) Retry local install" - echo -e " 2) Enter an external DSN instead" - echo -e " 3) Abort install" - echo -e " 4) Fall back to SQLite" - read -rp "Choose [1]: " pg_fail - pg_fail="${pg_fail:-1}" - case "$pg_fail" in - 2) pg_mode="2" ;; - 3) - echo -e "${red}Install aborted.${plain}" - exit 1 - ;; - 4) - db_choice="1" - xui_dsn="" - break - ;; - *) xui_dsn="" ;; - esac - fi - fi - done - if [[ -n "$xui_dsn" ]]; then - install -d -m 755 "$(dirname "$xui_env_file")" - umask 077 - cat > "$xui_env_file" << EOF -XUI_DB_TYPE=postgres -XUI_DB_DSN=${xui_dsn} -EOF - chmod 600 "$xui_env_file" - umask 022 - export XUI_DB_TYPE=postgres - export XUI_DB_DSN="${xui_dsn}" - ensure_pg_client || echo -e "${yellow}⚠ Could not install pg_dump/pg_restore. In-panel database backup/restore will be unavailable until you install the postgresql-client package.${plain}" - fi - fi - - if [[ "$NONINTERACTIVE" == "1" ]]; then - if [[ -n "${XUI_PANEL_PORT:-}" ]]; then - config_port="${XUI_PANEL_PORT}" - echo -e "${yellow}Your Panel Port is: ${config_port}${plain}" - else - config_port=$(shuf -i 1024-62000 -n 1) - echo -e "${yellow}Generated random port: ${config_port}${plain}" - fi - else - read -rp "Would you like to customize the Panel Port settings? (If not, a random port will be applied) [y/n]: " config_confirm - if [[ "${config_confirm}" == "y" || "${config_confirm}" == "Y" ]]; then - read -rp "Please set up the panel port: " config_port - echo -e "${yellow}Your Panel Port is: ${config_port}${plain}" - else - config_port=$(shuf -i 1024-62000 -n 1) - echo -e "${yellow}Generated random port: ${config_port}${plain}" - fi - fi - - ${xui_folder}/x-ui setting -username "${config_username}" -password "${config_password}" -port "${config_port}" -webBasePath "${config_webBasePath}" - - echo "" - echo -e "${green}═══════════════════════════════════════════${plain}" - echo -e "${green} SSL Certificate Setup (RECOMMENDED) ${plain}" - echo -e "${green}═══════════════════════════════════════════${plain}" - echo -e "${yellow}SSL is strongly recommended. Skip only if a reverse proxy${plain}" - echo -e "${yellow}or SSH tunnel handles TLS for you.${plain}" - echo -e "${yellow}Let's Encrypt now supports both domains and IP addresses!${plain}" - echo "" - - prompt_and_setup_ssl "${config_port}" "${config_webBasePath}" "${server_ip}" - - # Retrieve the API token for display - local config_apiToken=$(${xui_folder}/x-ui setting -getApiToken true | grep -Eo 'apiToken: .+' | awk '{print $2}') - - # Display final credentials and access information - echo "" - echo -e "${green}═══════════════════════════════════════════${plain}" - echo -e "${green} Panel Installation Complete! ${plain}" - echo -e "${green}═══════════════════════════════════════════${plain}" - echo -e "${green}Username: ${config_username}${plain}" - echo -e "${green}Password: ${config_password}${plain}" - echo -e "${green}Port: ${config_port}${plain}" - echo -e "${green}WebBasePath: ${config_webBasePath}${plain}" - echo -e "${green}Database: ${db_label}${plain}" - echo -e "${green}Access URL: ${SSL_SCHEME}://${SSL_HOST}:${config_port}/${config_webBasePath}${plain}" - echo -e "${green}API Token: ${config_apiToken}${plain}" - echo -e "${green}═══════════════════════════════════════════${plain}" - echo -e "${yellow}⚠ IMPORTANT: Save these credentials securely!${plain}" - if [[ "$SSL_SCHEME" == "https" ]]; then - echo -e "${yellow}⚠ SSL Certificate: Enabled and configured${plain}" - else - echo -e "${yellow}⚠ SSL Certificate: Skipped — panel is HTTP-only. Use a reverse proxy or SSH tunnel.${plain}" - fi - - if [[ "$db_choice" == "2" ]]; then - echo "" - echo -e "${green}PostgreSQL backup & restore is built into the panel:${plain}" - echo -e " ${blue}${SSL_SCHEME}://${SSL_HOST}:${config_port}/${config_webBasePath}${plain} → Backup & Restore" - echo -e "${yellow} Back Up downloads a pg_dump .dump file; Restore reloads it via pg_restore.${plain}" - fi - - if [[ "$db_choice" == "2" && "$pg_local_installed" == "1" ]]; then - echo "" - echo -e "${green}═══════════════════════════════════════════${plain}" - echo -e "${green} PostgreSQL Credentials ${plain}" - echo -e "${green}═══════════════════════════════════════════${plain}" - echo -e "${green}DB Name: ${PG_DB}${plain}" - echo -e "${green}Username: ${PG_USER}${plain}" - echo -e "${green}Password: ${PG_PASS}${plain}" - echo -e "${green}Host: ${PG_HOST}${plain}" - echo -e "${green}Port: ${PG_PORT}${plain}" - echo -e "${green}DSN: ${xui_dsn}${plain}" - echo -e "${green}Env file: ${xui_env_file}${plain}" - echo -e "${green}-------------------------------------------${plain}" - echo -e "${green}Connect from this server:${plain}" - echo -e " ${blue}sudo -u postgres psql -d ${PG_DB}${plain} (as the postgres superuser)" - echo -e " ${blue}PGPASSWORD='${PG_PASS}' psql -h ${PG_HOST} -p ${PG_PORT} -U ${PG_USER} -d ${PG_DB}${plain}" - echo -e "${green}═══════════════════════════════════════════${plain}" - echo -e "${yellow}⚠ The panel reads these credentials from ${xui_env_file}.${plain}" - echo -e "${yellow}⚠ Save the password — it is not stored anywhere else in plain text.${plain}" - unset PG_USER PG_PASS PG_HOST PG_PORT PG_DB - fi - - # Persist a machine-parseable credentials file for cloud-init / MOTD. - : "${SSL_SCHEME:=https}" - : "${SSL_HOST:=${server_ip}}" - local db_type_out="sqlite" - [[ "$db_choice" == "2" ]] && db_type_out="postgres" - write_install_result "${config_username}" "${config_password}" "${config_port}" \ - "${config_webBasePath}" "${SSL_SCHEME}" "${SSL_HOST}" "${config_apiToken}" "${db_type_out}" - else - local config_webBasePath=$(gen_random_string 18) - echo -e "${yellow}WebBasePath is missing or too short. Generating a new one...${plain}" - ${xui_folder}/x-ui setting -webBasePath "${config_webBasePath}" - echo -e "${green}New WebBasePath: ${config_webBasePath}${plain}" - - # If the panel is already installed but no certificate is configured, prompt for SSL now - if [[ -z "${existing_cert}" ]]; then - echo "" - echo -e "${green}═══════════════════════════════════════════${plain}" - echo -e "${green} SSL Certificate Setup (RECOMMENDED) ${plain}" - echo -e "${green}═══════════════════════════════════════════${plain}" - echo -e "${yellow}Let's Encrypt now supports both domains and IP addresses!${plain}" - echo "" - prompt_and_setup_ssl "${existing_port}" "${config_webBasePath}" "${server_ip}" - echo -e "${green}Access URL: ${SSL_SCHEME}://${SSL_HOST}:${existing_port}/${config_webBasePath}${plain}" - else - # If a cert already exists, just show the access URL - echo -e "${green}Access URL: https://${server_ip}:${existing_port}/${config_webBasePath}${plain}" - fi - fi - else - if [[ "$existing_hasDefaultCredential" == "true" ]]; then - local config_username="${XUI_USERNAME:-$(gen_random_string 10)}" - local config_password="${XUI_PASSWORD:-$(gen_random_string 10)}" - - echo -e "${yellow}Default credentials detected. Security update required...${plain}" - ${xui_folder}/x-ui setting -username "${config_username}" -password "${config_password}" - echo -e "Generated new random login credentials:" - echo -e "###############################################" - echo -e "${green}Username: ${config_username}${plain}" - echo -e "${green}Password: ${config_password}${plain}" - echo -e "###############################################" - - # Persist a machine-parseable credentials file for cloud-init / MOTD. - local config_apiToken - config_apiToken=$(${xui_folder}/x-ui setting -getApiToken true | grep -Eo 'apiToken: .+' | awk '{print $2}') - : "${SSL_SCHEME:=https}" - : "${SSL_HOST:=${server_ip}}" - write_install_result "${config_username}" "${config_password}" "${existing_port}" \ - "${existing_webBasePath}" "${SSL_SCHEME}" "${SSL_HOST}" "${config_apiToken}" "${XUI_DB_TYPE:-sqlite}" - else - echo -e "${green}Username, Password, and WebBasePath are properly set.${plain}" - fi - - # Existing install: if no cert configured, prompt user for SSL setup - # Properly detect empty cert by checking if cert: line exists and has content after it - existing_cert=$(${xui_folder}/x-ui setting -getCert true | grep 'cert:' | awk -F': ' '{print $2}' | tr -d '[:space:]') - if [[ -z "$existing_cert" ]]; then - echo "" - echo -e "${green}═══════════════════════════════════════════${plain}" - echo -e "${green} SSL Certificate Setup (RECOMMENDED) ${plain}" - echo -e "${green}═══════════════════════════════════════════${plain}" - echo -e "${yellow}Let's Encrypt now supports both domains and IP addresses!${plain}" - echo "" - prompt_and_setup_ssl "${existing_port}" "${existing_webBasePath}" "${server_ip}" - echo -e "${green}Access URL: ${SSL_SCHEME}://${SSL_HOST}:${existing_port}/${existing_webBasePath}${plain}" - else - echo -e "${green}SSL certificate already configured. No action needed.${plain}" - fi - fi - - ${xui_folder}/x-ui migrate -} - -# setup_fail2ban auto-installs and configures fail2ban for the IP Limit feature -# by invoking the freshly installed x-ui CLI. IP Limit is load-bearing on -# fail2ban (without it the panel disables the limitIp field and zeroes existing -# limits), so a fresh install should make it work out of the box, just like the -# Docker image already does. Non-fatal by design: a fail2ban failure must never -# abort the panel install. -setup_fail2ban() { - if [[ -n "${XUI_ENABLE_FAIL2BAN+x}" && "${XUI_ENABLE_FAIL2BAN}" != "true" ]]; then - echo -e "${yellow}XUI_ENABLE_FAIL2BAN=${XUI_ENABLE_FAIL2BAN}, skipping Fail2ban auto-setup.${plain}" - return 0 - fi - - if [[ ! -x /usr/bin/x-ui ]]; then - echo -e "${yellow}x-ui CLI not found; skipping Fail2ban auto-setup.${plain}" - return 0 - fi - - echo -e "${green}Setting up Fail2ban for the IP Limit feature...${plain}" - if /usr/bin/x-ui setup-fail2ban; then - echo -e "${green}Fail2ban setup complete.${plain}" - else - echo -e "${yellow}Fail2ban setup did not finish; IP Limit stays disabled until you run 'x-ui' and open the IP Limit menu. Continuing.${plain}" - fi - return 0 -} - -# Lands a systemd unit file at ${xui_service}/x-ui.service via a temp file + -# atomic mv, so a failed cp/curl or an interrupted mv never leaves a -# truncated unit file at the live path -- systemd would then fail to parse -# it on the next daemon-reload/start. Same pattern already used for -# /usr/bin/x-ui elsewhere in this script. source_is_url picks cp (from a -# file already extracted from the release tarball) vs curl (GitHub fallback). -_install_xui_service_unit() { - local source="$1" - local source_is_url="$2" - local dest="${xui_service}/x-ui.service" - local temp_file="${dest}.tmp.$$" - - rm -f "$temp_file" - if [[ "$source_is_url" == "true" ]]; then - curl -fLRo "$temp_file" "$source" > /dev/null 2>&1 - else - cp -f "$source" "$temp_file" > /dev/null 2>&1 - fi - if [[ $? -ne 0 ]]; then - rm -f "$temp_file" - return 1 - fi - if [[ ! -s "$temp_file" ]]; then - rm -f "$temp_file" - return 1 - fi - mv -f "$temp_file" "$dest" - if [[ $? -ne 0 ]]; then - rm -f "$temp_file" - return 1 - fi - return 0 -} - -# resolve_latest_tag prints the latest stable release tag. It prefers the web -# releases/latest redirect, which is not subject to the unauthenticated API's -# 60 req/h-per-IP limit that trips shared CI/CGNAT addresses (the install then -# fails with "Failed to fetch x-ui version"), and falls back to the API. -resolve_latest_tag() { - local url tag - url=$(curl -sSLI -o /dev/null -w '%{url_effective}' --retry 5 --retry-delay 3 --connect-timeout 15 --max-time 60 "https://github.com/MHSanaei/3x-ui/releases/latest" 2>/dev/null) - tag=${url##*/tag/} - if [[ "$tag" != "$url" && -n "$tag" && "$tag" != "latest" ]]; then - echo "$tag" - return 0 - fi - curl -Ls --retry 5 --retry-delay 3 --connect-timeout 15 --max-time 60 "https://api.github.com/repos/MHSanaei/3x-ui/releases/latest" | grep '"tag_name":' | sed -E 's/.*"([^"]+)".*/\1/' -} - -install_x-ui() { - cd ${xui_folder%/x-ui}/ - - # Download resources - if [ $# == 0 ]; then - tag_version=$(resolve_latest_tag) - if [[ ! -n "$tag_version" ]]; then - echo -e "${red}Failed to fetch x-ui version, it may be due to GitHub API restrictions, please try it later${plain}" - exit 1 - fi - echo -e "Got x-ui latest version: ${tag_version}, beginning the installation..." - curl -fLR --retry 5 --retry-delay 3 --connect-timeout 15 --speed-limit 1 --speed-time 300 -o ${xui_folder}-linux-$(arch).tar.gz https://github.com/MHSanaei/3x-ui/releases/download/${tag_version}/x-ui-linux-$(arch).tar.gz - if [[ $? -ne 0 ]]; then - echo -e "${red}Downloading x-ui failed, please be sure that your server can access GitHub ${plain}" - exit 1 - fi - if [[ ! -s ${xui_folder}-linux-$(arch).tar.gz ]]; then - rm ${xui_folder}-linux-$(arch).tar.gz -f - echo -e "${red}Downloaded x-ui release archive is empty${plain}" - exit 1 - fi - else - tag_version=$1 - # The rolling dev channel ships under a fixed, non-semver tag that is - # force-moved to the latest main commit on every push. Accept `dev` as a - # convenient alias and skip the numeric floor check for it. - if [[ "$tag_version" == "dev" || "$tag_version" == "dev-latest" ]]; then - tag_version="dev-latest" - echo -e "${yellow}Installing the rolling dev build (tag: dev-latest). This is a per-commit pre-release, not a stable version.${plain}" - else - tag_version_numeric=${tag_version#v} - min_version="2.3.5" - - if [[ "$(printf '%s\n' "$min_version" "$tag_version_numeric" | sort -V | head -n1)" != "$min_version" ]]; then - echo -e "${red}Please use a newer version (at least v2.3.5). Exiting installation.${plain}" - exit 1 - fi - fi - - url="https://github.com/MHSanaei/3x-ui/releases/download/${tag_version}/x-ui-linux-$(arch).tar.gz" - echo -e "Beginning to install x-ui ${tag_version}" - curl -fLR --retry 5 --retry-delay 3 --connect-timeout 15 --speed-limit 1 --speed-time 300 -o ${xui_folder}-linux-$(arch).tar.gz ${url} - if [[ $? -ne 0 ]]; then - echo -e "${red}Download x-ui ${tag_version} failed, please check if the version exists ${plain}" - exit 1 - fi - if [[ ! -s ${xui_folder}-linux-$(arch).tar.gz ]]; then - rm ${xui_folder}-linux-$(arch).tar.gz -f - echo -e "${red}Downloaded x-ui release archive is empty${plain}" - exit 1 - fi - fi - local xui_script_temp="/usr/bin/x-ui-temp.$$" - rm -f "${xui_script_temp}" - curl -fLRo "${xui_script_temp}" https://raw.githubusercontent.com/MHSanaei/3x-ui/main/x-ui.sh - if [[ $? -ne 0 ]]; then - rm -f "${xui_script_temp}" - echo -e "${red}Failed to download x-ui.sh${plain}" - exit 1 - fi - if [[ ! -s "${xui_script_temp}" ]]; then - rm -f "${xui_script_temp}" - echo -e "${red}Downloaded x-ui.sh is empty${plain}" - exit 1 - fi - - # Stop x-ui service and remove old resources - local custom_bin_backup="" - if [[ -e ${xui_folder}/ ]]; then - if [[ $release == "alpine" ]]; then - rc-service x-ui stop - else - systemctl stop x-ui - fi - # Kill any leftover mtg (MTProto) sidecars. x-ui runs them outside its own - # lifecycle, so on Linux a stale one can survive the stop and keep holding - # an inbound port with an outdated secret, silently breaking new clients. - # The freshly installed panel respawns a clean mtg per inbound on start. - pkill -f 'mtg-linux-[^ ]* run ' > /dev/null 2>&1 || true - - # bin/ is about to be wiped wholesale by the tar extraction below. The - # release only ships known assets (xray/mtg binaries, the bundled - # geoip*/geosite*.dat sets) -- anything else in bin/ was placed there - # by the admin (e.g. a hand-added custom geoip/geosite file referenced - # from a routing rule via "ext::") and would otherwise be - # silently deleted on every update, breaking Xray at next start with - # "failed to open : no such file or directory" for any routing - # rule that references it. Moved aside rather than copied: a rename - # on the same filesystem is atomic (no truncated file if disk space - # runs out mid-copy, unlike `cp`) and keeps the snapshot under - # /usr/local rather than a separate, possibly small/tmpfs $TMPDIR. - if [[ -d "${xui_folder}/bin" ]]; then - custom_bin_backup="${xui_folder%/x-ui}/x-ui-bin-backup.$$" - rm -rf "${custom_bin_backup}" - if ! mv "${xui_folder}/bin" "${custom_bin_backup}"; then - custom_bin_backup="" - echo -e "${yellow}Could not back up bin/ -- custom files there will not be preserved across this update${plain}" - fi - fi - # Sole cleanup path for the backup from here on -- covers both the - # two `exit 1`s below (extraction/binary-missing failures) and an - # interrupted update (Ctrl-C, signal) before the restore runs. - # Cleared once the restore below finishes normally. - trap '[[ -n "${custom_bin_backup}" ]] && rm -rf "${custom_bin_backup}"' EXIT INT TERM - rm ${xui_folder}/ -rf - fi - - # Extract resources and set permissions - tar zxvf x-ui-linux-$(arch).tar.gz - if [[ $? -ne 0 ]]; then - rm x-ui-linux-$(arch).tar.gz -f - rm -f "${xui_script_temp}" - echo -e "${red}Failed to extract the x-ui release archive -- the previous installation has already been removed, so the panel will not start until this is fixed; try running the installer again${plain}" - exit 1 - fi - rm x-ui-linux-$(arch).tar.gz -f - - cd x-ui - if [[ $? -ne 0 || ! -s x-ui ]]; then - rm -f "${xui_script_temp}" - echo -e "${red}Extracted x-ui archive is missing the x-ui binary -- the previous installation has already been removed, so the panel will not start until this is fixed; try running the installer again${plain}" - exit 1 - fi - chmod +x x-ui - chmod +x x-ui.sh - - # Check the system's architecture and rename the file accordingly. - # The panel binary maps GOARCH=arm to "arm32" (internal/xray/process.go), - # so the Xray binary must be named xray-linux-arm32; mtg keeps plain "arm". - if [[ $(arch) == "armv5" || $(arch) == "armv6" || $(arch) == "armv7" ]]; then - mv bin/xray-linux-$(arch) bin/xray-linux-arm32 - chmod +x bin/xray-linux-arm32 - if [[ -f bin/mtg-linux-$(arch) ]]; then - mv bin/mtg-linux-$(arch) bin/mtg-linux-arm - chmod +x bin/mtg-linux-arm - fi - fi - chmod +x x-ui bin/xray-linux-$(arch) - if [[ -f bin/mtg-linux-arm ]]; then - chmod +x bin/mtg-linux-arm - elif [[ -f bin/mtg-linux-$(arch) ]]; then - chmod +x bin/mtg-linux-$(arch) - fi - - # Restore anything from the old bin/ that the fresh release doesn't ship - # (custom geoip/geosite files, or anything else an admin hand-placed - # there) -- never overwrites a same-named file the new release provides, - # so bundled assets (geoip.dat, geoip_RU.dat, ...) still get the fresh - # per-release copy. Runs after the arch-rename above so xray-linux-arm32/ - # mtg-linux-arm already exist under their final names there and aren't - # mistaken for custom files needing a restore. Skips paths the panel - # itself regenerates at runtime (config.json, mtproto/*.toml -- see - # internal/xray/process.go, internal/mtproto/manager.go): those aren't - # admin-placed, and restoring a stale one only resurrects dead state (an - # orphaned mtg config for a since-deleted inbound) or the wrong - # directory permissions. - if [[ -n "${custom_bin_backup}" ]]; then - local restored_custom_bin=() - while IFS= read -r -d '' f; do - local rel="${f#"${custom_bin_backup}"/}" - case "${rel}" in - config.json | mtproto | mtproto/*) continue ;; - esac - if [[ ! -e "bin/${rel}" ]]; then - mkdir -p "bin/$(dirname "${rel}")" - cp -a "${f}" "bin/${rel}" - restored_custom_bin+=("${rel}") - fi - done < <(find "${custom_bin_backup}" \( -type f -o -type l \) -print0) - rm -rf "${custom_bin_backup}" - custom_bin_backup="" - if [[ ${#restored_custom_bin[@]} -gt 0 ]]; then - echo -e "${green}Restored custom file(s) in bin/ not shipped by this release: ${restored_custom_bin[*]}${plain}" - fi - fi - trap - EXIT INT TERM - - # Update x-ui cli and se set permission - mv -f "${xui_script_temp}" /usr/bin/x-ui - if [[ $? -ne 0 ]]; then - rm -f "${xui_script_temp}" - echo -e "${red}Failed to install x-ui.sh${plain}" - exit 1 - fi - chmod +x /usr/bin/x-ui - mkdir -p /var/log/x-ui - config_after_install - - # Etckeeper compatibility - if [ -d "/etc/.git" ]; then - if [ -f "/etc/.gitignore" ]; then - if ! grep -q "x-ui/x-ui.db" "/etc/.gitignore"; then - echo "" >> "/etc/.gitignore" - echo "x-ui/x-ui.db" >> "/etc/.gitignore" - echo -e "${green}Added x-ui.db to /etc/.gitignore for etckeeper${plain}" - fi - else - echo "x-ui/x-ui.db" > "/etc/.gitignore" - echo -e "${green}Created /etc/.gitignore and added x-ui.db for etckeeper${plain}" - fi - fi - - if [[ $release == "alpine" ]]; then - xui_rc_temp="/etc/init.d/x-ui.tmp.$$" - rm -f "${xui_rc_temp}" - curl -fLRo "${xui_rc_temp}" https://raw.githubusercontent.com/MHSanaei/3x-ui/main/x-ui.rc - if [[ $? -ne 0 ]]; then - rm -f "${xui_rc_temp}" - echo -e "${red}Failed to download x-ui.rc${plain}" - exit 1 - fi - if [[ ! -s "${xui_rc_temp}" ]]; then - rm -f "${xui_rc_temp}" - echo -e "${red}Downloaded x-ui.rc is empty${plain}" - exit 1 - fi - mv -f "${xui_rc_temp}" /etc/init.d/x-ui - if [[ $? -ne 0 ]]; then - rm -f "${xui_rc_temp}" - echo -e "${red}Failed to install x-ui.rc${plain}" - exit 1 - fi - chmod +x /etc/init.d/x-ui - rc-update add x-ui - rc-service x-ui start - else - # Install systemd service file - service_installed=false - - if [ -f "x-ui.service" ]; then - echo -e "${green}Found x-ui.service in extracted files, installing...${plain}" - if _install_xui_service_unit "x-ui.service" "false"; then - service_installed=true - fi - fi - - if [ "$service_installed" = false ]; then - case "${release}" in - ubuntu | debian | armbian) - if [ -f "x-ui.service.debian" ]; then - echo -e "${green}Found x-ui.service.debian in extracted files, installing...${plain}" - if _install_xui_service_unit "x-ui.service.debian" "false"; then - service_installed=true - fi - fi - ;; - arch | manjaro | parch) - if [ -f "x-ui.service.arch" ]; then - echo -e "${green}Found x-ui.service.arch in extracted files, installing...${plain}" - if _install_xui_service_unit "x-ui.service.arch" "false"; then - service_installed=true - fi - fi - ;; - *) - if [ -f "x-ui.service.rhel" ]; then - echo -e "${green}Found x-ui.service.rhel in extracted files, installing...${plain}" - if _install_xui_service_unit "x-ui.service.rhel" "false"; then - service_installed=true - fi - fi - ;; - esac - fi - - # If service file not found in tar.gz, download from GitHub - if [ "$service_installed" = false ]; then - echo -e "${yellow}Service files not found in tar.gz, downloading from GitHub...${plain}" - case "${release}" in - ubuntu | debian | armbian) - service_unit_url="https://raw.githubusercontent.com/MHSanaei/3x-ui/main/x-ui.service.debian" - ;; - arch | manjaro | parch) - service_unit_url="https://raw.githubusercontent.com/MHSanaei/3x-ui/main/x-ui.service.arch" - ;; - *) - service_unit_url="https://raw.githubusercontent.com/MHSanaei/3x-ui/main/x-ui.service.rhel" - ;; - esac - - if ! _install_xui_service_unit "$service_unit_url" "true"; then - echo -e "${red}Failed to install x-ui.service from GitHub${plain}" - exit 1 - fi - service_installed=true - fi - - if [ "$service_installed" = true ]; then - echo -e "${green}Setting up systemd unit...${plain}" - chown root:root ${xui_service}/x-ui.service > /dev/null 2>&1 - chmod 644 ${xui_service}/x-ui.service > /dev/null 2>&1 - systemctl daemon-reload - systemctl enable x-ui - systemctl start x-ui - else - echo -e "${red}Failed to install x-ui.service file${plain}" - exit 1 - fi - fi - - # IP Limit relies on fail2ban; install + configure it now so the feature - # works out of the box (no-op when XUI_ENABLE_FAIL2BAN=false). Never fatal. - setup_fail2ban - - echo -e "${green}x-ui ${tag_version}${plain} installation finished, it is running now..." - echo -e "" - echo -e "┌───────────────────────────────────────────────────────┐ -│ ${blue}x-ui control menu usages (subcommands):${plain} │ -│ │ -│ ${blue}x-ui${plain} - Admin Management Script │ -│ ${blue}x-ui start${plain} - Start │ -│ ${blue}x-ui stop${plain} - Stop │ -│ ${blue}x-ui restart${plain} - Restart │ -│ ${blue}x-ui status${plain} - Current Status │ -│ ${blue}x-ui settings${plain} - Current Settings │ -│ ${blue}x-ui enable${plain} - Enable Autostart on OS Startup │ -│ ${blue}x-ui disable${plain} - Disable Autostart on OS Startup │ -│ ${blue}x-ui log${plain} - Check logs │ -│ ${blue}x-ui banlog${plain} - Check Fail2ban ban logs │ -│ ${blue}x-ui update${plain} - Update │ -│ ${blue}x-ui legacy${plain} - Legacy version │ -│ ${blue}x-ui install${plain} - Install │ -│ ${blue}x-ui uninstall${plain} - Uninstall │ -└───────────────────────────────────────────────────────┘" -} - -echo -e "${green}Running...${plain}" -install_base -install_x-ui $1 +# 3x-ui-agent (fork) — the upstream one-click installer is intentionally +# disabled in this fork: running it would install the stock 3x-ui panel. +# 本 fork 不提供上游一键安装(运行上游脚本会装成原版 3x-ui)。 +# 请按 README 手动部署,或使用本仓的一键脚本 install-master.sh / install-agent.sh; +# 二进制见本仓 Releases:https://git.881218.xyz/rogue/3x-ui-agent/releases +echo "This fork (3x-ui-agent) does not provide the upstream one-click installer; see README.md and https://git.881218.xyz/rogue/3x-ui-agent/releases" +echo "本 fork 不提供上游一键安装(会装成原版)。请按 README 部署:https://git.881218.xyz/rogue/3x-ui-agent/releases" +exit 1 diff --git a/internal/web/service/panel/panel.go b/internal/web/service/panel/panel.go index 0e2a9fa..ac8d810 100644 --- a/internal/web/service/panel/panel.go +++ b/internal/web/service/panel/panel.go @@ -40,7 +40,7 @@ type PanelUpdateInfo struct { } const ( - panelUpdaterURL = "https://raw.githubusercontent.com/MHSanaei/3x-ui/main/update.sh" + panelUpdaterURL = "https://git.881218.xyz/rogue/3x-ui-agent/raw/branch/main/update.sh" maxPanelUpdaterBytes = 2 << 20 // devReleaseTag is the fixed-tag rolling pre-release the CI force-moves to the // newest main commit; the dev update channel installs from it. @@ -125,7 +125,7 @@ func (s *PanelService) RestartPanel(delay time.Duration) error { return nil } -// GetUpdateInfo checks GitHub for the latest 3x-ui release. When the dev channel +// GetUpdateInfo checks this fork's Gitea repository for the latest release. When the dev channel // is enabled on a dev build it compares commits against the rolling dev release; // otherwise it compares versions against the latest stable tag. func (s *PanelService) GetUpdateInfo() (*PanelUpdateInfo, error) { @@ -427,12 +427,14 @@ func fetchLatestPanelVersion() (string, error) { return release.TagName, nil } -// fetchPanelRelease fetches a release from GitHub. An empty tag resolves the -// latest stable release; a non-empty tag (e.g. dev-latest) resolves that tag. +// fetchPanelRelease fetches a release from this fork's Gitea repository +// (its API returns the same tag_name/body/target_commitish shape). An empty +// tag resolves the latest stable release; a non-empty tag (e.g. dev-latest) +// resolves that tag. func fetchPanelRelease(tag string) (*service.Release, error) { - url := "https://api.github.com/repos/MHSanaei/3x-ui/releases/latest" + url := "https://git.881218.xyz/api/v1/repos/rogue/3x-ui-agent/releases/latest" if tag != "" { - url = "https://api.github.com/repos/MHSanaei/3x-ui/releases/tags/" + tag + url = "https://git.881218.xyz/api/v1/repos/rogue/3x-ui-agent/releases/tags/" + tag } client := (&service.SettingService{}).NewProxiedHTTPClient(10 * time.Second) req, reqErr := http.NewRequestWithContext(context.Background(), http.MethodGet, url, nil) @@ -445,7 +447,7 @@ func fetchPanelRelease(tag string) (*service.Release, error) { } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { - return nil, fmt.Errorf("GitHub API returned status %d: %s", resp.StatusCode, resp.Status) + return nil, fmt.Errorf("release API returned status %d: %s", resp.StatusCode, resp.Status) } var release service.Release diff --git a/update.sh b/update.sh index e3945b0..e6261ea 100755 --- a/update.sh +++ b/update.sh @@ -1,1233 +1,10 @@ #!/bin/bash - -red='\033[0;31m' -green='\033[0;32m' -blue='\033[0;34m' -yellow='\033[0;33m' -plain='\033[0m' - -xui_folder="${XUI_MAIN_FOLDER:=/usr/local/x-ui}" -xui_service="${XUI_SERVICE:=/etc/systemd/system}" - -# Don't edit this config -b_source="${BASH_SOURCE[0]}" -while [ -h "$b_source" ]; do - b_dir="$(cd -P "$(dirname "$b_source")" > /dev/null 2>&1 && pwd || pwd -P)" - b_source="$(readlink "$b_source")" - [[ $b_source != /* ]] && b_source="$b_dir/$b_source" -done -cur_dir="$(cd -P "$(dirname "$b_source")" > /dev/null 2>&1 && pwd || pwd -P)" -script_name=$(basename "$0") - -# Check command exist function -_command_exists() { - type "$1" &> /dev/null -} - -# Fail, log and exit script function -_fail() { - local msg=${1} - echo -e "${red}${msg}${plain}" - exit 2 -} - -# Records this run's outcome for the panel's web updater to poll, since it -# launches this script detached and has no other way to learn whether it -# finished. Written to a fixed path outside XUI_MAIN_FOLDER so it survives -# the update regardless of what happens to that folder. The EXIT trap below -# covers every exit path in this file, including the bare `exit 1`/`exit 2` -# calls that don't go through _fail. -xui_update_run_id="${XUI_UPDATE_RUN_ID:-0}" -[[ "${xui_update_run_id}" =~ ^[0-9]+$ ]] || xui_update_run_id="0" -xui_update_status_file="${XUI_UPDATE_STATUS_FILE:-/etc/x-ui/update-status.json}" - -_write_update_status() { - local state="$1" - local exit_code="$2" - local status_dir - status_dir="$(dirname "${xui_update_status_file}")" - mkdir -p "${status_dir}" > /dev/null 2>&1 - local tmp_file="${xui_update_status_file}.tmp.$$" - printf '{"runId":"%s","state":"%s","exitCode":%s,"finishedAt":%s}\n' \ - "${xui_update_run_id}" "${state}" "${exit_code}" "$(date +%s)" > "${tmp_file}" 2> /dev/null - mv -f "${tmp_file}" "${xui_update_status_file}" > /dev/null 2>&1 -} - -_report_update_exit() { - local code=$? - if [[ "${code}" -eq 0 ]]; then - _write_update_status "success" "0" - else - _write_update_status "failed" "${code}" - fi -} -trap _report_update_exit EXIT -trap 'exit 143' TERM -trap 'exit 130' INT - -# check root -[[ $EUID -ne 0 ]] && _fail "FATAL ERROR: Please run this script with root privilege." - -if _command_exists curl; then - curl_bin=$(which curl) -else - _fail "ERROR: Command 'curl' not found." -fi - -# Check OS and set release variable -if [[ -f /etc/os-release ]]; then - source /etc/os-release - release=$ID -elif [[ -f /usr/lib/os-release ]]; then - source /usr/lib/os-release - release=$ID -else - _fail "Failed to check the system OS, please contact the author!" -fi -echo "The OS release is: $release" - -arch() { - case "$(uname -m)" in - x86_64 | x64 | amd64) echo 'amd64' ;; - i*86 | x86) echo '386' ;; - armv8* | armv8 | arm64 | aarch64) echo 'arm64' ;; - armv7* | armv7 | arm) echo 'armv7' ;; - armv6* | armv6) echo 'armv6' ;; - armv5* | armv5) echo 'armv5' ;; - s390x) echo 's390x' ;; - *) echo -e "${red}Unsupported CPU architecture!${plain}" && rm -f "${cur_dir}/${script_name}" > /dev/null 2>&1 && exit 2 ;; - esac -} - -echo "Arch: $(arch)" - -# Simple helpers -is_ipv4() { - [[ "$1" =~ ^([0-9]{1,3}\.){3}[0-9]{1,3}$ ]] && return 0 || return 1 -} -is_ipv6() { - [[ "$1" =~ : ]] && return 0 || return 1 -} -is_ip() { - is_ipv4 "$1" || is_ipv6 "$1" -} -is_domain() { - [[ "$1" =~ ^([A-Za-z0-9](-*[A-Za-z0-9])*\.)+(xn--[a-z0-9]{2,}|[A-Za-z]{2,})$ ]] && return 0 || return 1 -} - -# acme.sh's standalone server binds IPv4 by default; --listen-v6 makes it -# v6-only, which breaks HTTP-01 validation when the domain's A record points -# at this host's IPv4 (#4994). Only force IPv6 when the host has no global -# IPv4 address at all. -acme_listen_flag() { - if ip -4 addr show scope global 2> /dev/null | grep -q "inet "; then - echo "" - else - echo "--listen-v6" - fi -} - -# Port helpers -is_port_in_use() { - local port="$1" - if command -v ss > /dev/null 2>&1; then - ss -ltn 2> /dev/null | awk -v p=":${port}$" '$4 ~ p {exit 0} END {exit 1}' - return - fi - if command -v netstat > /dev/null 2>&1; then - netstat -lnt 2> /dev/null | awk -v p=":${port} " '$4 ~ p {exit 0} END {exit 1}' - return - fi - if command -v lsof > /dev/null 2>&1; then - lsof -nP -iTCP:${port} -sTCP:LISTEN > /dev/null 2>&1 && return 0 - fi - return 1 -} - -gen_random_string() { - local length="$1" - openssl rand -base64 $((length * 2)) \ - | tr -dc 'a-zA-Z0-9' \ - | head -c "$length" -} - -xui_env_file_path() { - case "${release}" in - ubuntu | debian | armbian) - echo "/etc/default/x-ui" - ;; - arch | manjaro | parch | alpine) - echo "/etc/conf.d/x-ui" - ;; - *) - echo "/etc/sysconfig/x-ui" - ;; - esac -} - -load_xui_env() { - local env_file - env_file="$(xui_env_file_path)" - if [[ -r "$env_file" ]]; then - set -a - # shellcheck disable=SC1090 - source "$env_file" - set +a - fi -} - -install_base() { - echo -e "${green}Updating and install dependency packages...${plain}" - case "${release}" in - ubuntu | debian | armbian) - apt-get update > /dev/null 2>&1 && apt-get install -y -q cron curl tar tzdata socat openssl > /dev/null 2>&1 - ;; - fedora | amzn | virtuozzo | rhel | almalinux | rocky | ol) - dnf makecache -y > /dev/null 2>&1 && dnf install -y -q cronie curl tar tzdata socat openssl > /dev/null 2>&1 - ;; - centos) - if [[ "${VERSION_ID}" =~ ^7 ]]; then - yum makecache -y > /dev/null 2>&1 && yum install -y -q cronie curl tar tzdata socat openssl > /dev/null 2>&1 - else - dnf makecache -y > /dev/null 2>&1 && dnf install -y -q cronie curl tar tzdata socat openssl > /dev/null 2>&1 - fi - ;; - arch | manjaro | parch) - pacman -Sy --noconfirm cronie curl tar tzdata socat openssl > /dev/null 2>&1 - ;; - opensuse-tumbleweed | opensuse-leap) - zypper refresh > /dev/null 2>&1 && zypper -q install -y cron curl tar timezone socat openssl > /dev/null 2>&1 - ;; - alpine) - apk update > /dev/null 2>&1 && apk add dcron curl tar tzdata socat openssl > /dev/null 2>&1 - ;; - *) - apt-get update > /dev/null 2>&1 && apt install -y -q cron curl tar tzdata socat openssl > /dev/null 2>&1 - ;; - esac -} - -install_acme() { - echo -e "${green}Installing acme.sh for SSL certificate management...${plain}" - cd ~ || return 1 - curl -s https://get.acme.sh | sh > /dev/null 2>&1 - if [ $? -ne 0 ]; then - echo -e "${red}Failed to install acme.sh${plain}" - return 1 - else - echo -e "${green}acme.sh installed successfully${plain}" - fi - return 0 -} - -setup_ssl_certificate() { - local domain="$1" - local server_ip="$2" - local existing_port="$3" - local existing_webBasePath="$4" - - echo -e "${green}Setting up SSL certificate...${plain}" - - # Check if acme.sh is installed - if ! command -v ~/.acme.sh/acme.sh &> /dev/null; then - install_acme - if [ $? -ne 0 ]; then - echo -e "${yellow}Failed to install acme.sh, skipping SSL setup${plain}" - return 1 - fi - fi - - # Create certificate directory - local certPath="/root/cert/${domain}" - mkdir -p "$certPath" - - # Issue certificate - echo -e "${green}Issuing SSL certificate for ${domain}...${plain}" - echo -e "${yellow}Note: Port 80 must be open and accessible from the internet${plain}" - - ~/.acme.sh/acme.sh --set-default-ca --server letsencrypt --force > /dev/null 2>&1 - ~/.acme.sh/acme.sh --issue -d ${domain} $(acme_listen_flag) --standalone --httpport 80 --force - - if [ $? -ne 0 ]; then - echo -e "${yellow}Failed to issue certificate for ${domain}${plain}" - echo -e "${yellow}Please ensure port 80 is open and try again later with: x-ui${plain}" - rm -rf ~/.acme.sh/${domain} 2> /dev/null - rm -rf "$certPath" 2> /dev/null - return 1 - fi - - # Install certificate - ~/.acme.sh/acme.sh --installcert --force -d ${domain} \ - --key-file /root/cert/${domain}/privkey.pem \ - --fullchain-file /root/cert/${domain}/fullchain.pem \ - --reloadcmd "systemctl restart x-ui" > /dev/null 2>&1 - - if [ $? -ne 0 ]; then - echo -e "${yellow}Failed to install certificate${plain}" - return 1 - fi - - # Enable auto-renew - ~/.acme.sh/acme.sh --upgrade --auto-upgrade > /dev/null 2>&1 - chmod 600 $certPath/privkey.pem 2> /dev/null - chmod 644 $certPath/fullchain.pem 2> /dev/null - - # Set certificate for panel - local webCertFile="/root/cert/${domain}/fullchain.pem" - local webKeyFile="/root/cert/${domain}/privkey.pem" - - if [[ -f "$webCertFile" && -f "$webKeyFile" ]]; then - ${xui_folder}/x-ui cert -webCert "$webCertFile" -webCertKey "$webKeyFile" > /dev/null 2>&1 - echo -e "${green}SSL certificate installed and configured successfully!${plain}" - return 0 - else - echo -e "${yellow}Certificate files not found${plain}" - return 1 - fi -} - -# Issue Let's Encrypt IP certificate with shortlived profile (~6 days validity) -# Requires acme.sh and port 80 open for HTTP-01 challenge -setup_ip_certificate() { - local ipv4="$1" - local ipv6="$2" # optional - - echo -e "${green}Setting up Let's Encrypt IP certificate (shortlived profile)...${plain}" - echo -e "${yellow}Note: IP certificates are valid for ~6 days and will auto-renew.${plain}" - echo -e "${yellow}Default listener is port 80. If you choose another port, ensure external port 80 forwards to it.${plain}" - - # Check for acme.sh - if ! command -v ~/.acme.sh/acme.sh &> /dev/null; then - install_acme - if [ $? -ne 0 ]; then - echo -e "${red}Failed to install acme.sh${plain}" - return 1 - fi - fi - - # Validate IP address - if [[ -z "$ipv4" ]]; then - echo -e "${red}IPv4 address is required${plain}" - return 1 - fi - - if ! is_ipv4 "$ipv4"; then - echo -e "${red}Invalid IPv4 address: $ipv4${plain}" - return 1 - fi - - # Create certificate directory - local certDir="/root/cert/ip" - mkdir -p "$certDir" - - # Build domain arguments - local domain_args="-d ${ipv4}" - if [[ -n "$ipv6" ]] && is_ipv6 "$ipv6"; then - domain_args="${domain_args} -d ${ipv6}" - echo -e "${green}Including IPv6 address: ${ipv6}${plain}" - fi - - # Set reload command for auto-renewal (add || true so it doesn't fail if service stopped) - local reloadCmd="systemctl restart x-ui 2>/dev/null || rc-service x-ui restart 2>/dev/null || true" - - # Choose port for HTTP-01 listener (default 80, prompt override) - local WebPort="" - read -rp "Port to use for ACME HTTP-01 listener (default 80): " WebPort - WebPort="${WebPort:-80}" - if ! [[ "${WebPort}" =~ ^[0-9]+$ ]] || ((WebPort < 1 || WebPort > 65535)); then - echo -e "${red}Invalid port provided. Falling back to 80.${plain}" - WebPort=80 - fi - echo -e "${green}Using port ${WebPort} for standalone validation.${plain}" - if [[ "${WebPort}" -ne 80 ]]; then - echo -e "${yellow}Reminder: Let's Encrypt still connects on port 80; forward external port 80 to ${WebPort}.${plain}" - fi - - # Ensure chosen port is available - while true; do - if is_port_in_use "${WebPort}"; then - echo -e "${yellow}Port ${WebPort} is currently in use.${plain}" - - local alt_port="" - read -rp "Enter another port for acme.sh standalone listener (leave empty to abort): " alt_port - alt_port="${alt_port// /}" - if [[ -z "${alt_port}" ]]; then - echo -e "${red}Port ${WebPort} is busy; cannot proceed.${plain}" - return 1 - fi - if ! [[ "${alt_port}" =~ ^[0-9]+$ ]] || ((alt_port < 1 || alt_port > 65535)); then - echo -e "${red}Invalid port provided.${plain}" - return 1 - fi - WebPort="${alt_port}" - continue - else - echo -e "${green}Port ${WebPort} is free and ready for standalone validation.${plain}" - break - fi - done - - # Issue certificate with shortlived profile - echo -e "${green}Issuing IP certificate for ${ipv4}...${plain}" - ~/.acme.sh/acme.sh --set-default-ca --server letsencrypt --force > /dev/null 2>&1 - - ~/.acme.sh/acme.sh --issue \ - ${domain_args} \ - --standalone \ - --server letsencrypt \ - --certificate-profile shortlived \ - --days 6 \ - --httpport ${WebPort} \ - --force - - if [ $? -ne 0 ]; then - echo -e "${red}Failed to issue IP certificate${plain}" - echo -e "${yellow}Please ensure port ${WebPort} is reachable (or forwarded from external port 80)${plain}" - # Cleanup acme.sh data for both IPv4 and IPv6 if specified - rm -rf ~/.acme.sh/${ipv4} 2> /dev/null - [[ -n "$ipv6" ]] && rm -rf ~/.acme.sh/${ipv6} 2> /dev/null - rm -rf ${certDir} 2> /dev/null - return 1 - fi - - echo -e "${green}Certificate issued successfully, installing...${plain}" - - # Install certificate - # Note: acme.sh may report "Reload error" and exit non-zero if reloadcmd fails, - # but the cert files are still installed. We check for files instead of exit code. - ~/.acme.sh/acme.sh --installcert --force -d ${ipv4} \ - --key-file "${certDir}/privkey.pem" \ - --fullchain-file "${certDir}/fullchain.pem" \ - --reloadcmd "${reloadCmd}" 2>&1 || true - - # Verify certificate files exist (don't rely on exit code - reloadcmd failure causes non-zero) - if [[ ! -f "${certDir}/fullchain.pem" || ! -f "${certDir}/privkey.pem" ]]; then - echo -e "${red}Certificate files not found after installation${plain}" - # Cleanup acme.sh data for both IPv4 and IPv6 if specified - rm -rf ~/.acme.sh/${ipv4} 2> /dev/null - [[ -n "$ipv6" ]] && rm -rf ~/.acme.sh/${ipv6} 2> /dev/null - rm -rf ${certDir} 2> /dev/null - return 1 - fi - - echo -e "${green}Certificate files installed successfully${plain}" - - # Enable auto-upgrade for acme.sh (ensures cron job runs) - ~/.acme.sh/acme.sh --upgrade --auto-upgrade > /dev/null 2>&1 - - chmod 600 ${certDir}/privkey.pem 2> /dev/null - chmod 644 ${certDir}/fullchain.pem 2> /dev/null - - # Configure panel to use the certificate - echo -e "${green}Setting certificate paths for the panel...${plain}" - ${xui_folder}/x-ui cert -webCert "${certDir}/fullchain.pem" -webCertKey "${certDir}/privkey.pem" - if [ $? -ne 0 ]; then - echo -e "${yellow}Warning: Could not set certificate paths automatically.${plain}" - echo -e "${yellow}You may need to set them manually in the panel settings.${plain}" - echo -e "${yellow}Cert path: ${certDir}/fullchain.pem${plain}" - echo -e "${yellow}Key path: ${certDir}/privkey.pem${plain}" - else - echo -e "${green}Certificate paths set successfully!${plain}" - fi - - echo -e "${green}IP certificate installed and configured successfully!${plain}" - echo -e "${green}Certificate valid for ~6 days, auto-renews via acme.sh cron job.${plain}" - echo -e "${yellow}Panel will automatically restart after each renewal.${plain}" - return 0 -} - -# Comprehensive manual SSL certificate issuance via acme.sh -ssl_cert_issue() { - local existing_webBasePath=$(${xui_folder}/x-ui setting -show true | grep 'webBasePath:' | awk -F': ' '{print $2}' | tr -d '[:space:]' | sed 's#^/##') - local existing_port=$(${xui_folder}/x-ui setting -show true | grep 'port:' | awk -F': ' '{print $2}' | tr -d '[:space:]') - - # check for acme.sh first - if ! command -v ~/.acme.sh/acme.sh &> /dev/null; then - echo "acme.sh could not be found. Installing now..." - cd ~ || return 1 - curl -s https://get.acme.sh | sh - if [ $? -ne 0 ]; then - echo -e "${red}Failed to install acme.sh${plain}" - return 1 - else - echo -e "${green}acme.sh installed successfully${plain}" - fi - fi - - # get the domain here, and we need to verify it - local domain="" - while true; do - read -rp "Please enter your domain name: " domain - domain="${domain// /}" # Trim whitespace - - if [[ -z "$domain" ]]; then - echo -e "${red}Domain name cannot be empty. Please try again.${plain}" - continue - fi - - if ! is_domain "$domain"; then - echo -e "${red}Invalid domain format: ${domain}. Please enter a valid domain name.${plain}" - continue - fi - - break - done - echo -e "${green}Your domain is: ${domain}, checking it...${plain}" - SSL_ISSUED_DOMAIN="${domain}" - - # detect existing certificate and reuse it if present - local cert_exists=0 - if ~/.acme.sh/acme.sh --list 2> /dev/null | awk '{print $1}' | grep -Fxq "${domain}"; then - cert_exists=1 - local certInfo=$(~/.acme.sh/acme.sh --list 2> /dev/null | grep -F "${domain}") - echo -e "${yellow}Existing certificate found for ${domain}, will reuse it.${plain}" - [[ -n "${certInfo}" ]] && echo "$certInfo" - else - echo -e "${green}Your domain is ready for issuing certificates now...${plain}" - fi - - # create a directory for the certificate - certPath="/root/cert/${domain}" - if [ ! -d "$certPath" ]; then - mkdir -p "$certPath" - else - rm -rf "$certPath" - mkdir -p "$certPath" - fi - - # get the port number for the standalone server - local WebPort=80 - read -rp "Please choose which port to use (default is 80): " WebPort - if [[ -z ${WebPort} ]]; then - WebPort=80 - elif [[ ! ${WebPort} =~ ^[1-9][0-9]*$ || ${WebPort} -gt 65535 ]]; then - echo -e "${yellow}Your input ${WebPort} is invalid, will use default port 80.${plain}" - WebPort=80 - fi - echo -e "${green}Will use port: ${WebPort} to issue certificates. Please make sure this port is open.${plain}" - - # Stop panel temporarily - echo -e "${yellow}Stopping panel temporarily...${plain}" - systemctl stop x-ui 2> /dev/null || rc-service x-ui stop 2> /dev/null - - if [[ ${cert_exists} -eq 0 ]]; then - # issue the certificate - ~/.acme.sh/acme.sh --set-default-ca --server letsencrypt --force - ~/.acme.sh/acme.sh --issue -d ${domain} $(acme_listen_flag) --standalone --httpport ${WebPort} --force - if [ $? -ne 0 ]; then - echo -e "${red}Issuing certificate failed, please check logs.${plain}" - rm -rf ~/.acme.sh/${domain} - systemctl start x-ui 2> /dev/null || rc-service x-ui start 2> /dev/null - return 1 - else - echo -e "${green}Issuing certificate succeeded, installing certificates...${plain}" - fi - else - echo -e "${green}Using existing certificate, installing certificates...${plain}" - fi - - # Setup reload command - reloadCmd="systemctl restart x-ui || rc-service x-ui restart" - echo -e "${green}Default --reloadcmd for ACME is: ${yellow}systemctl restart x-ui || rc-service x-ui restart${plain}" - echo -e "${green}This command will run on every certificate issue and renew.${plain}" - read -rp "Would you like to modify --reloadcmd for ACME? (y/n): " setReloadcmd - if [[ "$setReloadcmd" == "y" || "$setReloadcmd" == "Y" ]]; then - echo -e "\n${green}\t1.${plain} Preset: systemctl reload nginx ; systemctl restart x-ui" - echo -e "${green}\t2.${plain} Input your own command" - echo -e "${green}\t0.${plain} Keep default reloadcmd" - read -rp "Choose an option: " choice - case "$choice" in - 1) - echo -e "${green}Reloadcmd is: systemctl reload nginx ; systemctl restart x-ui${plain}" - reloadCmd="systemctl reload nginx ; systemctl restart x-ui" - ;; - 2) - echo -e "${yellow}It's recommended to put x-ui restart at the end${plain}" - read -rp "Please enter your custom reloadcmd: " reloadCmd - echo -e "${green}Reloadcmd is: ${reloadCmd}${plain}" - ;; - *) - echo -e "${green}Keeping default reloadcmd${plain}" - ;; - esac - fi - - # install the certificate - local installOutput="" - installOutput=$(~/.acme.sh/acme.sh --installcert --force -d ${domain} \ - --key-file /root/cert/${domain}/privkey.pem \ - --fullchain-file /root/cert/${domain}/fullchain.pem --reloadcmd "${reloadCmd}" 2>&1) - local installRc=$? - echo "${installOutput}" - - local installWroteFiles=0 - if echo "${installOutput}" | grep -q "Installing key to:" && echo "${installOutput}" | grep -q "Installing full chain to:"; then - installWroteFiles=1 - fi - - if [[ -f "/root/cert/${domain}/privkey.pem" && -f "/root/cert/${domain}/fullchain.pem" && (${installRc} -eq 0 || ${installWroteFiles} -eq 1) ]]; then - echo -e "${green}Installing certificate succeeded, enabling auto renew...${plain}" - else - echo -e "${red}Installing certificate failed, exiting.${plain}" - if [[ ${cert_exists} -eq 0 ]]; then - rm -rf ~/.acme.sh/${domain} - fi - systemctl start x-ui 2> /dev/null || rc-service x-ui start 2> /dev/null - return 1 - fi - - # enable auto-renew - ~/.acme.sh/acme.sh --upgrade --auto-upgrade - if [ $? -ne 0 ]; then - echo -e "${yellow}Auto renew setup had issues, certificate details:${plain}" - ls -lah /root/cert/${domain}/ - chmod 600 $certPath/privkey.pem - chmod 644 $certPath/fullchain.pem - else - echo -e "${green}Auto renew succeeded, certificate details:${plain}" - ls -lah /root/cert/${domain}/ - chmod 600 $certPath/privkey.pem - chmod 644 $certPath/fullchain.pem - fi - - # Restart panel - systemctl start x-ui 2> /dev/null || rc-service x-ui start 2> /dev/null - - # Prompt user to set panel paths after successful certificate installation - read -rp "Would you like to set this certificate for the panel? (y/n): " setPanel - if [[ "$setPanel" == "y" || "$setPanel" == "Y" ]]; then - local webCertFile="/root/cert/${domain}/fullchain.pem" - local webKeyFile="/root/cert/${domain}/privkey.pem" - - if [[ -f "$webCertFile" && -f "$webKeyFile" ]]; then - ${xui_folder}/x-ui cert -webCert "$webCertFile" -webCertKey "$webKeyFile" - echo -e "${green}Certificate paths set for the panel${plain}" - echo -e "${green}Certificate File: $webCertFile${plain}" - echo -e "${green}Private Key File: $webKeyFile${plain}" - echo "" - echo -e "${green}Access URL: https://${domain}:${existing_port}/${existing_webBasePath}${plain}" - echo -e "${yellow}Panel will restart to apply SSL certificate...${plain}" - systemctl restart x-ui 2> /dev/null || rc-service x-ui restart 2> /dev/null - else - echo -e "${red}Error: Certificate or private key file not found for domain: $domain.${plain}" - fi - else - echo -e "${yellow}Skipping panel path setting.${plain}" - fi - - return 0 -} -# Unified interactive SSL setup (domain or IP) -# Sets global `SSL_HOST` to the chosen domain/IP -prompt_and_setup_ssl() { - local panel_port="$1" - local web_base_path="$2" # expected without leading slash - local server_ip="$3" - - local ssl_choice="" - - echo -e "${yellow}Choose SSL certificate setup method:${plain}" - echo -e "${green}1.${plain} Let's Encrypt for Domain (90-day validity, auto-renews)" - echo -e "${green}2.${plain} Let's Encrypt for IP Address (6-day validity, auto-renews)" - echo -e "${green}3.${plain} Custom SSL Certificate (Path to existing files)" - echo -e "${green}4.${plain} Skip SSL (advanced — behind reverse proxy / SSH tunnel only)" - echo -e "${blue}Note:${plain} Options 1 & 2 require port 80 open. Option 3 requires manual paths." - echo -e "${blue}Note:${plain} Option 4 serves the panel over plain HTTP — only safe behind nginx/Caddy or an SSH tunnel." - read -rp "Choose an option (default 2 for IP): " ssl_choice - ssl_choice="${ssl_choice// /}" # Trim whitespace - - # Default to 2 (IP cert) if input is empty or invalid (not 1, 3 or 4) - if [[ "$ssl_choice" != "1" && "$ssl_choice" != "3" && "$ssl_choice" != "4" ]]; then - ssl_choice="2" - fi - - case "$ssl_choice" in - 1) - # User chose Let's Encrypt domain option - echo -e "${green}Using Let's Encrypt for domain certificate...${plain}" - if ssl_cert_issue; then - local cert_domain="${SSL_ISSUED_DOMAIN}" - if [[ -z "${cert_domain}" ]]; then - cert_domain=$(~/.acme.sh/acme.sh --list 2> /dev/null | tail -1 | awk '{print $1}') - fi - - if [[ -n "${cert_domain}" ]]; then - SSL_HOST="${cert_domain}" - echo -e "${green}✓ SSL certificate configured successfully with domain: ${cert_domain}${plain}" - else - echo -e "${yellow}SSL setup may have completed, but domain extraction failed${plain}" - SSL_HOST="${server_ip}" - fi - else - echo -e "${red}SSL certificate setup failed for domain mode.${plain}" - SSL_HOST="${server_ip}" - fi - ;; - 2) - # User chose Let's Encrypt IP certificate option - echo -e "${green}Using Let's Encrypt for IP certificate (shortlived profile)...${plain}" - - # Confirm the auto-detected IP before issuing for it: with asymmetric - # routing / multi-WAN the echo services can return a transit address. - local ip_confirm="" - read -rp "Is ${server_ip} the correct incoming public IPv4 address for this server? [Default y]: " ip_confirm - if [[ -n "$ip_confirm" && "$ip_confirm" != "y" && "$ip_confirm" != "Y" ]]; then - server_ip="" - while [[ -z "$server_ip" ]]; do - read -rp "Please enter your server's public IPv4 address: " server_ip - server_ip="${server_ip// /}" - if [[ ! "$server_ip" =~ ^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$ ]]; then - echo -e "${red}Invalid IPv4 address. Please try again.${plain}" - server_ip="" - fi - done - fi - - # Ask for optional IPv6 - local ipv6_addr="" - read -rp "Do you have an IPv6 address to include? (leave empty to skip): " ipv6_addr - ipv6_addr="${ipv6_addr// /}" # Trim whitespace - - # Stop panel if running (port 80 needed) - if [[ $release == "alpine" ]]; then - rc-service x-ui stop > /dev/null 2>&1 - else - systemctl stop x-ui > /dev/null 2>&1 - fi - - setup_ip_certificate "${server_ip}" "${ipv6_addr}" - if [ $? -eq 0 ]; then - SSL_HOST="${server_ip}" - echo -e "${green}✓ Let's Encrypt IP certificate configured successfully${plain}" - else - echo -e "${red}✗ IP certificate setup failed. Please check port 80 is open.${plain}" - SSL_HOST="${server_ip}" - fi - - # Restart panel after SSL is configured (restart applies new cert settings) - if [[ $release == "alpine" ]]; then - rc-service x-ui restart > /dev/null 2>&1 - else - systemctl restart x-ui > /dev/null 2>&1 - fi - - ;; - 3) - # User chose Custom Paths (User Provided) option - echo -e "${green}Using custom existing certificate...${plain}" - local custom_cert="" - local custom_key="" - local custom_domain="" - - # 3.1 Request Domain to compose Panel URL later - read -rp "Please enter domain name certificate issued for: " custom_domain - custom_domain="${custom_domain// /}" # Remove spaces - - # 3.2 Loop for Certificate Path - while true; do - read -rp "Input certificate path (keywords: .crt / fullchain): " custom_cert - # Strip quotes if present - custom_cert=$(echo "$custom_cert" | tr -d '"' | tr -d "'") - - if [[ -f "$custom_cert" && -r "$custom_cert" && -s "$custom_cert" ]]; then - break - elif [[ ! -f "$custom_cert" ]]; then - echo -e "${red}Error: File does not exist! Try again.${plain}" - elif [[ ! -r "$custom_cert" ]]; then - echo -e "${red}Error: File exists but is not readable (check permissions)!${plain}" - else - echo -e "${red}Error: File is empty!${plain}" - fi - done - - # 3.3 Loop for Private Key Path - while true; do - read -rp "Input private key path (keywords: .key / privatekey): " custom_key - # Strip quotes if present - custom_key=$(echo "$custom_key" | tr -d '"' | tr -d "'") - - if [[ -f "$custom_key" && -r "$custom_key" && -s "$custom_key" ]]; then - break - elif [[ ! -f "$custom_key" ]]; then - echo -e "${red}Error: File does not exist! Try again.${plain}" - elif [[ ! -r "$custom_key" ]]; then - echo -e "${red}Error: File exists but is not readable (check permissions)!${plain}" - else - echo -e "${red}Error: File is empty!${plain}" - fi - done - - # 3.4 Apply Settings via x-ui binary - ${xui_folder}/x-ui cert -webCert "$custom_cert" -webCertKey "$custom_key" > /dev/null 2>&1 - - # Set SSL_HOST for composing Panel URL - if [[ -n "$custom_domain" ]]; then - SSL_HOST="$custom_domain" - else - SSL_HOST="${server_ip}" - fi - - echo -e "${green}✓ Custom certificate paths applied.${plain}" - echo -e "${yellow}Note: You are responsible for renewing these files externally.${plain}" - - systemctl restart x-ui > /dev/null 2>&1 || rc-service x-ui restart > /dev/null 2>&1 - ;; - 4) - echo "" - echo -e "${red}⚠ Panel will be installed WITHOUT SSL/TLS.${plain}" - echo -e "${yellow}Login credentials and cookies will travel as plain HTTP.${plain}" - echo -e "${yellow}Only safe when:${plain}" - echo -e "${yellow} • A reverse proxy (nginx, Caddy, Traefik) terminates TLS for you, or${plain}" - echo -e "${yellow} • You access the panel exclusively via SSH tunnel${plain}" - echo "" - - SSL_SCHEME="http" - SSL_HOST="${server_ip}" - - local bind_local="" - read -rp "Bind the panel to 127.0.0.1 only? (recommended — forces SSH tunnel / reverse-proxy access) [y/N]: " bind_local - if [[ "$bind_local" == "y" || "$bind_local" == "Y" ]]; then - ${xui_folder}/x-ui setting -listenIP "127.0.0.1" > /dev/null 2>&1 - SSL_HOST="127.0.0.1" - echo -e "${green}✓ Panel bound to 127.0.0.1 only. It is now unreachable from the public internet.${plain}" - echo "" - echo -e "${green}SSH Port Forwarding — open the panel from your local machine via:${plain}" - echo -e " Standard SSH command:" - echo -e " ${yellow}ssh -L 2222:127.0.0.1:${panel_port} root@${server_ip}${plain}" - echo -e " If using an SSH key:" - echo -e " ${yellow}ssh -i -L 2222:127.0.0.1:${panel_port} root@${server_ip}${plain}" - echo -e " Then open in your browser:" - echo -e " ${yellow}http://localhost:2222/${web_base_path}${plain}" - echo "" - echo -e "${yellow}Alternative: point a reverse proxy (nginx/Caddy) at 127.0.0.1:${panel_port} and let it terminate TLS.${plain}" - else - echo -e "${yellow}Panel will listen on all interfaces over plain HTTP. Make sure something else is terminating TLS in front of it.${plain}" - fi - - systemctl restart x-ui > /dev/null 2>&1 || rc-service x-ui restart > /dev/null 2>&1 - echo -e "${green}✓ SSL setup skipped.${plain}" - ;; - *) - echo -e "${red}Invalid option. Skipping SSL setup.${plain}" - SSL_HOST="${server_ip}" - ;; - esac -} - -config_after_update() { - local panel_needs_restart=0 - - echo -e "${yellow}x-ui settings:${plain}" - ${xui_folder}/x-ui setting -show true - ${xui_folder}/x-ui migrate - - # Properly detect empty cert by checking if cert: line exists and has content after it - local existing_cert=$(${xui_folder}/x-ui setting -getCert true 2> /dev/null | grep 'cert:' | awk -F': ' '{print $2}' | tr -d '[:space:]') - local existing_port=$(${xui_folder}/x-ui setting -show true | grep -Eo 'port: .+' | awk '{print $2}') - local existing_webBasePath=$(${xui_folder}/x-ui setting -show true | grep -Eo 'webBasePath: .+' | awk '{print $2}' | sed 's#^/##') - - # Get server IP - local URL_lists=( - "https://api4.ipify.org" - "https://ipv4.icanhazip.com" - "https://v4.api.ipinfo.io/ip" - "https://ipv4.myexternalip.com/raw" - "https://4.ident.me" - "https://check-host.net/ip" - ) - local server_ip="" - for ip_address in "${URL_lists[@]}"; do - local response=$(curl -s -w "\n%{http_code}" --max-time 3 "${ip_address}" 2> /dev/null) - local http_code=$(echo "$response" | tail -n1) - local ip_result=$(echo "$response" | head -n-1 | tr -d '[:space:]"') - if [[ "${http_code}" == "200" && "${ip_result}" =~ ^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$ ]]; then - server_ip="${ip_result}" - break - fi - done - - if [[ -z "$server_ip" ]]; then - echo -e "${yellow}Could not auto-detect server IP from any provider.${plain}" - while [[ -z "$server_ip" ]]; do - read -rp "Please enter your server's public IPv4 address: " server_ip - server_ip="${server_ip// /}" - if [[ ! "$server_ip" =~ ^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$ ]]; then - echo -e "${red}Invalid IPv4 address. Please try again.${plain}" - server_ip="" - fi - done - fi - - # Handle missing/short webBasePath - if [[ ${#existing_webBasePath} -lt 4 ]]; then - echo -e "${yellow}WebBasePath is missing or too short. Generating a new one...${plain}" - local config_webBasePath=$(gen_random_string 18) - ${xui_folder}/x-ui setting -webBasePath "${config_webBasePath}" - existing_webBasePath="${config_webBasePath}" - panel_needs_restart=1 - echo -e "${green}New WebBasePath: ${config_webBasePath}${plain}" - fi - - # Check and prompt for SSL if missing - if [[ -z "$existing_cert" ]]; then - echo "" - echo -e "${red}═══════════════════════════════════════════${plain}" - echo -e "${red} ⚠ NO SSL CERTIFICATE DETECTED ⚠ ${plain}" - echo -e "${red}═══════════════════════════════════════════${plain}" - echo -e "${yellow}For security, SSL certificate is MANDATORY for all panels.${plain}" - echo -e "${yellow}Let's Encrypt now supports both domains and IP addresses!${plain}" - echo "" - - # Prompt and setup SSL (domain or IP) - prompt_and_setup_ssl "${existing_port}" "${existing_webBasePath}" "${server_ip}" - - echo "" - echo -e "${green}═══════════════════════════════════════════${plain}" - echo -e "${green} Panel Access Information ${plain}" - echo -e "${green}═══════════════════════════════════════════${plain}" - echo -e "${green}Access URL: https://${SSL_HOST}:${existing_port}/${existing_webBasePath}${plain}" - echo -e "${green}═══════════════════════════════════════════${plain}" - echo -e "${yellow}⚠ SSL Certificate: Enabled and configured${plain}" - else - echo -e "${green}SSL certificate is already configured${plain}" - # Show access URL with existing certificate - local cert_domain=$(basename "$(dirname "$existing_cert")") - echo "" - echo -e "${green}═══════════════════════════════════════════${plain}" - echo -e "${green} Panel Access Information ${plain}" - echo -e "${green}═══════════════════════════════════════════${plain}" - echo -e "${green}Access URL: https://${cert_domain}:${existing_port}/${existing_webBasePath}${plain}" - echo -e "${green}═══════════════════════════════════════════${plain}" - fi - - if [[ "$panel_needs_restart" -eq 1 ]]; then - echo -e "${yellow}Restarting panel to apply the new web base path...${plain}" - systemctl restart x-ui 2> /dev/null || rc-service x-ui restart 2> /dev/null - fi -} - -# setup_fail2ban auto-installs and configures fail2ban for the IP Limit feature -# by invoking the freshly downloaded x-ui CLI. IP Limit is load-bearing on -# fail2ban (without it the panel disables the limitIp field and zeroes existing -# limits), so updating an older install should make it work without a manual -# trip through the IP Limit menu. Non-fatal: a fail2ban failure must never abort -# the update. XUI_ENABLE_FAIL2BAN is honored (load_xui_env exports it from the -# persisted env file, so a deliberate opt-out survives updates). -setup_fail2ban() { - if [[ -n "${XUI_ENABLE_FAIL2BAN+x}" && "${XUI_ENABLE_FAIL2BAN}" != "true" ]]; then - echo -e "${yellow}XUI_ENABLE_FAIL2BAN=${XUI_ENABLE_FAIL2BAN}, skipping Fail2ban auto-setup.${plain}" - return 0 - fi - - if [[ ! -x /usr/bin/x-ui ]]; then - echo -e "${yellow}x-ui CLI not found; skipping Fail2ban auto-setup.${plain}" - return 0 - fi - - echo -e "${green}Setting up Fail2ban for the IP Limit feature...${plain}" - if /usr/bin/x-ui setup-fail2ban; then - echo -e "${green}Fail2ban setup complete.${plain}" - else - echo -e "${yellow}Fail2ban setup did not finish; IP Limit stays disabled until you run 'x-ui' and open the IP Limit menu. Continuing.${plain}" - fi - return 0 -} - -# Lands a systemd unit file at ${xui_service}/x-ui.service via a temp file + -# atomic mv, so a failed cp/curl or an interrupted mv never leaves a -# truncated unit file at the live path -- systemd would then fail to parse -# it on the next daemon-reload/start. Same pattern already used for -# /usr/bin/x-ui elsewhere in this script. source_is_url picks cp (from a -# file already extracted from the release tarball) vs curl (GitHub fallback). -_install_xui_service_unit() { - local source="$1" - local source_is_url="$2" - local dest="${xui_service}/x-ui.service" - local temp_file="${dest}.tmp.$$" - - rm -f "$temp_file" - if [[ "$source_is_url" == "true" ]]; then - ${curl_bin} -fLRo "$temp_file" "$source" > /dev/null 2>&1 - else - cp -f "$source" "$temp_file" > /dev/null 2>&1 - fi - if [[ $? -ne 0 ]]; then - rm -f "$temp_file" - return 1 - fi - if [[ ! -s "$temp_file" ]]; then - rm -f "$temp_file" - return 1 - fi - mv -f "$temp_file" "$dest" - if [[ $? -ne 0 ]]; then - rm -f "$temp_file" - return 1 - fi - return 0 -} - -update_x-ui() { - cd ${xui_folder%/x-ui}/ - - load_xui_env - - if [ -f "${xui_folder}/x-ui" ]; then - current_xui_version=$(${xui_folder}/x-ui -v) - echo -e "${green}Current x-ui version: ${current_xui_version}${plain}" - else - _fail "ERROR: Current x-ui version: unknown" - fi - - echo -e "${green}Downloading new x-ui version...${plain}" - - # XUI_UPDATE_TAG lets the panel target a specific release tag (e.g. the - # rolling dev-latest pre-release). Empty keeps the default latest-stable flow. - if [[ -n "${XUI_UPDATE_TAG}" ]]; then - tag_version="${XUI_UPDATE_TAG}" - echo -e "${green}Using update tag: ${tag_version}${plain}" - else - tag_version=$(${curl_bin} -Ls "https://api.github.com/repos/MHSanaei/3x-ui/releases/latest" 2> /dev/null | grep '"tag_name":' | sed -E 's/.*"([^"]+)".*/\1/') - if [[ ! -n "$tag_version" ]]; then - _fail "ERROR: Failed to fetch x-ui version, it may be due to GitHub API restrictions, please try it later" - fi - fi - echo -e "Got x-ui latest version: ${tag_version}, beginning the installation..." - ${curl_bin} -fLRo ${xui_folder}-linux-$(arch).tar.gz https://github.com/MHSanaei/3x-ui/releases/download/${tag_version}/x-ui-linux-$(arch).tar.gz 2> /dev/null - if [[ $? -ne 0 ]]; then - _fail "ERROR: Failed to download x-ui, please be sure that your server can access GitHub" - fi - if [[ ! -s ${xui_folder}-linux-$(arch).tar.gz ]]; then - rm ${xui_folder}-linux-$(arch).tar.gz -f > /dev/null 2>&1 - _fail "ERROR: Downloaded x-ui release archive is empty, please be sure that your server can access GitHub" - fi - - if [[ -e ${xui_folder}/ ]]; then - echo -e "${green}Stopping x-ui...${plain}" - if [[ $release == "alpine" ]]; then - if [ -f "/etc/init.d/x-ui" ]; then - rc-service x-ui stop > /dev/null 2>&1 - rc-update del x-ui > /dev/null 2>&1 - echo -e "${green}Removing old service unit version...${plain}" - rm -f /etc/init.d/x-ui > /dev/null 2>&1 - else - rm x-ui-linux-$(arch).tar.gz -f > /dev/null 2>&1 - _fail "ERROR: x-ui service unit not installed." - fi - else - if [ -f "${xui_service}/x-ui.service" ]; then - systemctl stop x-ui > /dev/null 2>&1 - systemctl disable x-ui > /dev/null 2>&1 - echo -e "${green}Removing old systemd unit version...${plain}" - rm ${xui_service}/x-ui.service -f > /dev/null 2>&1 - systemctl daemon-reload > /dev/null 2>&1 - else - rm x-ui-linux-$(arch).tar.gz -f > /dev/null 2>&1 - _fail "ERROR: x-ui systemd unit not installed." - fi - fi - # Kill any leftover mtg (MTProto) sidecars. x-ui runs them outside its own - # lifecycle, so on Linux a stale one can survive the stop and keep holding - # an inbound port with an outdated secret, silently breaking new clients. - # The new panel respawns a clean mtg per inbound on next start. - pkill -f 'mtg-linux-[^ ]* run ' > /dev/null 2>&1 || true - echo -e "${green}Removing old x-ui version...${plain}" - rm ${xui_folder} -f > /dev/null 2>&1 - rm ${xui_folder}/x-ui.service -f > /dev/null 2>&1 - rm ${xui_folder}/x-ui.service.debian -f > /dev/null 2>&1 - rm ${xui_folder}/x-ui.service.arch -f > /dev/null 2>&1 - rm ${xui_folder}/x-ui.service.rhel -f > /dev/null 2>&1 - rm ${xui_folder}/x-ui -f > /dev/null 2>&1 - rm ${xui_folder}/x-ui.sh -f > /dev/null 2>&1 - echo -e "${green}Removing old mtg version...${plain}" - rm ${xui_folder}/bin/mtg-linux-$(arch) -f > /dev/null 2>&1 - echo -e "${green}Removing old xray version...${plain}" - rm ${xui_folder}/bin/xray-linux-$(arch) -f > /dev/null 2>&1 - echo -e "${green}Removing old README and LICENSE file...${plain}" - rm ${xui_folder}/bin/README.md -f > /dev/null 2>&1 - rm ${xui_folder}/bin/LICENSE -f > /dev/null 2>&1 - else - rm x-ui-linux-$(arch).tar.gz -f > /dev/null 2>&1 - _fail "ERROR: x-ui not installed." - fi - - echo -e "${green}Installing new x-ui version...${plain}" - tar zxvf x-ui-linux-$(arch).tar.gz > /dev/null 2>&1 - if [[ $? -ne 0 ]]; then - rm x-ui-linux-$(arch).tar.gz -f > /dev/null 2>&1 - _fail "ERROR: Failed to extract the x-ui release archive -- the previous installation has already been removed, so the panel will not start until this is fixed; try running the update again" - fi - rm x-ui-linux-$(arch).tar.gz -f > /dev/null 2>&1 - cd x-ui > /dev/null 2>&1 - if [[ $? -ne 0 || ! -s x-ui ]]; then - _fail "ERROR: Extracted x-ui archive is missing the x-ui binary -- the previous installation has already been removed, so the panel will not start until this is fixed; try running the update again" - fi - chmod +x x-ui > /dev/null 2>&1 - - # Check the system's architecture and rename the file accordingly. - # The panel binary maps GOARCH=arm to "arm32" (internal/xray/process.go), - # so the Xray binary must be named xray-linux-arm32; mtg keeps plain "arm". - if [[ $(arch) == "armv5" || $(arch) == "armv6" || $(arch) == "armv7" ]]; then - mv bin/xray-linux-$(arch) bin/xray-linux-arm32 > /dev/null 2>&1 - chmod +x bin/xray-linux-arm32 > /dev/null 2>&1 - if [[ -f bin/mtg-linux-$(arch) ]]; then - mv bin/mtg-linux-$(arch) bin/mtg-linux-arm > /dev/null 2>&1 - chmod +x bin/mtg-linux-arm > /dev/null 2>&1 - fi - fi - - chmod +x x-ui bin/xray-linux-$(arch) > /dev/null 2>&1 - if [[ -f bin/mtg-linux-arm ]]; then - chmod +x bin/mtg-linux-arm > /dev/null 2>&1 - elif [[ -f bin/mtg-linux-$(arch) ]]; then - chmod +x bin/mtg-linux-$(arch) > /dev/null 2>&1 - fi - - echo -e "${green}Downloading and installing x-ui.sh script...${plain}" - local xui_script_temp="/usr/bin/x-ui-temp.$$" - rm -f "${xui_script_temp}" - ${curl_bin} -fLRo "${xui_script_temp}" https://raw.githubusercontent.com/MHSanaei/3x-ui/main/x-ui.sh > /dev/null 2>&1 - if [[ $? -ne 0 ]]; then - rm -f "${xui_script_temp}" - _fail "ERROR: Failed to download x-ui.sh script, please be sure that your server can access GitHub" - fi - if [[ ! -s "${xui_script_temp}" ]]; then - rm -f "${xui_script_temp}" - _fail "ERROR: Downloaded x-ui.sh script is empty, please be sure that your server can access GitHub" - fi - mv -f "${xui_script_temp}" /usr/bin/x-ui - if [[ $? -ne 0 ]]; then - rm -f "${xui_script_temp}" - _fail "ERROR: Failed to install x-ui.sh script" - fi - - chmod +x ${xui_folder}/x-ui.sh > /dev/null 2>&1 - chmod +x /usr/bin/x-ui > /dev/null 2>&1 - mkdir -p /var/log/x-ui > /dev/null 2>&1 - - echo -e "${green}Changing owner...${plain}" - chown -R root:root ${xui_folder} > /dev/null 2>&1 - - if [ -f "${xui_folder}/bin/config.json" ]; then - echo -e "${green}Changing on config file permissions...${plain}" - chmod 640 ${xui_folder}/bin/config.json > /dev/null 2>&1 - fi - - if [[ $release == "alpine" ]]; then - echo -e "${green}Downloading and installing startup unit x-ui.rc...${plain}" - xui_rc_temp="/etc/init.d/x-ui.tmp.$$" - rm -f "${xui_rc_temp}" - ${curl_bin} -fLRo "${xui_rc_temp}" https://raw.githubusercontent.com/MHSanaei/3x-ui/main/x-ui.rc > /dev/null 2>&1 - if [[ $? -ne 0 ]]; then - rm -f "${xui_rc_temp}" - _fail "ERROR: Failed to download startup unit x-ui.rc, please be sure that your server can access GitHub" - fi - if [[ ! -s "${xui_rc_temp}" ]]; then - rm -f "${xui_rc_temp}" - _fail "ERROR: Downloaded startup unit x-ui.rc is empty, please be sure that your server can access GitHub" - fi - mv -f "${xui_rc_temp}" /etc/init.d/x-ui - if [[ $? -ne 0 ]]; then - rm -f "${xui_rc_temp}" - _fail "ERROR: Failed to install startup unit x-ui.rc" - fi - chmod +x /etc/init.d/x-ui > /dev/null 2>&1 - chown root:root /etc/init.d/x-ui > /dev/null 2>&1 - rc-update add x-ui > /dev/null 2>&1 - rc-service x-ui start > /dev/null 2>&1 - else - if [ -f "x-ui.service" ]; then - echo -e "${green}Installing systemd unit...${plain}" - if ! _install_xui_service_unit "x-ui.service" "false"; then - echo -e "${red}Failed to copy x-ui.service${plain}" - exit 1 - fi - else - service_installed=false - case "${release}" in - ubuntu | debian | armbian) - if [ -f "x-ui.service.debian" ]; then - echo -e "${green}Installing debian-like systemd unit...${plain}" - if _install_xui_service_unit "x-ui.service.debian" "false"; then - service_installed=true - fi - fi - ;; - arch | manjaro | parch) - if [ -f "x-ui.service.arch" ]; then - echo -e "${green}Installing arch-like systemd unit...${plain}" - if _install_xui_service_unit "x-ui.service.arch" "false"; then - service_installed=true - fi - fi - ;; - *) - if [ -f "x-ui.service.rhel" ]; then - echo -e "${green}Installing rhel-like systemd unit...${plain}" - if _install_xui_service_unit "x-ui.service.rhel" "false"; then - service_installed=true - fi - fi - ;; - esac - - # If service file not found in tar.gz, download from GitHub - if [ "$service_installed" = false ]; then - echo -e "${yellow}Service files not found in tar.gz, downloading from GitHub...${plain}" - case "${release}" in - ubuntu | debian | armbian) - service_unit_url="https://raw.githubusercontent.com/MHSanaei/3x-ui/main/x-ui.service.debian" - ;; - arch | manjaro | parch) - service_unit_url="https://raw.githubusercontent.com/MHSanaei/3x-ui/main/x-ui.service.arch" - ;; - *) - service_unit_url="https://raw.githubusercontent.com/MHSanaei/3x-ui/main/x-ui.service.rhel" - ;; - esac - - if ! _install_xui_service_unit "$service_unit_url" "true"; then - echo -e "${red}Failed to install x-ui.service from GitHub${plain}" - exit 1 - fi - fi - fi - chown root:root ${xui_service}/x-ui.service > /dev/null 2>&1 - chmod 644 ${xui_service}/x-ui.service > /dev/null 2>&1 - systemctl daemon-reload > /dev/null 2>&1 - systemctl enable x-ui > /dev/null 2>&1 - systemctl start x-ui > /dev/null 2>&1 - fi - - config_after_update - - # IP Limit relies on fail2ban; install + configure it now so the feature - # works out of the box on update too (no-op when XUI_ENABLE_FAIL2BAN=false). - # Never fatal. - setup_fail2ban - - echo -e "${green}x-ui ${tag_version}${plain} updating finished, it is running now..." - echo -e "" - echo -e "┌───────────────────────────────────────────────────────┐ -│ ${blue}x-ui control menu usages (subcommands):${plain} │ -│ │ -│ ${blue}x-ui${plain} - Admin Management Script │ -│ ${blue}x-ui start${plain} - Start │ -│ ${blue}x-ui stop${plain} - Stop │ -│ ${blue}x-ui restart${plain} - Restart │ -│ ${blue}x-ui status${plain} - Current Status │ -│ ${blue}x-ui settings${plain} - Current Settings │ -│ ${blue}x-ui enable${plain} - Enable Autostart on OS Startup │ -│ ${blue}x-ui disable${plain} - Disable Autostart on OS Startup │ -│ ${blue}x-ui log${plain} - Check logs │ -│ ${blue}x-ui banlog${plain} - Check Fail2ban ban logs │ -│ ${blue}x-ui update${plain} - Update │ -│ ${blue}x-ui legacy${plain} - Legacy version │ -│ ${blue}x-ui install${plain} - Install │ -│ ${blue}x-ui uninstall${plain} - Uninstall │ -└───────────────────────────────────────────────────────┘" -} - -echo -e "${green}Running...${plain}" -install_base -update_x-ui $1 +# 3x-ui-agent (fork) — the upstream updater is intentionally disabled in +# this fork: running it would overwrite this fork with the stock 3x-ui panel. +# The panel's built-in self-update downloads this file from this fork's own +# repository, so it lands here and stops instead of replacing the fork. +# 本 fork 不提供上游一键更新(运行上游脚本会把 fork 覆盖成原版 3x-ui)。 +# 请从本仓 Releases 手动下载新版二进制替换:https://git.881218.xyz/rogue/3x-ui-agent/releases +echo "This fork (3x-ui-agent) does not provide upstream one-click updates; download new builds manually from https://git.881218.xyz/rogue/3x-ui-agent/releases" +echo "本 fork 不提供上游一键更新(会覆盖成原版)。请从本仓 Releases 手动下载替换:https://git.881218.xyz/rogue/3x-ui-agent/releases" +exit 1 diff --git a/x-ui.sh b/x-ui.sh index 9dcf27c..3512025 100644 --- a/x-ui.sh +++ b/x-ui.sh @@ -129,14 +129,16 @@ before_show_menu() { } install() { - bash <(curl -Ls https://raw.githubusercontent.com/MHSanaei/3x-ui/main/install.sh) - if [[ $? == 0 ]]; then - if [[ $# == 0 ]]; then - start - else - start 0 - fi + # Fork build (3x-ui-agent): the upstream one-click installer is disabled — + # it would install the stock panel over this fork. + # 本 fork 已禁用上游一键安装(会装成原版)。请从本仓 Releases 手动下载部署: + echo -e "${yellow}This fork does not provide the upstream one-click installer (it installs the stock panel).${plain}" + echo -e "${yellow}Please download the binary manually from this fork's Releases:${plain}" + echo -e "${green}https://git.881218.xyz/rogue/3x-ui-agent/releases${plain}" + if [[ $# == 0 ]]; then + before_show_menu fi + return 1 } update() { @@ -148,11 +150,16 @@ update() { fi return 0 fi - bash <(curl -Ls https://raw.githubusercontent.com/MHSanaei/3x-ui/main/update.sh) - if [[ $? == 0 ]]; then - LOGI "Update is complete, Panel has automatically restarted " + # Fork build (3x-ui-agent): the upstream updater is disabled — it would + # overwrite this fork with the stock panel. + # 本 fork 已禁用上游更新(会覆盖成原版)。请手动从本仓 Releases 下载新版二进制替换: + echo -e "${yellow}This fork cannot be updated via the upstream updater (it would overwrite the fork with the stock panel).${plain}" + echo -e "${yellow}Please download the new binary manually from this fork's Releases and replace it:${plain}" + echo -e "${green}https://git.881218.xyz/rogue/3x-ui-agent/releases${plain}" + if [[ $# == 0 ]]; then before_show_menu fi + return 1 } update_dev() { @@ -164,13 +171,16 @@ update_dev() { fi return 0 fi - # XUI_UPDATE_TAG tells update.sh to install the dev-latest pre-release - # instead of the latest stable tag. - XUI_UPDATE_TAG="dev-latest" bash <(curl -Ls https://raw.githubusercontent.com/MHSanaei/3x-ui/main/update.sh) - if [[ $? == 0 ]]; then - LOGI "Dev update is complete, Panel has automatically restarted " + # Fork build (3x-ui-agent): there is no upstream dev channel for this + # fork, and the upstream updater would overwrite it with the stock panel. + # 本 fork 无上游 dev 更新通道。请手动从本仓 Releases 下载新版二进制替换: + echo -e "${yellow}This fork has no upstream dev-channel update (the upstream updater would overwrite the fork).${plain}" + echo -e "${yellow}Please download the new binary manually from this fork's Releases and replace it:${plain}" + echo -e "${green}https://git.881218.xyz/rogue/3x-ui-agent/releases${plain}" + if [[ $# == 0 ]]; then before_show_menu fi + return 1 } replace_xui_script() { @@ -219,29 +229,28 @@ update_menu() { return 0 fi - if replace_xui_script "https://raw.githubusercontent.com/MHSanaei/3x-ui/main/x-ui.sh" "false"; then - chmod +x ${xui_folder}/x-ui.sh - echo -e "${green}Update successful. The panel has automatically restarted.${plain}" - exit 0 - else - echo -e "${red}Failed to update the menu.${plain}" - return 1 + # Fork build (3x-ui-agent): the menu script is maintained in this fork's + # own repository; replacing it from upstream is disabled. + # 本 fork 的菜单脚本随本仓维护,已禁用从上游自替换: + echo -e "${yellow}This fork's menu script is maintained in its own repository; upstream self-update is disabled.${plain}" + echo -e "${green}https://git.881218.xyz/rogue/3x-ui-agent${plain}" + if [[ $# == 0 ]]; then + before_show_menu fi + return 1 } legacy_version() { - echo -n "Enter the panel version (like 2.4.0):" - read -r tag_version - - if [ -z "$tag_version" ]; then - echo "Panel version cannot be empty. Exiting." - exit 1 + # Fork build (3x-ui-agent): installing upstream legacy versions is + # disabled — it would replace this fork with the stock panel. + # 本 fork 已禁用安装上游旧版本(会装成原版)。本 fork 的可用构建见 Releases: + echo -e "${yellow}Installing upstream legacy versions is disabled in this fork.${plain}" + echo -e "${yellow}For available builds of this fork, see its Releases:${plain}" + echo -e "${green}https://git.881218.xyz/rogue/3x-ui-agent/releases${plain}" + if [[ $# == 0 ]]; then + before_show_menu fi - # Use the entered panel version in the download link - install_command="bash <(curl -Ls "https://raw.githubusercontent.com/mhsanaei/3x-ui/v$tag_version/install.sh") v$tag_version" - - echo "Downloading and installing panel version $tag_version..." - eval $install_command + return 1 } # Function to handle the deletion of the script file @@ -302,8 +311,8 @@ uninstall() { echo "" echo -e "Uninstalled Successfully.\n" - echo "If you need to install this panel again, you can use below command:" - echo -e "${green}bash <(curl -Ls https://raw.githubusercontent.com/mhsanaei/3x-ui/master/install.sh)${plain}" + echo "If you need to install this fork again, download the binary manually from its Releases page:" + echo -e "${green}https://git.881218.xyz/rogue/3x-ui-agent/releases${plain}" echo "" # Trap the SIGTERM signal trap delete_script SIGTERM @@ -836,14 +845,12 @@ enable_bbr() { } update_shell() { - if replace_xui_script "https://github.com/MHSanaei/3x-ui/raw/main/x-ui.sh" "true"; then - LOGI "Upgrade script succeeded, Please rerun the script" - before_show_menu - else - echo "" - LOGE "Failed to download script, Please check whether the machine can connect Github" - before_show_menu - fi + # Fork build (3x-ui-agent): upstream self-update of this script is + # disabled; the script is maintained in this fork's own repository. + # 本 fork 已禁用脚本自更新(下载源为上游)。脚本随本仓维护: + echo -e "${yellow}Self-update is disabled in this fork; the script is maintained in its own repository:${plain}" + echo -e "${green}https://git.881218.xyz/rogue/3x-ui-agent${plain}" + before_show_menu } xui_pid() {