From 195988bdc138399e91c7acfa6d0d397c3b1e7e94 Mon Sep 17 00:00:00 2001 From: ilyusha Date: Thu, 3 Sep 2026 17:50:53 +0300 Subject: [PATCH] fix(install): fetch x-ui.sh and unit files from the installed release tag (#6391) * fix(install): fetch x-ui.sh and unit files from the installed release tag install.sh and update.sh pin the panel archive to a release tag but always took x-ui.sh, x-ui.rc and the service units from main, so the management script and the binary of one installation came from different commits: the fail2ban templates and setting flags the script writes drift silently against an older binary, two installs of the same tag differ, and a reviewed or digest-pinned installer still runs unreviewed code from main. Use the same ref as the archive, keeping main only for the rolling dev-latest build. The menu's "update menu" and update_shell paths now fetch the script matching the installed version and fall back to main with a visible notice when no script is published for it. Assisted-by: Claude Code:claude-fable-5-1 * fix(install): fall back to main for files a pinned tag does not publish Review follow-ups. install.sh accepts tags down to v2.3.5, but x-ui.rc only exists from v2.8.4 and the split x-ui.service.* files are newer still, so pinning those to the tag made an Alpine install of an old tag 404 after the previous install was already removed. Probe the tag for each file and fall back to main with a notice when it is missing, as the menu already does for x-ui.sh. The fail2ban auto-setup probe also trusted the exit status of 'x-ui setup-fail2ban', but scripts before v3.4.0 have no such subcommand and exit 0 from the usage banner, so the installer reported a setup that never ran. Skip with a notice when the installed script does not know the subcommand. Assisted-by: Claude Code:claude-fable-5-1 * fix(install): refuse a tag that does not publish a needed script Falling back to main reintroduced the binary/script mismatch the tag pinning exists to remove, and it fired at points where install.sh and update.sh have already stopped and removed the previous installation -- so the quiet path was also the one that could not be undone. Probe the tag instead, before anything is touched, for every file that is always fetched from GitHub (x-ui.sh, plus x-ui.rc on Alpine), and abort with the HTTP status when one is missing. The unit files stay unprobed: they are only fetched when the release tarball omits them, so an old tag that ships x-ui.service inside its tarball still installs. Their existing failure message now names the ref it tried. Also tighten the setup-fail2ban probe to the dispatcher's case arm rather than any mention of the string, which also matches a comment. --------- Co-authored-by: Claude --- install.sh | 47 +++++++++++++++++++++++++++++++++++++++++------ update.sh | 45 +++++++++++++++++++++++++++++++++++++++------ x-ui.sh | 17 +++++++++++++++-- 3 files changed, 95 insertions(+), 14 deletions(-) diff --git a/install.sh b/install.sh index 919024f09..ee0bc2584 100644 --- a/install.sh +++ b/install.sh @@ -1368,6 +1368,13 @@ setup_fail2ban() { return 0 fi + # Scripts older than v3.4.0 have no setup-fail2ban and exit 0 from the + # usage banner, which would read as success here. + if ! grep -q '"setup-fail2ban")' /usr/bin/x-ui; then + echo -e "${yellow}This x-ui.sh predates 'x-ui setup-fail2ban'; 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}" @@ -1426,6 +1433,23 @@ resolve_latest_tag() { 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/' } +# Older tags predate some of these files (x-ui.rc arrived in v2.8.4). Serving +# main's copy against an old binary is the mismatch this pinning exists to +# prevent, so probe before anything is stopped or removed and refuse the tag. +require_repo_files() { + local ref="$1" name status + shift + [[ "${ref}" == "main" ]] && return 0 + for name in "$@"; do + status=$(curl -sIL --retry 3 --connect-timeout 15 -o /dev/null -w '%{http_code}' "https://raw.githubusercontent.com/MHSanaei/3x-ui/${ref}/${name}") + if [[ "${status}" != "200" ]]; then + echo -e "${red}${name} is not available for ${ref} (HTTP ${status})${plain}" + echo -e "${red}Install a release that ships it, or 'dev' for the rolling build. Your existing installation has not been touched.${plain}" + exit 1 + fi + done +} + install_x-ui() { cd ${xui_folder%/x-ui}/ @@ -1478,9 +1502,20 @@ install_x-ui() { exit 1 fi fi + # x-ui.sh, x-ui.rc and the unit files must come from the same release as + # the binary; only the rolling dev build tracks main. + local script_ref="${tag_version}" + if [[ "${tag_version}" == "dev-latest" ]]; then + script_ref="main" + fi + # The unit files are only fetched when the release tarball lacks them, so + # they are checked at that point instead of here. + local required_files=("x-ui.sh") + [[ $release == "alpine" ]] && required_files+=("x-ui.rc") + require_repo_files "${script_ref}" "${required_files[@]}" 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 + curl -fLRo "${xui_script_temp}" "https://raw.githubusercontent.com/MHSanaei/3x-ui/${script_ref}/x-ui.sh" if [[ $? -ne 0 ]]; then rm -f "${xui_script_temp}" echo -e "${red}Failed to download x-ui.sh${plain}" @@ -1631,7 +1666,7 @@ install_x-ui() { 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 + curl -fLRo "${xui_rc_temp}" "https://raw.githubusercontent.com/MHSanaei/3x-ui/${script_ref}/x-ui.rc" if [[ $? -ne 0 ]]; then rm -f "${xui_rc_temp}" echo -e "${red}Failed to download x-ui.rc${plain}" @@ -1696,18 +1731,18 @@ install_x-ui() { 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" + service_unit_url="https://raw.githubusercontent.com/MHSanaei/3x-ui/${script_ref}/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/${script_ref}/x-ui.service.arch" ;; *) - service_unit_url="https://raw.githubusercontent.com/MHSanaei/3x-ui/main/x-ui.service.rhel" + service_unit_url="https://raw.githubusercontent.com/MHSanaei/3x-ui/${script_ref}/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}" + echo -e "${red}Failed to install x-ui.service from GitHub (${script_ref}) -- the release tarball did not ship one either${plain}" exit 1 fi service_installed=true diff --git a/update.sh b/update.sh index e3945b0a8..e972d84b7 100755 --- a/update.sh +++ b/update.sh @@ -924,6 +924,13 @@ setup_fail2ban() { return 0 fi + # Scripts older than v3.4.0 have no setup-fail2ban and exit 0 from the + # usage banner, which would read as success here. + if ! grep -q '"setup-fail2ban")' /usr/bin/x-ui; then + echo -e "${yellow}This x-ui.sh predates 'x-ui setup-fail2ban'; 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}" @@ -967,6 +974,21 @@ _install_xui_service_unit() { return 0 } +# Older tags predate some of these files (x-ui.rc arrived in v2.8.4). Serving +# main's copy against an old binary is the mismatch this pinning exists to +# prevent, so probe before the old install is removed and refuse the tag. +require_repo_files() { + local ref="$1" name status + shift + [[ "${ref}" == "main" ]] && return 0 + for name in "$@"; do + status=$(${curl_bin} -sIL --retry 3 --connect-timeout 15 -o /dev/null -w '%{http_code}' "https://raw.githubusercontent.com/MHSanaei/3x-ui/${ref}/${name}") + if [[ "${status}" != "200" ]]; then + _fail "ERROR: ${name} is not available for ${ref} (HTTP ${status}). Update to a release that ships it, or to 'dev-latest'. The current installation is untouched." + fi + done +} + update_x-ui() { cd ${xui_folder%/x-ui}/ @@ -993,6 +1015,17 @@ update_x-ui() { fi fi echo -e "Got x-ui latest version: ${tag_version}, beginning the installation..." + # x-ui.sh, x-ui.rc and the unit files must come from the same release as + # the binary; only the rolling dev build tracks main. + script_ref="${tag_version}" + if [[ "${tag_version}" == "dev-latest" ]]; then + script_ref="main" + fi + # The unit files are only fetched when the release tarball lacks them, so + # they are checked at that point instead of here. + local required_files=("x-ui.sh") + [[ $release == "alpine" ]] && required_files+=("x-ui.rc") + require_repo_files "${script_ref}" "${required_files[@]}" ${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" @@ -1086,7 +1119,7 @@ update_x-ui() { 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 + ${curl_bin} -fLRo "${xui_script_temp}" "https://raw.githubusercontent.com/MHSanaei/3x-ui/${script_ref}/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" @@ -1117,7 +1150,7 @@ update_x-ui() { 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 + ${curl_bin} -fLRo "${xui_rc_temp}" "https://raw.githubusercontent.com/MHSanaei/3x-ui/${script_ref}/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" @@ -1176,18 +1209,18 @@ update_x-ui() { 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" + service_unit_url="https://raw.githubusercontent.com/MHSanaei/3x-ui/${script_ref}/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/${script_ref}/x-ui.service.arch" ;; *) - service_unit_url="https://raw.githubusercontent.com/MHSanaei/3x-ui/main/x-ui.service.rhel" + service_unit_url="https://raw.githubusercontent.com/MHSanaei/3x-ui/${script_ref}/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}" + echo -e "${red}Failed to install x-ui.service from GitHub (${script_ref}) -- the release tarball did not ship one either${plain}" exit 1 fi fi diff --git a/x-ui.sh b/x-ui.sh index 9dcf27c0a..e9d74a9a5 100644 --- a/x-ui.sh +++ b/x-ui.sh @@ -208,6 +208,19 @@ replace_xui_script() { return 0 } +# The menu must match the installed panel, so update it from that release's +# tag; fall back to main only when no script is published for the version. +installed_script_url() { + local ver + ver=$("${xui_folder}/x-ui" -v 2> /dev/null | tr -d '[:space:]') + if [[ "$ver" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]] && curl -fsIL -o /dev/null "https://raw.githubusercontent.com/MHSanaei/3x-ui/v${ver}/x-ui.sh"; then + echo "https://raw.githubusercontent.com/MHSanaei/3x-ui/v${ver}/x-ui.sh" + else + echo -e "${yellow}No x-ui.sh published for the installed version (${ver:-unknown}), using main${plain}" >&2 + echo "https://raw.githubusercontent.com/MHSanaei/3x-ui/main/x-ui.sh" + fi +} + update_menu() { echo -e "${yellow}Updating Menu${plain}" confirm "This function will update the menu to the latest changes." "y" @@ -219,7 +232,7 @@ update_menu() { return 0 fi - if replace_xui_script "https://raw.githubusercontent.com/MHSanaei/3x-ui/main/x-ui.sh" "false"; then + if replace_xui_script "$(installed_script_url)" "false"; then chmod +x ${xui_folder}/x-ui.sh echo -e "${green}Update successful. The panel has automatically restarted.${plain}" exit 0 @@ -836,7 +849,7 @@ enable_bbr() { } update_shell() { - if replace_xui_script "https://github.com/MHSanaei/3x-ui/raw/main/x-ui.sh" "true"; then + if replace_xui_script "$(installed_script_url)" "true"; then LOGI "Upgrade script succeeded, Please rerun the script" before_show_menu else